JavaAgent探针技术第一篇:在主程序运行之前的代理程序

一、Java Agent是什么?

通过操作Instrumentation的api就可以实现不重启服务对单个类进行简单的修改。Instrumentation是一个interface,它的实现类InstrumentationImpl只有一个private的构造方法。

怎么拿到这个对象呢?下面是Instrumentation类的一段注释说明:

There are two ways to botain an instance of the Instrumentation interface:

  1. When a JVM is launched in a way that indicates an agent class. In that case an Instrumentation instance is passed to the parent method of the agent class.
  2. When a JVM provides a mechanism to start agents sometime after the JVM is
    launched . In that case an Instrumentation instance is passed yo the agentmain method of the agent code.
    These mechainsms are described in the package specification.
    Once an agent acquires an Instrumentation instance, the agent may call methods on the instance at any time.

这里对上面的英文原文的注释做一个简单的理解:一共有两种方式拿到Instrumentation对象:

  1. JVM启动时指定agent,Instrumentation对象会通过agent的premain方法传递过去。
  2. JVM启动后通过JVM提供的机制加载agent,Instrumentation对象会通过agent的agentmain方法传递过去。

本文重点介绍,在主程序运行之前启动agent的基本用法。

二、如何使用Java Agent技术?

2.1 简单示例

新起一个简单的Java工程AgentDemo,新建包路径com.alibaba.ei.agent,新增一个类AgentDemo,编写代码如下:

public class AgentDemo {

    private static Instrumentation instrumentation;

    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     *
     * @param agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
     * @param inst      是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("=========premain方法执行1========");
        System.out.println(agentArgs);

        instrumentation = inst;

        SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer(transformer);
    }

    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst)
     * 则会执行 premain(String agentArgs)
     *
     * @param agentArgs
     * @author xifeijian
     * @create 2018年4月18日
     */
    public static void premain(String agentArgs) {
        System.out.println("=========premain方法执行2========");
        System.out.println(agentArgs);
    }

}

在这个 premain 函数中,开发者可以进行对类的各种操作。

  1. agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
  2. Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,它是agent技术主要使用的API,我们可以使用它来改变和重新定义类的行为。

编写转换类SimpleClassTransformer:

/**
 * @Description
 * @Author louxiujun
 * @Date 2020/2/4 11:49
 **/
public class SimpleClassTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) {
            ClassPool classPool = ClassPool.getDefault();
            CtClass clazz = null;
            try {
                clazz = classPool.get("sun.net.www.protocol.http.HttpURLConnection");

                CtConstructor[] cs = clazz.getConstructors();
                for (CtConstructor constructor : cs) {
                    // 在构造函数结束的位置插入如下的内容
                    constructor.insertAfter("System.out.println(this.getURL());");
                }

                byte[] byteCode = clazz.toBytecode();

                // 将类移出
                clazz.detach();

                return byteCode;
            } catch (NotFoundException | CannotCompileException | IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

写完这个类后,我们还需要做一步配置工作。
1)如果项目是普通Java项目的话,则在 src 目录下生成 META-INF/MANIFEST.MF 文件。
切换到工程设置面板,切换到Artifacts面板,点击按钮,新增一个JAR,选择From modules with dependencies...选项,如下图所示:

20191222203456.jpg

Main Class一栏留空不填,下面的单选按钮选择copy to the output directory and link via manifest选项,其他的按照默认生成的走就可以,完成之后点击OK按钮。

20191222203522.jpg

完成之后面板显示如下,点击OK按钮完成配置。

20191222203534.jpg

然后编辑META-INF/MANIFEST.MF,MANIFEST.MF文件用于描述Jar包的信息,例如指定入口函数等。我们需要在该文件中加入如下配置,指定我们编写的含有premain方法类的全路径,然后将agent类打成Jar包。

1 Manifest-Version: 1.0
2 Premain-Class: com.alibaba.ei.agent.AgentDemo
3
4

要特别注意的是:最后一行是空行,还有就是Premain-Class冒号后面有个空格。

接下来选择菜单栏中的Build下拉中的Build Artifacts..选项,

20191222203943.jpg

然后我们在弹出的快捷菜单中选择Action为Build,从而将整个工程打包代码为 javaagent.jar

20191222203957.jpg

此时,会在工程目录的out文件夹成生成一个jar文件,复制下这个jar文件的绝对路径备用。本文的jar文件所在目录为/Users/XXX/Documents/code/javaagent/agentDemo/out/artifacts/agentDemo_jar/agentDemo.jar。当前整个工程的结构图如下图所示:

image.png

2)如果你是使用Maven来构建的项目,则在pom.xml文件中添加如下的内容,Maven帮助生成MANIFEST.MF文件。

 
        
            
                org.apache.maven.plugins
                maven-jar-plugin
                 2.3.1
                
                    
                        
                            true
                        
                        
                            
                            com.alibaba.ei.agent.AgentDemo
                        
                    
                
            
        
    

再添加一下pom依赖:

3.20.0-GA


            org.javassist
            javassist
            ${javassist.version}
        

执行mvn clean install指令,则会在当前工程目录下生成一个target文件夹,里面有一个agentDemo-1.0-SNAPSHOT.jar的jar包,拷贝起文件路径备用即可,感兴趣的可以解压看一下里面是否有一个MANIFEST.MF文件。

接着我们再创建一个新的工程agentTest,新建包路径com.alibaba.ei.agent,新建文件AgentTest.java

public class AgentTest {

    public static void main(String[] args) {
        System.out.println("===========执行main方法=============");
        HttpUtil.fetch("http://www.baidu.com");
        HttpUtil.fetch("http://www.163.com");
    }
}

这里的程序就是我们要代理的程序,我们在主程序的VM options添加上启动参数

-javaagent: 你的路径/test-1.0-SNAPSHOT.jar=hello

其中hello为上文中传入permain方法的agentArgs参数。运行我们的主程序

编辑应用的JVM启动参数如下


image.png

点击运行按钮后输出如下:

=========premain方法执行1========
 Hello
=========premain方法执行2========
World
===========执行main方法=============
http://www.baidu.com
Content size:2283
http://www.163.com
Content size:488569

我们也可以将项目打包成jar包,再以命令行的方式启动:

java -javaagent:/Users/XXX/Documents/code/javaagent/agentDemo/out/artifacts/agentDemo_jar/agentDemo.jar=Hello -javaagent:/Users/XXX/Documents/code/javaagent/agentDemo/out/artifacts/agentDemo_jar/agentDemo.jar=World -jar agentTest.jar
objc[18080]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/bin/java (0x106aea4c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x106b964e0). One of the two will be used. Which one is undefined.
=========premain方法执行1========
Hello
=========premain方法执行2========
World
=========Main主方法执行=========
http://www.baidu.com
Content size:2283
http://www.163.com
Content size:488569

特别提醒:如果你把 -javaagent相关的参数放在-jar相关参数的后面,则不会生效。也就是说,放在主程序后面的 agent 是无效的。

四、参考资料

  1. 一个最简单的javaagent demo实例

你可能感兴趣的:(JavaAgent探针技术第一篇:在主程序运行之前的代理程序)