字节码插桩
我们知道JVM是不能直接执行.java 代码,也不能直接执行.class文件,它只能执行.class 文件中存储的指令码。这就是为什么class需要通过classLoader 装载以后才能运行。基于此机制可否在ClassLoader装载之前拦截修改class当中的内容(jvm 指令码)从而让程序中包含我们的埋点逻辑呢?答案是肯定的,但需要用到两个技术 javaagent与javassist 。前者用于拦截ClassLoad装载,后者用于操作修改class文件。
javaagent
javaagent介绍
javaagent 是java1.5之后引入的特性,其主要作用是在class 被加载之前对其拦截,以插入我们的监听字节码
javaagent jar包
javaagent 最后展现形式是一个Jar包,有以下特性:
1.必须 META-INF/MANIFEST.MF中指定Premain-Class 设定启agent启动类。
2.在启类需写明启动方法 public static void main(String arg,)
3.不可直接运行,只能通过 jvm 参数-javaagent:xxx.jar 附着于其它jvm 进程运行。
javaagent使用
1、编写agent方法
public class MyAgent {
public static void premain(String args, Instrumentation instrumentation) throws Exception {
System.out.println("Hello javaagent permain:"+args);
}
}
2、添加premain-class参数
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Project-name>${project.name}</Project-name>
<Project-version>${project.version}</Project-version>
<Premain-Class>com.javaagent.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
3、构建打包
4、在任一JAVA应用中 添加jvm 参数并启动 -javaagent:xxx.jarjavaagent META-INF/MANIFEST.MF
参数说明:
Premain-Class:必填,agent启动
classCan-Redefine-Classes:默认为false ,是否允许重新定义
classCan-Retransform-Classes:默认为false,是否允许重置Class,重置后相当于class 从classLoade中清除,下次有需要的时候会重新装载,也会重新走Transformer 流程。
Boot-Class-Path:agent 所依赖的jar 路径,多个用空格分割
创建一个测试类MyAgentTest并运行查看结果
public class MyAgentTest {
public static void main(String[] args) {
System.out.println("main");
}
}
//运行结果:main
添加jvm参数
参数内容:-javaagent:/Users/jinyunlong/IdeaProjects/test-agent/target/test-agent-1.0-SNAPSHOT.jar=123
再次运行测试类MyAgentTest并查看结果
javassist
javassist介绍
javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成(注:也可以使用ASM实现,但需要会操作字节码指令,学习使用成本高)
javassist使用
使用javassist需要引入javasssist的jar包,添加内容如下:
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.1-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Project-name>${project.name}</Project-name>
<Project-version>${project.version}</Project-version>
<Premain-Class>com.javaagent.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Boot-Class-Path>javassist-3.18.1-GA.jar</Boot-Class-Path>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
演示插入打印当前时间
创建类MyServer
public class MyServer {
public Integer sayHello(String name,String message){
System.out.println("hello");
return 0;
}
}
myAgent类
创建测试类并调用MyServer中的sayHello方法
演示计算方法调用时间
类MyAgent
public class MyAgent {
public static void premain(String args, Instrumentation instrumentation) throws Exception {
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if(!"com/javaagent/MyServer".equals(className)){
return null;
}
try {
return buildMonitorClass();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
},true);
}
private static byte[] buildMonitorClass() throws Exception{
/**
* 1、拷贝一个新的方法
* 2、修改原方法名
* 3、加入监听代码
*/
ClassPool pool = new ClassPool();
pool.appendSystemPath();
CtClass ctClass = pool.get("com.javaagent.MyServer");
CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
CtMethod copyMethod = CtNewMethod.copy(ctMethod,ctClass,new ClassMap());
ctMethod.setName("sayHello$agent");
copyMethod.setBody("{\n" +
" long begin = System.nanoTime();\n" +
" try {\n" +
" return sayHello$agent($1,$2);\n" +
" } finally {\n" +
" System.out.println(System.nanoTime() - begin);}\n" +
" }");
ctClass.addMethod(copyMethod);
return ctClass.toBytecode();
}
}
修改类MyServer
public class MyServer {
public Integer sayHello(String name,String message){
System.out.println("hello name:"+name+",message:"+message);
return 0;
}
}
修改测试类并运行
public class MyAgentTest {
public static void main(String[] args) {
MyServer myServer = new MyServer();
myServer.sayHello("paul","1234");
}
}
//运行结果:
hello name:paul,message:1234
186537
javassist 特殊语法