前言 最近因為公司需要,需要瞭解下java探針,在網上找資料,發現資料還是有很多的,但是例子太少,有的直接把公司代碼粘貼出來,太複雜了,有的又特別簡單不是我想要的例子, 我想要這樣的一個例子: jvm在運行,我想動態修改一個類,jvm在不用重啟的情況下, 自動載入新的類定義. 動態修改類定義,聽著感 ...
前言
最近因為公司需要,需要瞭解下java探針,在網上找資料,發現資料還是有很多的,但是例子太少,有的直接把公司代碼粘貼出來,太複雜了,有的又特別簡單不是我想要的例子, 我想要這樣的一個例子:
jvm在運行,我想動態修改一個類,jvm在不用重啟的情況下, 自動載入新的類定義. 動態修改類定義,聽著感覺就很酷. 本文將實現一個方法監控的例子, 開始方法是沒有監控的, 動態修改後, 方法執行結束會列印方法耗時.
Instrumentation介紹
使用 Instrumentation,開發者可以構建一個獨立於應用程式的代理程式(Agent),用來監測和協助運行在 JVM 上的程式,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發者就可以實現更為靈活的運行時虛擬機監控和 Java 類操作了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 做任何升級和改動,就可以實現某些 AOP 的功能了。
在 Java SE 5 中,Instrument 要求在運行前利用命令行參數或者系統參數來設置代理類,在實際的運行之中,虛擬機在初始化之時(在絕大多數的 Java 類庫被載入之前),啟動instrumentation 的設置,從而可以在載入位元組碼之前,修改類的定義。
在 Java SE6 裡面,則更進一步,可以在jvm運行時,動態修改類定義,使用就更方便了,本文也主要是講著一種方式.
Instrumentation 類 定義如下:
1 /*有兩種獲取Instrumentation介面實例的方法: 2 1.以指示代理類的方式啟動JVM時。 在這種情況下,將Instrumentation實例傳遞給代理類的premain方法。 3 2. JVM提供了一種在JVM啟動後的某個時間啟動代理的機制。 在這種情況下,將Instrumentation實例傳遞給代理代碼的agentmain方法。 4 這些機制在包裝規範中進行了描述。 5 代理獲取某個Instrumentation實例後,該代理可以隨時在該實例上調用方法。 6 */ 7 public interface Instrumentation { 8 //增加一個Class 文件的轉換器,轉換器用於改變 Class 二進位流的數據,參數 canRetransform 設置是否允許重新轉換。 9 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); 10 //註冊一個轉換器 11 void addTransformer(ClassFileTransformer transformer); 12 13 //刪除一個類轉換器 14 boolean removeTransformer(ClassFileTransformer transformer); 15 16 boolean isRetransformClassesSupported(); 17 18 //在類載入之後,重新定義 Class。這個很重要,該方法是1.6 之後加入的,事實上,該方法是 update 了一個類。 19 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; 20 21 boolean isRedefineClassesSupported(); 22 /*此方法用於替換類的定義,而無需引用現有的類文件位元組,除了在常規JVM語義下會發生的初始化之外,此方法不會引起任何初始化。換句話說,重新定義類不會導致其初始化程式運行。靜態變數的值將保持調用前的狀態。 23 重新定義的類的實例不受影響。*/ 24 void redefineClasses(ClassDefinition... definitions) 25 throws ClassNotFoundException, UnmodifiableClassException; 26 27 boolean isModifiableClass(Class<?> theClass); 28 //獲取所有已經載入的類 29 @SuppressWarnings("rawtypes") 30 Class[] getAllLoadedClasses(); 31 32 @SuppressWarnings("rawtypes") 33 Class[] getInitiatedClasses(ClassLoader loader); 34 //獲取一個對象的大小 35 long getObjectSize(Object objectToSize); 36 37 void appendToBootstrapClassLoaderSearch(JarFile jarfile); 38 39 void appendToSystemClassLoaderSearch(JarFile jarfile); 40 boolean isNativeMethodPrefixSupported(); 41 void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); 42 }
- 其中addTransformer 和 retransformClasses 是有關聯的, addTransformer 註冊轉換器,retransformClasses 觸發轉換器.
- redefineClass是除了Transformer 之外另外一中轉變類定義的方式.
Instrument的兩種方式
第一種: JVM啟動前靜態Instrument
使用Javaagent命令啟動代理程式。參數 javaagent 可以用於指定一個 jar 包,並且對該 java 包有2個要求:
- 這個 jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項。
- Premain-Class 指定的那個類必須實現 premain() 方法。
premain 方法,從字面上理解,就是運行在 main 函數之前的的類。當Java 虛擬機啟動時,在執行 main 函數之前,JVM 會先運行-javaagent
所指定 jar 包內 Premain-Class 這個類的 premain 方法 。
在命令行輸入 java
可以看到相應的參數,其中有 和 java agent相關的:
-agentlib:<libname>[=<選項>] 載入本機代理庫 <libname>, 例如 -agentlib:hprof 另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:<pathname>[=<選項>] 按完整路徑名載入本機代理庫 -javaagent:<jarpath>[=<選項>] 載入 Java 編程語言代理, 請參閱 java.lang.instrument
從本質上講,Java Agent 是一個遵循一組嚴格約定的常規 Java 類。 上面說到 javaagent命令要求指定的類中必須要有premain()方法,並且對premain方法的簽名也有要求,簽名必須滿足以下兩種格式:
public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)
JVM 會優先載入 帶 Instrumentation
簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中.
如何使用javaagent?
使用 javaagent 需要幾個步驟:
- 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
- 創建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
- 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
- 使用參數 -javaagent: jar包路徑 啟動要代理的方法。
在執行以上步驟後,JVM 會先執行 premain 方法,大部分類載入都會通過該方法,註意:是大部分,不是所有。當然,遺漏的主要是系統類,因為很多系統類先於 agent 執行,而用戶類的載入肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的載入活動,既然可以攔截類的載入,那麼就可以去做重寫類這樣的操作,結合第三方的位元組碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。
MANIFREST.MF文件的常用配置:
Premain-Class :包含 premain 方法的類(類的全路徑名)
Agent-Class :包含 agentmain 方法的類(類的全路徑名)
Boot-Class-Path :設置引導類載入器搜索的路徑列表。查找類的特定於平臺的機制失敗後,引導類載入器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑組件語法。如果該路徑以斜杠字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 文件的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之後某一時刻啟動的,則忽略不表示 JAR 文件的路徑。(可選)
Can-Redefine-Classes :true表示能重定義此代理所需的類,預設值為 false(可選)
Can-Retransform-Classes :true 表示能重轉換此代理所需的類,預設值為 false (可選)
Can-Set-Native-Method-Prefix: true表示能設置此代理所需的本機方法首碼,預設值為 false(可選)
列舉一個premain 的例子:
1 public class PreMainTraceAgent { 2 public static void premain(String agentArgs, Instrumentation inst) { 3 System.out.println("agentArgs : " + agentArgs); 4 inst.addTransformer(new DefineTransformer(), true); 5 } 6 7 static class DefineTransformer implements ClassFileTransformer{ 8 @Override 9 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 10 System.out.println("premain load Class:" + className); 11 return classfileBuffer; 12 } 13 } 14 }
由於本文不關註這種靜態Instrumentation的方式,這裡只是做簡介,感興趣的可以去搜索下.
第二種動態Instrumentation的方式
在 Java SE 6 的 Instrumentation 當中,有一個跟 premain“並駕齊驅”的“agentmain”方法,可以在 main 函數開始運行之後再運行。
跟 premain 函數一樣, 開發者可以編寫一個含有“agentmain”函數的 Java 類:
由於本文不關註這種靜態Instrumentation的方式,這裡只是做簡介,感興趣的可以去搜索下. 第二種動態Instrumentation的方式 在 Java SE 6 的 Instrumentation 當中,有一個跟 premain“並駕齊驅”的“agentmain”方法,可以在 main 函數開始運行之後再運行。 跟 premain 函數一樣, 開發者可以編寫一個含有“agentmain”函數的 Java 類:
跟 premain 函數一樣,開發者可以在 agentmain 中進行對類的各種操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。
與“Premain-Class”類似,開發者必須在 manifest 文件裡面設置“Agent-Class”來指定包含 agentmain 函數的類。
可是,跟 premain 不同的是,agentmain 需要在 main 函數開始運行後才啟動,至於該方法如何運行,怎麼跟正在運行的jvm 關聯上, 就需要介紹下Attach API.
Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API,用來向目標 JVM ”附著”(Attach)代理工具程式的。有了它,開發者可以方便的監控一個 JVM,運行一個外加的代理程式。
Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裡面: VirtualMachine 代表一個 Java 虛擬機,也就是程式需要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動作和 Detach 動作(Attach 動作的相反行為,從 JVM 上面解除一個代理)等等 ; VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各種功能。
下邊我們利用上邊說的實現一個監控方法執行耗時的例子: 定時執行一個方法,開始方法是沒有監控的, 方法重定義加上監控。
一個簡單的方法監控例子
那麼我們想一下需要實現這個例子,需要幾個模塊.
- 一個代理模塊(監控邏輯);
- 一個main函數(運行的jvm);
- 一個把上邊兩個模塊關聯在一起的程式.
從代理模塊開始:
1. 需要監控的TimeTest類:
/** * @ClassName TimeTest * @Author jiangyuechao * @Date 2020/1/20-10:36 * @Version 1.0 */ public class TimeTest { public static void sayHello( ){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sayhHello.........."); } public static void sayHello2(String word){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sayhHello2.........."+word); } }
2. 編寫agent 代碼
位元組碼轉換類:
1 public class MyTransformer implements ClassFileTransformer { 2 3 // 被處理的方法列表 4 final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>(); 5 6 public MyTransformer() { 7 add("com.chaochao.java.agent.TimeTest.sayHello"); 8 add("com.chaochao.java.agent.TimeTest.sayHello2"); 9 } 10 11 private void add(String methodString) { 12 String className = methodString.substring(0, methodString.lastIndexOf(".")); 13 String methodName = methodString.substring(methodString.lastIndexOf(".") + 1); 14 List<String> list = methodMap.get(className); 15 if (list == null) { 16 list = new ArrayList<String>(); 17 methodMap.put(className, list); 18 } 19 list.add(methodName); 20 } 21 22 @Override 23 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 24 ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 25 System.out.println("className:"+className); 26 if (methodMap.containsKey(className)) {// 判斷載入的class的包路徑是不是需要監控的類 27 try { 28 ClassPool classPool=new ClassPool(); 29 classPool.insertClassPath(new LoaderClassPath(loader)); 30 CtClass ctClass= classPool.get(className.replace("/",".")); 31 // CtMethod ctMethod= ctClass.getDeclaredMethod("run"); 32 CtMethod[] declaredMethods = ctClass.getDeclaredMethods(); 33 for (CtMethod ctMethod : declaredMethods) { 34 //插入本地變數 35 ctMethod.addLocalVariable("begin",CtClass.longType); 36 ctMethod.addLocalVariable("end",CtClass.longType); 37 38 ctMethod.insertBefore("begin=System.currentTimeMillis();System.out.println(\"begin=\"+begin);"); 39 //前面插入:最後插入的放最上面 40 ctMethod.insertBefore("System.out.println( \"埋點開始-1\" );"); 41 42 ctMethod.insertAfter("end=System.currentTimeMillis();System.out.println(\"end=\"+end);"); 43 ctMethod.insertAfter("System.out.println(\"性能:\"+(end-begin)+\"毫秒\");"); 44 45 //後面插入:最後插入的放最下麵 46 ctMethod.insertAfter("System.out.println( \"埋點結束-1\" );"); 47 } 48 return ctClass.toBytecode(); 49 } catch (NotFoundException | CannotCompileException|IOException e) { 50 e.printStackTrace(); 51 } 52 return new byte[0]; 53 } 54 else 55 System.out.println("沒找到."); 56 return null; 57 } 58 59 }
上邊的類就是在方法前後加上耗時列印.
下邊是定義的AgentMainTest:
import java.lang.instrument.Instrumentation; public class AgentMainTest { //關聯後執行的方法 public static void agentmain(String args, Instrumentation inst) throws Exception { System.out.println("Args:" + args); Class[] classes = inst.getAllLoadedClasses(); for (Class clazz : classes) { System.out.println(clazz.getName()); } System.out.println("開始執行自定義MyTransformer"); // 添加Transformer inst.addTransformer(new MyTransformer(),true); inst.retransformClasses(TimeTest.class); } public static void premain(String args, Instrumentation inst) throws Exception { System.out.println("Pre Args:" + args); Class[] classes = inst.getAllLoadedClasses(); for (Class clazz : classes) { System.out.println(clazz.getName()); } } }
MANIFREST.MF文件定義,註意最後一行是空格:
Manifest-Version: 1.0
Premain-Class: com.chaochao.java.agent.AgentMainTest
Agent-Class: com.chaochao.java.agent.AgentMainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true
代理模塊介紹完畢, 下邊是一個main函數程式.這個就很簡單了.
1 public class TestMan { 2 3 public static void main(String[] args) throws InterruptedException 4 { 5 TimeTest tt = new TimeTest(); 6 tt.sayHello(); 7 tt.sayHello2("one"); 8 while(true) 9 { 10 Thread.sleep(60000); 11 new Thread(new WaitThread()).start(); 12 tt.sayHello(); 13 tt.sayHello2("two"); 14 } 15 } 16 17 static class WaitThread implements Runnable 18 { 19 @Override 20 public void run() 21 { 22 System.out.println("Hello"); 23 } 24 } 25 }
最後一個關聯模塊:
/** * * @author jiangyuechao * */ public class AttachMain { public static void main(String[] args) throws Exception{ VirtualMachine vm = null; String pid = null; List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { System.out.println("pid:" + vmd.id() + ":" + vmd.displayName()); if(vmd.displayName().contains("TestMan")) { pid = vmd.id(); } } //E:\eclipse-workspace\JavaStudyAll\JVMStudy\target // String agentjarpath = "E:/jee-workspace/javaAgent/TestAgent.jar"; //agentjar路徑 String agentjarpath = "E:/jee-workspace/javaAgent/AgentMainTest.jar"; //agentjar路徑 vm = VirtualMachine.attach(pid);//目標JVM的進程ID(PID) vm.loadAgent(agentjarpath, "This is Args to the Agent."); vm.detach(); } }
也很簡單, 第一步獲取pid ,第二步使用attach 方法關聯jvm.
上便代碼準備好了,那麼怎麼把他們運行起來呢, 需要幾步:
- 先把agent 代碼打包為jar 包
- 運行main 函數,執行agent
agent 打包
把agent代碼打包為普通的jar 包即可, 使用eclipse或intellij 都可以. 以eclipse 為例,只需要註意一步使用你寫好的MANIFREST文件
但是我推薦使用另外一種方式,命令行的方式, 使用java 命令行直接來的, 既方便又快捷.
首先把需要的類放在一個文件夾下, javac編譯:
javac -encoding UTF-8 -classpath .;E:\tools\jdk1.8.0_65\lib\tools.jar;E:\eclipse-workspace\JavaStudyAll\JVMStudy\lib\javassist.jar; AgentMainTest.java MyTransformer.java
其中需要依賴tools.jar和 javassist jar包.
編譯後的class文件打包為jar包:
jar cvmf MANIFEST.MF AgentMainTest.jar AgentMainTest.class MyTransformer.class
如下所示:
agent包准備好之後, 就簡單了,先運行main函數,啟動一個虛擬機. 運行入下:
sayhHello..........
sayhHello2..........one
運行AttachMain 類,關聯agent程式,就會看到如下的輸出:
可以看到 在方法執行結束後, 已經有了耗時的列印. 測試成功.
Instrumentation的局限性
大多數情況下,我們使用Instrumentation都是使用其位元組碼插樁的功能,或者籠統說就是類重定義(Class Redefine)的功能,但是有以下的局限性:
- premain和agentmain兩種方式修改位元組碼的時機都是類文件載入之後,也就是說必須要帶有Class類型的參數,不能通過位元組碼文件和自定義的類名重新定義一個本來不存在的類。
- 類的位元組碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新類和老類的父類必須相同;
- 新類和老類實現的介面數也要相同,並且是相同的介面;
- 新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;
- 新類和老類新增或刪除的方法必須是private static/final修飾的;
- 可以修改方法體。
除了上面的方式,如果想要重新定義一個類,可以考慮基於類載入器隔離的方式:創建一個新的自定義類載入器去通過新的位元組碼去定義一個全新的類,不過也存在只能通過反射調用該全新類的局限性。
參考:
https://www.cnblogs.com/rickiyang/p/11368932.html
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
轉發請註明出處: https://www.cnblogs.com/jycboy/p/12249472.html