1. 原理介绍
java instrument是一种字节码增强技术,在jdk1.5开始已引入,其核心功能实现依赖java.lang.instrument.Instrumentation接口,通过实现该接口,我们可以对已加载和未加载的类进行修改。
java instrumentation最常用的一种使用方式是通过jvm的启动参数:-javaagent来启动,例如:java -javaagent:myagent.jar MyMain。为了能让jvm识别到agent的入口类,需要在MANIFEST.MF文件中指定Premain-Class等配置,例如:(最后需要留一个空行,否则会报错)
通过-javaagent来启动,实际上是一种静态的代理,其处理流程如下:
在我们实现的Agent类中,有一个premain方法,顾名思义,他会在java启动入口方法main之前被调用,在premain中我们可以进行类的字节码增强,例如通过实现ClassFileTransformer接口,可以将指定规则的类的字节码进行替换,最终jvm的类加载器会去加载已经被agent修改后的类字节码。
这种静态的agent只能在jar包启动时进行代理,存在比较大的局限性,jdk1.6开始引入了动态的Attach Agent方式,可以在jvm启动之后的任意时刻通过Attach API远程加载Agent的jar,比如阿里开源的arthas工具就是基于Attach API实现的。
JVM Attach API的实现主要是基于UNIX域套接字的,与TCP,UDP不同,UNIX域套接字主要应用于同一个主机上的进程间通信,虽然理论上使用localhost也可以在同一个主机上实现进程间通信,但是UNIX域套接字更可靠,效率也更高。(因为它不用进行协议处理,不需要计算序列号,也不用发送确认报文,只需读写数据即可,因为在同一个主机上进程之间实际上就是处于一个高度可信可靠的通信环境)详见:Unix域套接字简介。
Attach API的执行过程如下:
2. 静态agent使用示例
首先新建一个InstrumentTest类,如下:
package com.xycode.techlecture.instrument;
/**
* 测试javaagent的入口类
* @author: xycode
* @email: lianguang.xy@alibaba-inc.com
* @date: 2022/8/17
*/
public class InstrumentTest {
public void foo(){
System.out.println("execute foo");
bar();
}
public void bar(){
System.out.println("execute bar");
}
public static void main(String[] args) {
new InstrumentTest().foo();
}
}
期望在InstrumentTest类实例方法执行前后添加log,我们可以写出如下代码:
package com.xycode.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
/**
* java agent入口类
*
* @author: xycode
* @email: lianguang.xy@alibaba-inc.com
* @date: 2022/8/17
*/
public class AgentMain {
public static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
//只处理com.xycode.techlecture.instrument.InstrumentTest
if (!"com/xycode/techlecture/instrument/InstrumentTest".equals(className)) {
return classfileBuffer;
}
try {
//asm字节码操作
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature,
exceptions);
if (name.equals("<init>")) {
return methodVisitor;
}
return new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {
@Override
protected void onMethodEnter() {
//getstatic #2 //Field java/lang/System.out:Ljava/io/PrintStream;
this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//ldc #3 //String ">> enter " + name
this.mv.visitLdcInsn(">> enter " + name);
//invokevirtual #4 //Method java/io/PrintStream.println:(Ljava/lang/String;)V
this.mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
this.mv.visitLdcInsn(">> exit " + name);
this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
}
};
}
};
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return classWriter.toByteArray();
} catch (Exception exception) {
exception.printStackTrace();
}
return classfileBuffer;
}
}
//notice: javaagent jar入口方法为premain
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain");
//注册类文件转换器
instrumentation.addTransformer(new MyClassFileTransformer(), true);
}
}
接着将AgentMain编译成jar,我们还需要配置MANIFEST.MF,目录结构与内容如下:
pom文件:(需要指定编译参数以及MANIFEST.MF文件路径)
<?xml version="1.0" encoding="UTF-8"?>
<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">
<groupId>com.xycode</groupId>
<artifactId>testJavaAgent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<!-- 将jdk的依赖jar打入项目中,这样项目中使用的jdk的依赖就可以正常使用 -->
<bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!-- 指定MANIFEST.MF文件路径-->
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
使用mvn编译出jar包:testJavaAgent-0.0.1-SNAPSHOT.jar,然后我们可以通过命令行运行,如下:
java -javaagent:${your_path}/testJavaAgent-0.0.1-SNAPSHOT.jar com.xycode.techlecture.instrument.InstrumentTest
也可以选择在IDEA中运行,只需在运行InstrumentTest时指定vm参数即可,如下:(在IEDA中可以使用idea的debug能力, 只需在java agent类中添加断点即可)
运行效果:
3. 动态agent使用示例
先创建一个AttachMainTest类,如下:
package com.xycode.techlecture.instrument.attach;
import java.util.concurrent.TimeUnit;
/**
* @author: xycode
* @email: lianguang.xy@alibaba-inc.com
* @date: 2022/8/26
*/
public class AttachMainTest {
public static void main(String[] args) throws InterruptedException {
while (true) {
System.out.println(foo());
TimeUnit.SECONDS.sleep(2);
}
}
public static int foo() {
return 100;
}
}
我们这里希望在jvm运行中动态修改foo方法的返回值,代码如下:
package com.xycode.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
/**
* attachAgent
*
* @author: xycode
* @email: lianguang.xy@alibaba-inc.com
* @date: 2022/8/29
*/
public class AttachAgentMain {
public static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//指定处理com.xycode.techlecture.instrument.attach.AttachMainTest
if (!"com/xycode/techlecture/instrument/attach/AttachMainTest".equals(className)) {
return classfileBuffer;
}
//ASM字节码操作
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
//转换foo方法, 修改其返回值
if ("foo".equals(name)) {
return new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {
@Override
protected void onMethodEnter() {
//在方法开始处插入return 50;
this.mv.visitIntInsn(Opcodes.BIPUSH, 50); //bipush 50
this.mv.visitInsn(Opcodes.IRETURN); //ireturn
}
};
}
return methodVisitor;
}
};
classReader.accept(classVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();
}
}
//notice: attach agent jar入口方法为agentmain
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {
System.out.println("agentMain");
//注册类文件转换器
instrumentation.addTransformer(new MyClassFileTransformer(),true);
for (Class loadedClass : instrumentation.getAllLoadedClasses()) {
//因为动态更改jvm中的指定类, 因此这里需要重新加载
//notice: 注意这里的类名, 格式与asm中的类名不一样
// 分别是com.xycode.techlecture.instrument.attach.AttachMainTest 与 com/xycode/techlecture/instrument/attach/AttachMainTest
if (loadedClass.getName().equals("com.xycode.techlecture.instrument.attach.AttachMainTest")) {
System.out.println("reloading " + loadedClass.getName());
instrumentation.retransformClasses(loadedClass);
break;
}
}
}
}
目录结构与MANIFEST.MF如下:
pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<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.xycode</groupId>
<artifactId>testAttachAgent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<!-- 将jdk的依赖jar打入项目中,这样项目中使用的jdk的依赖就可以正常使用 -->
<bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!-- 指定MANIFEST.MF文件路径-->
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
然后编译出testAttachAgent-0.0.1-SNAPSHOT.jar,并运行AttachMainTest类,并找到其pid,如下
pid为19854,新建AttachClient类,并指定attach的pid,如下:
package com.xycode.techlecture.instrument.attach;
import java.io.IOException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
/**
* @author: xycode
* @email: lianguang.xy@alibaba-inc.com
* @date: 2022/8/29
*/
public class AttachClient {
public static void main(String[] args) throws IOException, AttachNotSupportedException {
//attach上指定pid的java进程
VirtualMachine virtualMachine = VirtualMachine.attach("19854");
try {
virtualMachine.loadAgent("/Users/xycode/IdeaProjects/techlecture/techlecture/testAttachAgent/target/testAttachAgent-0.0.1-SNAPSHOT.jar");
}catch (Exception e){
virtualMachine.detach();
}
}
}
最终效果如下:(可以看出,类已被重新加载,并更改了foo的返回值)