字節碼插樁
我們知道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 特殊語法