Java Attach API使用笔记
手敲代码来体验IDEA+ASM+Java Attach API实现方法增强的一个示例过程记录。
需求和目的
/**
* 模拟业务方法
* @author xujian
* 2021-03-12 10:52
**/
public class MyBizMain {
public String foo() {
return "------我是MyBizMain-----";
}
public static void main(String[] args) throws InterruptedException {
MyBizMain myBizMain = new MyBizMain();
while (true) {
System.out.println(myBizMain.foo());
Thread.sleep(1000);
}
}
}
有一个程序MyBizMain.java
,循环调用foo方法打印-“-----我是MyBizMain-----”,我们的目的是在其打印过程中,通过java agent将其打印的内容修改为“------我是MyBizMain的Agent-----”
实现过程
创建Attach程序
1、在IDEA中新建一maven项目attach-demo;
2、引入ASM相关依赖
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>7.1</version>
</dependency>
<dependency>
<artifactId>asm-commons</artifactId>
<groupId>org.ow2.asm</groupId>
<version>7.1</version>
</dependency>
3、引入tools.jar
因为要使用到VirtualMachine,所以需要手动引入JDK目录下Contents/Home/lib/tools.jar,如下图所示:
4、编写attach代码
/**
* @author xujian
* 2021-03-12 13:45
**/
public class MyAttachMain {
public static void main(String[] args) {
VirtualMachine vm = null;
try {
vm = VirtualMachine.attach("3188");//MyBizMain进程ID
vm.loadAgent("/Users/jarry/IdeaProjects/agent-demo/target/agent-demo-1.0-SNAPSHOT.jar");//java agent jar包路径
} catch (Exception e) {
e.printStackTrace();
} finally {
if (vm != null) {
try {
vm.detach();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
创建Agent程序
1、在IDEA中新建一个maven项目agent-demo
2、引入ASM相关依赖以及打jar包的maven插件
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>7.1</version>
</dependency>
<dependency>
<artifactId>asm-commons</artifactId>
<groupId>org.ow2.asm</groupId>
<version>7.1</version>
</dependency>
<plugin>
...
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!-- 打jar的文件清单,对应META-INF/MANIFEST.MF文件 -->
<manifestEntries>
<!-- 主程序启动类 -->
<Agent-Class>
org.example.MyBizAgentMain
</Agent-Class>
<!-- 允许重新定义类 -->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!-- 允许转换并重新加载类 -->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
3、自定义ASM ClassVisitor
/**
* 自定义ClassVisitor,修改foo方法字节码
* @author xujian
* 2021-03-12 11:14
**/
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("foo".equals(name)) {
System.out.println("----准备修改foo方法----");
return new MyMethodVisitor(api,mv,access,name,descriptor);
}
return mv;
}
}
4、自定义ASM MethodVisitor
/**
* 自定义MethodVisitor,修改字节码
* @author xujian
* 2021-03-12 11:02
**/
public class MyMethodVisitor extends AdviceAdapter {
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
mv.visitLdcInsn("------我是MyBizMain的Agent-----");//从常量池加载字符串
mv.visitInsn(ARETURN);//返回
}
}
5、自定义ClssFileTransformer
/**
* 自定义类文件转换器,通过ASM修改MyBizMain类字节码
* @author xujian
* 2021-03-12 11:42
**/
public class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!"agent/MyBizMain".equals(className)) return classfileBuffer;
//以下为ASM常规操作,详情可以查看ASM使用相关文档
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr,ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new MyClassVisitor(ASM7,cw);
cr.accept(cv,ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
}
6、编写agent程序
/**
* @author xujian
* 2021-03-12 10:58
**/
public class MyBizAgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("---agent called---");
inst.addTransformer(new MyClassFileTransformer(),true);//添加类文件转换器,第二个参数必须设置为true,表示可以重新转换类文件
Class[] classes = inst.getAllLoadedClasses();
for (int i = 0; i < classes.length; i++) {
if ("agent.MyBizMain".equals(classes[i].getName())) {
System.out.println("----重新加载MyBizMain开始----");
inst.retransformClasses(classes[i]);
System.out.println("----重新加载MyBizMain完毕----");
break;
}
}
}
}
启动程序
- 先启动
MyBizMain.java
程序,使用jps
命令查询其对应的进程号; - 将上一步拿到的进程号填写到
MyAttachMain.java
的对应位置vm = VirtualMachine.attach("3188");//MyBizMain进程ID
; - 使用maven打包插件将agent-demo项目打包成agent-demo.jar;
- 将上一步得到的jar包路径填写到
MyAttachMain.java
的对应位置vm.loadAgent("/Users/jarry/IdeaProjects/agent-demo/target/agent-demo-1.0-SNAPSHOT.jar");//java agent jar包路径
- 启动
MyAttachMain.java
程序查看输出结果;
达到的效果
总结
字节码层面的方法增强=修改字节码(ASM等字节码操作框架)+修改后的字节码重新加载(Java Agent、Java Attach API、Instrumentation)。
详细的代码示例可以参考https://github.com/xujian01/blogcode/tree/master/src/main/java/javaagent