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)打印了我在命令行中指定的参数,这个在真实的代码中可以带上你的环境变量进行更复杂的操作,后面我们慢慢详解。