手敲代碼來體驗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