參考文章:
https://www.cnblogs.com/chiangchou/p/javassist.html
https://blog.csdn.net/u010039929/article/details/62881743
https://www.jianshu.com/p/0f64779cdcea
【本文代碼下載】
【背景】
最近在工作中進行程序的性能調優時,想起之前同事的介紹的阿里的Java在線診斷工具 —— arthas,決定試用一下。
這玩意,是真的好用,能在對被檢測程序 不做 任何改動 和 設置 的情況下,無侵入的對運行中的程序進行性能分析診斷,監控進入指定方法的請求並展示請求的參數,甚至在線熱更新代碼,
通過查閱資料發現,arthas是基於javaAgent技術 和 Java字節碼增強技術 實現的,所以接下來就開始介紹javaAgent 和 Java字節碼 技術的學習及案例
【知識准備】
什么是java agent?
Java agent是在JDK1.5引入的,是一種可以動態修改Java字節碼的技術。java類編譯之后形成字節碼被JVM執行,JVM在執行這些字節碼之前獲取這些字節碼信息,並且對這些字節碼進行修改,來完成一些額外的功能,這種就是java agent技術。
我們可以使用agent技術構建一個獨立於應用程序的代理程序(即為Agent),用來協助監測、運行甚至替換其他JVM上的程序。使用它可以實現虛擬機級別的AOP功能,並且比起 Spring的 AOP 更加的優美。
Agent分為兩種,一種是在主程序之前運行的Agent,一種是在主程序之后運行的Agent(前者的升級版,1.6以后提供),待會都會講解
什么是字節碼增強技術?
個人理解,是在Java字節碼生成之后,運行期對其進行修改,增強其功能,這種方式相當於對應用程序的二進制文件進行修改。Java字節碼增強主要是為了減少冗余代碼,提高性能等。
通常可以用 ASM 或 javassist 框架來修改字節碼
JVM Attach機制
jvm attach機制上JVM提供的一種JVM進程間通信的功能,能讓一個進程傳命令給另一個進程,並進行一些內部的操作,比如進行線程dump,那么就需要執行jstack進行,然后把pid等參數傳遞給需要dump的線程來執行,這就是一種java attach。
Class Transform的實現
第一次類加載的時候要求被transform的場景,在加載類文件的時候發出ClassFileLoad事件,交給instrument agent來調用java agent里注冊的ClassFileTransformer實現字節碼的修改
【案例】
在了解java agent 和 字節碼增強技術 后,我們可以結合起來做一個小案例:
在指定類的指定方法執行前,先打印一串11111111
正如上文所述,Agent分為兩種,一種是在主程序之前運行的Agent,一種是在主程序之后運行的Agent,接下來兩種案例都會給出,本次案例使用javassist 框架
=======================准備好被增強類的代碼=======================
被增強類的pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>java_agent</artifactId> 7 <groupId>xcy</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>app</artifactId> 13 14 <dependencies> 15 <!-- 在被增強類的pom文中,需要加入jdk的tools工具,或者自己把該jar包放入到項目中引用,因為我發現在默認情況下,idea即便引用jdk,用的包居然都是jdk中的jre,jre中沒有tools.jar這個包,就會導致被增強類啟動后,沒有attach服務,無法連接過來 --> 16 <dependency> 17 <groupId>jdk.tools</groupId> 18 <artifactId>jdk.tools</artifactId> 19 <version>jdk1.8.0_121</version> 20 <scope>system</scope> 21 <systemPath>F:/ProgramFiles/java/jdk1.8.0_121/lib/tools.jar</systemPath> 22 </dependency> 23 </dependencies> 24 25 <build> 26 <!-- <finalName>App</finalName>--> 27 <plugins> 28 <plugin> 29 <groupId>org.apache.maven.plugins</groupId> 30 <artifactId>maven-compiler-plugin</artifactId> 31 <configuration> 32 <source>1.8</source> 33 <target>1.8</target> 34 <encoding>utf-8</encoding> 35 </configuration> 36 </plugin> 37 <plugin> 38 <groupId>org.apache.maven.plugins</groupId> 39 <artifactId>maven-shade-plugin</artifactId> 40 <version>1.2.1</version> 41 <executions> 42 <execution> 43 <!-- <phase>package</phase>--> 44 <goals> 45 <goal>shade</goal> 46 </goals> 47 <configuration> 48 <transformers> 49 <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 50 <mainClass>com.app.App</mainClass> 51 </transformer> 52 </transformers> 53 </configuration> 54 </execution> 55 </executions> 56 </plugin> 57 </plugins> 58 </build> 59 </project>
被增強的類
備注:代碼在java_agent項目中的app模塊下
1 package com.app;
2
3 public class App { 4 public static void main(String[] args) { 5 hello(); 6 } 7 8 public static void hello() { 9 System.out.println("hello"); 10 } 11 }
=======================主程序之前運行的Agent方式=======================
備注:代碼在java_agent項目中的agent_non_attach模塊下
pom.xml
agent通過MANIFEST.MF 中指定的Premain-Class參數,獲取代理程序入口,所以寫好的代理類想要運行,在打 jar 包前,還需要要在 MANIFEST.MF 中指定代理程序入口。
在pom文件中,配置好Premain-Class,就會封裝到META-INF下的MANIFEST.MF中
備注:與 agent 相關的參數
- Premain-Class :JVM 啟動時指定了代理,此屬性指定代理類,即包含 premain 方法的類。
- Agent-Class :JVM動態加載代理,此屬性指定代理類,即包含 agentmain 方法的類。
- Boot-Class-Path :設置引導類加載器搜索的路徑列表,列表中的路徑由一個或多個空格分開。
- Can-Redefine-Classes :布爾值(true 或 false)。是否能重定義此代理所需的類。
- Can-Retransform-Classes :布爾值(true 或 false)。是否能重轉換此代理所需的類。
- Can-Set-Native-Method-Prefix :布爾值(true 或 false)。是否能設置此代理所需的本機方法前綴。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>java_agent</artifactId> 7 <groupId>xcy</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>agent_non_attach</artifactId> 13 14 <dependencies> 15 <!-- https://mvnrepository.com/artifact/org.javassist/javassist --> 16 <dependency> 17 <groupId>org.javassist</groupId> 18 <artifactId>javassist</artifactId> 19 <version>3.26.0-GA</version> 20 </dependency> 21 </dependencies> 22 23 <build> 24 <plugins> 25 <plugin> 26 <groupId>org.apache.maven.plugins</groupId> 27 <artifactId>maven-compiler-plugin</artifactId> 28 <configuration> 29 <source>1.8</source> 30 <target>1.8</target> 31 <encoding>utf-8</encoding> 32 </configuration> 33 </plugin> 34 <plugin> 35 <groupId>org.apache.maven.plugins</groupId> 36 <artifactId>maven-shade-plugin</artifactId> 37 <version>3.0.0</version> 38 <executions> 39 <execution> 40 <phase>package</phase> 41 <goals> 42 <goal>shade</goal> 43 </goals> 44 <configuration> 45 <transformers> 46 <transformer 47 implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 48 <manifestEntries> 49 <Premain-Class>com.agent.non.attach.demo.Agent</Premain-Class> 50 </manifestEntries> 51 </transformer> 52 </transformers> 53 </configuration> 54 </execution> 55 </executions> 56 </plugin> 57 </plugins> 58 </build> 59 </project>
Agent代理類
addTransformer:注冊一個Transformer,從此之后的類加載都會被 transformer 攔截。
VM啟動后動態加載的 agent,Instrumentation 會通過 agentmain 方法傳入代理程序,agentmain 在 main 函數開始運行后才被調用。
對於VM啟動時加載的 agent,Instrumentation 會通過 premain 方法傳入代理程序,premain 方法會在程序 main 方法執行之前被調用。此時大部分Java類都沒有被加載(“大部分”是因為,agent類本身和它依賴的類還是無法避免的會先加載的),是一個對類加載埋點做手腳(addTransformer)的好機會。但這種方式有很大的局限性,Instrumentation 僅限於 main 函數執行前,此時有很多類還沒有被加載,如果想為其注入 Instrumentation 就無法辦到。
1 package com.agent.non.attach.demo;
2
3 import java.lang.instrument.Instrumentation; 4 5 public class Agent { 6 /** 7 * agentArgs 是 premain 函數得到的程序參數,通過 -javaagent 傳入。這個參數是個字符串,如果程序參數有多個,需要程序自行解析這個字符串。 8 * inst 是一個 java.lang.instrument.Instrumentation 的實例,由 JVM 自動傳入。 9 */ 10 public static void premain(String agentOps, Instrumentation inst) { 11 System.out.println("=========premain方法執行========"); 12 // 添加Transformer 13 inst.addTransformer(new MyTransformer("com.app.App", "hello")); 14 } 15 16 /** 17 * 帶有 Instrumentation 參數的 premain 優先級高於不帶此參數的 premain。 18 * 如果存在帶 Instrumentation 參數的 premain,不帶此參數的 premain 將被忽略。 19 * @param agentArgs 20 */ 21 public static void premain(String agentArgs) { 22 23 } 24 }
MyTransformer類
攔截指定方法,進行類增強
1 package com.agent.non.attach.demo;
2
3 import javassist.ClassPool; 4 import javassist.CtClass; 5 import javassist.CtMethod; 6 7 import java.lang.instrument.ClassFileTransformer; 8 import java.lang.instrument.IllegalClassFormatException; 9 import java.security.ProtectionDomain; 10 11 public class MyTransformer implements ClassFileTransformer { 12 private String targetClassName;//被增強類的類名 13 private String targetMethodName;//被增強的方法名 14 15 public MyTransformer(String targetClassName, String targetMethodName) { 16 this.targetClassName = targetClassName; 17 this.targetMethodName = targetMethodName; 18 } 19 20 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 21 className = className.replace("/", "."); 22 if (className.equals(targetClassName)) { 23 CtClass ctclass = null; 24 try { 25 ctclass = ClassPool.getDefault().get(className);// 使用全稱,用於取得字節碼類<使用javassist> 26 CtMethod ctmethod = ctclass.getDeclaredMethod(targetMethodName);// 得到這方法實例 27 ctmethod.insertBefore("System.out.println(1111111);"); 28 return ctclass.toBytecode(); 29 } catch (Exception e) { 30 System.out.println(e.getMessage()); 31 e.printStackTrace(); 32 } 33 } 34 return null; 35 } 36 }
運行
運行有兩種方式:
1、直接運行,格式:java -javaagent:agent的jar路徑 被增強jar的路徑
例如:java -jar -javaagent:agent_non_attach-1.0-SNAPSHOT.jar app-1.0-SNAPSHOT.jar
2、在idea中運行
在被增強類項目中新建一個啟動類,在該類中添加main方法,在運行時,指定VM options參數:-javaagent: agent的jar包路徑 被增強的jar包路徑
例如:-javaagent:F:\workspace\java_agent\agent_non_attach\target\agent_non_attach-1.0-SNAPSHOT.jar
=======================主程序之后運行的Agent方式=======================
備注:代碼在java_agent項目中的agent_attach模塊下
被增強類的pom.xml
在被增強類的pom文中,需要加入jdk的tools工具,或者自己把該jar包放入到項目中引用,因為我發現在默認情況下,idea即便引用jdk,用的包居然都是jdk中的jre,jre中沒有tools.jar這個包,就會導致被增強類啟動后,沒有attach服務,無法連接過來
1 <dependency> 2 <groupId>jdk.tools</groupId> 3 <artifactId>jdk.tools</artifactId> 4 <version>jdk1.8.0_121</version> 5 <scope>system</scope> 6 <systemPath>F:/ProgramFiles/java/jdk1.8.0_121/lib/tools.jar</systemPath> 7 </dependency>
KeepRunMain類
該agent需要在被增強類運行的時候執行,所以需要讓被增強類一直運行
1 package com.app;
2
3 public class KeepRunMain { 4 public static void main(String[] args) throws InterruptedException { 5 String[] params = {}; 6 while (true) { 7 App.hello(); 8 Thread.sleep(1000L); 9 } 10 } 11 }
agent的pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>java_agent</artifactId> 7 <groupId>xcy</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>agent_attach</artifactId> 13 14 <dependencies> 15 <!-- https://mvnrepository.com/artifact/org.javassist/javassist --> 16 <dependency> 17 <groupId>org.javassist</groupId> 18 <artifactId>javassist</artifactId> 19 <version>3.26.0-GA</version> 20 </dependency> 21 22 <dependency> 23 <groupId>jdk.tools</groupId> 24 <artifactId>jdk.tools</artifactId> 25 <version>jdk1.8.0_121</version> 26 <scope>system</scope> 27 <systemPath>F:/ProgramFiles/java/jdk1.8.0_121/lib/tools.jar</systemPath> 28 </dependency> 29 </dependencies> 30 31 <build> 32 <plugins> 33 <plugin> 34 <groupId>org.apache.maven.plugins</groupId> 35 <artifactId>maven-compiler-plugin</artifactId> 36 <configuration> 37 <source>1.8</source> 38 <target>1.8</target> 39 <encoding>utf-8</encoding> 40 </configuration> 41 </plugin> 42 <plugin> 43 <groupId>org.apache.maven.plugins</groupId> 44 <artifactId>maven-shade-plugin</artifactId> 45 <version>3.0.0</version> 46 <executions> 47 <execution> 48 <phase>package</phase> 49 <goals> 50 <goal>shade</goal> 51 </goals> 52 <configuration> 53 <transformers> 54 <transformer 55 implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 56 <manifestEntries> 57 <Agent-Class>com.agent.attach.demo.Agent</Agent-Class> 58 <Can-Retransform-Classes>true</Can-Retransform-Classes> 59 </manifestEntries> 60 </transformer> 61 </transformers> 62 </configuration> 63 </execution> 64 </executions> 65 </plugin> 66 </plugins> 67 </build> 68 </project>
Agent類
1 package com.agent.attach.demo;
2
3 import java.lang.instrument.Instrumentation; 4 import java.lang.instrument.UnmodifiableClassException; 5 6 public class Agent { 7 public static String className="com.app.App"; 8 public static String methon="hello"; 9 static { 10 System.out.println("ddd"); 11 } 12 public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException { 13 Class[] allClass = inst.getAllLoadedClasses(); 14 for (Class c : allClass) { 15 System.out.println(c.getName()); 16 if(c.getName().equals(className)){ 17 System.out.println("agent loaded"); 18 inst.addTransformer(new MyTransformer(className, methon), true); 19 inst.retransformClasses(c); 20 } 21 } 22 } 23 }
MyTransformer類
1 package com.agent.attach.demo;
2
3 import javassist.ClassPool; 4 import javassist.CtClass; 5 import javassist.CtMethod; 6 7 import java.lang.instrument.ClassFileTransformer; 8 import java.lang.instrument.IllegalClassFormatException; 9 import java.security.ProtectionDomain; 10 11 public class MyTransformer implements ClassFileTransformer { 12 private String targetClassName;//被增強類的類名 13 private String targetMethodName;//被增強的方法名 14 15 public MyTransformer(String targetClassName, String targetMethodName) { 16 this.targetClassName = targetClassName; 17 this.targetMethodName = targetMethodName; 18 } 19 20 @Override 21 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 22 try { 23 CtClass ctClass = ClassPool.getDefault().get(this.targetClassName); 24 CtMethod ctMethod=ctClass.getDeclaredMethod(this.targetMethodName); 25 System.out.println(ctMethod.getName()); 26 ctMethod.insertBefore("System.out.println(\" 11111111111111111111111\");"); 27 ctClass.writeFile(); 28 return ctClass.toBytecode(); 29 } catch (Exception e) { 30 System.out.println(e.getMessage()); 31 } 32 return null; 33 } 34 }
AttachMain類
1 package com.agent.attach.demo;
2
3 import com.sun.tools.attach.AttachNotSupportedException; 4 import com.sun.tools.attach.VirtualMachine; 5 import com.sun.tools.attach.VirtualMachineDescriptor; 6 7 import java.util.List; 8 9 public class AttachMain { 10 public static void main(String args[]) throws AttachNotSupportedException { 11 VirtualMachine vm; 12 List<VirtualMachineDescriptor> vmList= VirtualMachine.list(); 13 if(vmList==null || vmList.isEmpty()){ 14 System.out.println("當前沒有java程序運行"); 15 return; 16 } 17 18 //展示所有運行中的java程序 19 System.out.println("當前運行中的java程序:"); 20 for(int i=0;i<vmList.size();i++){ 21 System.out.println("["+i+"] "+vmList.get(i).displayName()+" ,id:"+vmList.get(i).id()+" ,provider:"+vmList.get(i).provider()); 22 } 23 System.out.println("請選擇(輸入序號):"); 24 25 //選擇其中一個java進程進行增強 26 try{ 27 int num=System.in.read()-48; 28 if(num!=-1&&num<vmList.size()){ 29 vm= VirtualMachine.attach(vmList.get(num)); 30 vm.loadAgent("F:\\workspace\\java_agent\\agent_attach\\target\\agent_attach-1.0-SNAPSHOT.jar"); 31 System.in.read(); 32 } 33 }catch(Exception e){ 34 e.printStackTrace(); 35 } 36 } 37 }
運行方式
直接執行AttachMain類的main方法,選擇要增強類的進程即可
