在平常工作中有时候需要查看方法的调用时间,或者需要知道某个业务的调用逻辑,需要些大量侵入式代码来完成。现在可以使用javaagent在main方法前执行,然后加载的类,通过字节码技术,在类中加入需要的代码。
构建agent项目
1、 新建maven jar项目
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.whh</groupId> <artifactId>javaagentdemo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging>
<name>javaagentdemo</name> <url>http://maven.apache.org</url>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
<dependencies>
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.21.0-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin>
</plugins> </build> </project>
|
2、 新建java文件、新增premain方法如下:
1 2 3 4 5
| public class PreMain { public static void premain(String args, Instrumentation instrumentation){ System.out.println("premain"); } }
|
3、resource下新建META-INF/MANIFEST.MF
1 2 3 4
| Manifest-Version: 1.0 Premain-Class: com.whh.PreMain Can-Redefine-Classes: true
|
4、 打包生成jar
5、 随便写一个Main启动测试、在启动时添加VM参数:-javaagent:javaagentdemo-1.0-SNAPSHOT.jar。会发现PreMain中premain会被执行。
字节码修改
我们需要对加载的类做字节码修改,所以需要用到Instrumentation。
新建类TransformerDemo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
| import javassist.*;
import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.List;
public class TransformerDemo implements ClassFileTransformer {
private static ThreadLocal<List<MethodStackInfo>> inMethodStack = ThreadLocal.withInitial(ArrayList::new);
public static void startMethod(String className, String method) { long now = System.currentTimeMillis(); MethodStackInfo methodStackInfo = new MethodStackInfo(className + "." + method); methodStackInfo.setStartTime(now); List<MethodStackInfo> inStack = inMethodStack.get(); if (inStack.size() == 0) { methodStackInfo.setDepth(0); } else { for (int i = inStack.size() - 1; i >= 0; i--) { MethodStackInfo lastInStack = inStack.get(i); if (lastInStack.getEndTime() == 0) { methodStackInfo.setDepth(lastInStack.getDepth() + 1); break; } } } inStack.add(methodStackInfo); }
public static void endMethod(String className, String method) { long now = System.currentTimeMillis(); List<MethodStackInfo> inStack = inMethodStack.get(); for (int i = inStack.size() - 1; i >= 0; i--) { MethodStackInfo methodStackInfo = inStack.get(i); if (methodStackInfo. getEndTime() == 0) { methodStackInfo.setEndTime(now); break; } } if (inStack.get(0).getName().equals(className + "." + method)) { for (MethodStackInfo methodStackInfo : inStack) { StringBuilder sb = new StringBuilder(); sb.append("|"); for (int i = 0; i < methodStackInfo.getDepth(); i++) { sb.append(" |"); } sb.append("__").append(methodStackInfo.getName()) .append(" :") .append(methodStackInfo.getEndTime() - methodStackInfo.getStartTime()) .append(" ms"); System.out.println(sb.toString()); } inMethodStack.remove(); } }
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replaceAll("/", "."); if (!className.contains("com.whh.Main") || className.contains("$$")) { return null; } try { ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new LoaderClassPath(loader)); CtClass ctClass = classPool.get(className); CtMethod[] declaredMethods = ctClass.getDeclaredMethods(); for (CtMethod declaredMethod : declaredMethods) { String methodName = declaredMethod.getName(); if (declaredMethod.isEmpty()) continue; declaredMethod.insertBefore("com.whh.transformer.TestTransformer.startMethod(\"" + className + "\", \"" + methodName + "\");"); declaredMethod.insertAfter("com.whh.transformer.TestTransformer.endMethod(\"" + className + "\", \"" + methodName + "\");", true); }
return ctClass.toBytecode(); } catch (NotFoundException | CannotCompileException | IOException e) { System.out.println("~~~~~~~~~~" + className); e.printStackTrace(); } return new byte[0]; } }
public class MethodStackInfo { private String name; private long startTime; private long endTime; private int depth;
public MethodStackInfo() { }
public MethodStackInfo(String name) { this.name = name; }
public MethodStackInfo(String name, long startTime, long endTime, int depth) { this.name = name; this.startTime = startTime; this.endTime = endTime; this.depth = depth; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public long getStartTime() { return startTime; }
public void setStartTime(long startTime) { this.startTime = startTime; }
public long getEndTime() { return endTime; }
public void setEndTime(long endTime) { this.endTime = endTime; }
public int getDepth() { return depth; }
public void setDepth(int depth) { this.depth = depth; } }
|
修改PreMain方法
1 2 3 4 5 6 7
| public class PreMain { public static void premain(String args, Instrumentation instrumentation){ System.out.println("premain"); instrumentation.addTransformer(new TransformerDemo()); } }
|
项目打包后通过之前方法测试即可。
遇到的问题
1、通过Tomcat启动时获取不到类的字节码
解决:因为Tomcat启动时使用多个类加载器作为系统类加载器。这时需要使用insertClassPath来解决
2、部分方法无法修改
解决:过滤抽象方法
后续问题
1、如果代码中使用循环,最好是能识别出来或者在后续打印过程中去掉
2、如果有死循环需要特别处理
这个例子是在main方法启动前,还有get在main方法启动后agentmain。
不想在启动时加入VM参数可以参考lombok、stagemonitor的相关实现。
参考:
- Javassist 使用指南(一)
- Instrumentation 新功能
- 利用 Javassist 进行面向方面的更改
- Java 5 特性 Instrumentation 实践