Java Agent入门

1,135 阅读3分钟

Jdk1.5时我们可以使用premain方法使用静态代理,需要命令行制定代理jar Jdk1.6时提供了agentmain方法进行运行时代理,需要attach到目标jvm的pid上

premain的用法

###新建代理工程java-agent,后面需要打包成java-agent.jar 编写premain方法

public static void premain(String agentOps, Instrumentation inst) {
    System.out.println("====premain 方法执行");
    System.out.println(agentOps);
    inst.addTransformer(new MyTransformer());
} 

编写MyTransformer字节转换类,这个类需要继承ClassFileTransformer

 1public class MyTransformer implements ClassFileTransformer {
 2
 3    final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
 4    final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";
 5
 6
 7    @Override
 8    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
 9        final String classReName = className.replace("/", ".");
10        if (classReName.equals("com.lengye.demo.MainClass")) {
11            CtClass ctclass = null;
12            try {
13                ctclass = ClassPool.getDefault().get(classReName);
14                CtMethod[] declaredMethods = ctclass.getDeclaredMethods();
15
16                for (CtMethod method : declaredMethods) {
17                    String methodName = method.getName();
18                    System.out.println("methodName:" + methodName);
19                    String outputStr = "\nSystem.out.println(\"this method " + methodName
20                            + " cost:\" +(endTime - startTime) +\"ms.\");";
21                    // 定义之前的方法名为旧方法名
22                    String oldMethodName = methodName + "$old";
23                    // 将原来的方法名字修改
24                    method.setName(oldMethodName);
25
26                    // 创建新的方法,复制原来的方法,名字为原来的名字
27                    CtMethod newMethod = CtNewMethod.copy(method, methodName, ctclass, null);
28
29                    // 构建新的方法体
30                    StringBuilder bodyStr = new StringBuilder();
31                    bodyStr.append("{");
32                    bodyStr.append(prefix);
33                    // 调用原有代码,类似于method();(?)表示所有的参数
34                    bodyStr.append(oldMethodName + "(?);\n");
35                    bodyStr.append(postfix);
36                    bodyStr.append(outputStr);
37                    bodyStr.append("}");
38                    // 替换新方法
39                    newMethod.setBody(bodyStr.toString());
40                    // 增加新方法
41                    ctclass.addMethod(newMethod);
42                }
43                return ctclass.toBytecode();
44            } catch (Exception e) {
45                e.printStackTrace();
46                return new byte[0];
47            }
48        } else {
49            return new byte[0];
50        }
51    }
52}

如果className是com.lengye.demo.MainClass,那么进行代理类的替换,在这和例子当中,我替换了代理类的所有方法。构建方法体,计算调用方法的时间。

其中用到的是java字节码工具类javassist,帮助我们构建新的字节码,这里需要注意的一点是,我们需要以原来的方法名构建新的方法,并且将之前的方法名做修复,见代码的22-24行

编写目标工程java-demo

 1public class MainClass {
 
 3    public static void main(String[] args) {
 4        try {
 5            Thread.sleep(1000);
 6        } catch (Exception e) {
 7            e.printStackTrace();
 8        }
 9        System.out.println("这是主函数");
10    }
11}

这个就是我们的目标类,需要对这个类的进行代理,为什么是静态代理,在加载类之前我们就修改了代理类的字节码,本质上已经不是之前的那个类了

编译打包 相信编译打包大家都很了解,java-demo.jar的打包方式非常简单,不论你是用ide还是maven打包都很简单,需要说明一下java-agent.jar的打包方式 如果你是定义MANIFEST.MF文件的话,格式如下:

Manifest-Version: 1.0
Premain-Class: com.taobao.javaagent.PreMethod
Can-Redefine-Classes: true
Boot-Class-Path: javassist-3.25.0-GA.jar

需要注意的两点,大家在以前打包的过程当中肯定都知道

1.冒号后面留一个空格 2.最后一行需要留空

将javassist-3.25.0-GA.jar和java-agent.jar放在同级目录下 如果是希望maven一键打包的话,格式如下:

 1<plugin>
 2    <groupId>org.apache.maven.plugins</groupId>
 3    <artifactId>maven-jar-plugin</artifactId>
 4    <version>3.1.0</version>
 5    <configuration>
 6        <archive>
 7            <manifest>
 8                <addClasspath>true</addClasspath>
 9            </manifest>
10            <manifestEntries>
11                <Premain-Class>
12                    com.lengye.javaagent.PreMethod
13                </Premain-Class>
14                <Can-Redefine-Classes>true</Can-Redefine-Classes>
15                <Can-Retransform-Classes>true</Can-Retransform-Classes>
16                <Boot-Class-Path>
17                    javassist-3.25.0-GA.jar
18                </Boot-Class-Path>
19            </manifestEntries>
20        </archive>
21    </configuration>
22</plugin>

mvn install即可,相比较之下还是maven的打包方式比较简单

最后,使用命令运行即可

java -javaagent:/Users/lengye/java-agent.jar=lengye -jar /Users/lengye/java-demo.jar

看下运行的结果

====premain 方法执行
lengye
methodName:main
这是主函数
this method main cost:1003ms.

其中System.out.println(agentOps)打印了我在命令行中指定的参数,这个在真实的代码中可以带上你的环境变量进行更复杂的操作,后面我们慢慢详解。