動態加載class文件

想要在jvm啓動後,動態的加載class類文件,我們首先需要了解Instrumentation、Attach、Agent、VirtualMachine、ClassFileTransformer這幾個類的用法和他們之間的關係。 

Java的com.sun.tools.attach包中的VirtualMachine類,該類允許我們通過給attach方法傳入一個jvm的pid(進程id),遠程連接到jvm上。然後我們可以通過loadAgent方法向jvm註冊一個代理程序agent,在該agent的代理程序中會得到一個Instrumentation實例,該實例可以在class加載前改變class的字節碼,可以在class加載後重新加載。在調用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer接口中提供的方法進行處理。 

下面先詳細介紹下VirtualMachine、Attach、Agent、Instrumentation、ClassFileTransformer這幾個類的用法。 
一、VirtualMachine 
VirtualMachine 詳細API可以在這裏查看: 
http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html 

VirtualMachine中的attach(String id)方法允許我們通過jvm的pid,遠程連接到jvm。當通過Attach API連接到JVM的進程上後,系統會加載management-agent.jar,然後在JVM中啓動一個Jmx代理,最後通過Jmx連接到虛擬機。 

下面展示通過attach到目標jvm,然後通過loadAgent註冊management-agent.jar代理程序,啓動jmx代理服務。 
Java代碼  收藏代碼
  1. // 被監控jvm的pid(windows上可以通過任務管理器查看)  
  2.             String targetVmPid = "5936";  
  3.             // Attach到被監控的JVM進程上  
  4.             VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);  
  5.   
  6.             // 讓JVM加載jmx Agent  
  7.             String javaHome = virtualmachine.getSystemProperties().getProperty("java.home");  
  8.             String jmxAgent = javaHome + File.separator + "lib" + File.separator + "management-agent.jar";  
  9.             virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");  
  10.   
  11.             // 獲得連接地址  
  12.             Properties properties = virtualmachine.getAgentProperties();  
  13.             String address = (String) properties.get("com.sun.management.jmxremote.localConnectorAddress");  
  14.   
  15.             // Detach  
  16.             virtualmachine.detach();  
  17.             // 通過jxm address來獲取RuntimeMXBean對象,從而得到虛擬機運行時相關信息  
  18.             JMXServiceURL url = new JMXServiceURL(address);  
  19.             JMXConnector connector = JMXConnectorFactory.connect(url);  
  20.             RuntimeMXBean rmxb = ManagementFactory.newPlatformMXBeanProxy(connector.getMBeanServerConnection(), "java.lang:type=Runtime",  
  21.                     RuntimeMXBean.class);  
  22.             // 得到目標虛擬機佔用cpu時間  
  23.             System.out.println(rmxb.getUptime());  


位於jre\lib目錄中的management-agent.jar是沒有任何class類文件的,整個jar包中只有MANIFEST.MF文件,文件內容如下: 
Java代碼  收藏代碼
  1. Manifest-Version: 1.0  
  2. Created-By: 1.6.0 (Sun Microsystems Inc.)  
  3. Agent-Class: sun.management.Agent  
  4. Premain-Class: sun.management.Agent  

關於更多的JVM Management API(JVM管理工具API及用法請參考下面URI) 
http://ayufox.iteye.com/blog/653214 

二、Agent類 
目前Agent類的啓動有兩種方式,一種是在JDK5版本中提供隨JVM啓動的Agent,我們稱之爲premain方式。另一種是在JDK6中在JDK5的基礎之上又提供了JVM啓動之後通過Attach去加載的Agent類,我們稱之爲agentmain方式。 

Agent類的兩種實現方式: 
在這兩種啓動方式下,Agent JAR文件中的代理類中都必須實現特定的方法,如下所示: 
1、隨JVM啓動的Agent方式必須實現下面兩個方法中的其中一個: 
Java代碼  收藏代碼
  1. public static void premain(String agentArgs, Instrumentation inst);[1]  
  2. public static void premain(String agentArgs);[2]  

JVM 首先嚐試在代理類上調用以下方法: 
Java代碼  收藏代碼
  1. public static void premain(String agentArgs, Instrumentation inst);  

如果代理類沒有實現此方法,那麼 JVM 將嘗試調用: 
Java代碼  收藏代碼
  1. public static void premain(String agentArgs);  



2、通過Attach去啓動Agent類方式必須實現下面兩個方法中的其中一個: 
Java代碼  收藏代碼
  1. public static void agentmain (String agentArgs, Instrumentation inst);[1]   
  2. public static void agentmain (String agentArgs);[2]   

代理類必須實現公共靜態agentmain方法。系統類加載器(ClassLoader.getSystemClassLoader)必須支持將代理 JAR 文件添加到系統類路徑的機制。代理 JAR 將被添加到系統類路徑。系統類路徑是通常加載包含應用程序 main 方法的類的類路徑。代理類將被加載,JVM 嘗試調用agentmain 方法。JVM 首先嚐試對代理類調用以下方法: 
Java代碼  收藏代碼
  1. public static void agentmain(String agentArgs, Instrumentation inst);  

如果代理類沒有實現此方法,那麼 JVM 將嘗試調用: 
Java代碼  收藏代碼
  1. public static void agentmain(String agentArgs);  

如果是使用命令行選項啓動代理,那麼agentmain方法將不會被調用。 

代理類agent的加載: 
代理類將被系統類加載器加載(參見 ClassLoader.getSystemClassLoader),系統類加載器是通常加載包含應用程序main方法的類的類加載器。 

MANIFEST.MF文件配置: 
Agent類(又稱爲代理類)必須被部署爲JAR 文件。Agent代理類jar包中的MANIFEST.MF文件中,必須指定Premain-Class或者Agent-Class參數。MANIFEST.MF文件內容如下: 
Java代碼  收藏代碼
  1. Manifest-Version: 1.0  
  2. Created-By: 1.6.0 (Sun Microsystems Inc.)  
  3. Agent-Class: sun.management.Agent  
  4. Premain-Class: sun.management.Agent  


Premain-Class 
如果 JVM 啓動時指定了代理,那麼此屬性指定代理類,即包含 premain 方法的類。如果 JVM 啓動時指定了代理,那麼此屬性是必需的。如果該屬性不存在,那麼 JVM 將中止。注:此屬性是類名,不是文件名或路徑。 

Agent-Class 
如果實現支持 VM 啓動之後某一時刻啓動代理的機制,那麼此屬性指定代理類。 即包含 agentmain 方法的類。 此屬性是必需的,如果不存在,代理將無法啓動。 注:這是類名,而不是文件名或路徑。 

兩種代理模式的啓動方式: 
1、premain啓動代理的方式: 
在jvm的啓動參數中加入 
Java代碼  收藏代碼
  1. -javaagent:jarpath[=options]  

jarpath 是代理 JAR 文件的路徑,options 是代理選項。此開關可以在同一代碼行使用多次,從而創建多個代理。多個代理可以使用相同的 jarpath。代理 JAR 文件必須遵守 JAR 文件規範。代理類必須實現公共靜態premain 方法,該方法的原理與main應用程序入口點類似。在 Java 虛擬機 (JVM) 初始化後,每個 premain 方法將按照指定代理的順序調用,然後將調用實際的應用程序 main 方法。每個 premain 方法必須按照依次進行的啓動順序返回。

-javaagent使用方法 
一個java程序中-javaagent這個參數的個數是沒有限制的,所以可以添加任意多個java agent。 
所有的java agent會按照你定義的順序執行。 
例如: 
Java代碼  收藏代碼
  1. java -javaagent:MyAgent1.jar -javaagent:MyAgent2.jar -jar MyProgram.jar  

假設MyProgram.jar裏面的main函數在MyProgram中。 
MyAgent1.jar, MyAgent2.jar,  這2個jar包中實現了premain的類分別是MyAgent1, MyAgent2 
程序執行的順序將會是 
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main 
另外,放在main函數之後的premain是不會被執行的, 
例如 
Java代碼  收藏代碼
  1. java -javaagent:MyAgent1.jar  -jar MyProgram.jar -javaagent:MyAgent2.jar  

MyAgent2 和MyAgent3 都放在了MyProgram.jar後面,所以MyAgent2的premain都不會被執行, 
所以執行的結果將是 
MyAgent1.premain -> MyProgram.main 
每一個java agent 都可以接收一個字符串類型的參數,也就是premain中的agentArgs,這個agentArgs是通過java option中定義的。 
如: 
Java代碼  收藏代碼
  1. java -javaagent:MyAgent2.jar=thisIsAgentArgs -jar MyProgram.jar  

MyAgent2中premain接收到的agentArgs的值將是”thisIsAgentArgs” (不包括雙引號) 

2、agentmain啓動代理的方式: 
先通過VirtualMachine.attach(targetVmPid)連接到虛擬機,然後通過virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");註冊agent代理類。 
Java代碼  收藏代碼
  1. // 被監控jvm的pid(windows上可以通過任務管理器查看)  
  2.         String targetVmPid = "5936";  
  3.         // Attach到被監控的JVM進程上  
  4.         VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);  
  5.   
  6.         // 讓JVM加載jmx Agent  
  7.         String javaHome = virtualmachine.getSystemProperties().getProperty("java.home");  
  8.         String jmxAgent = javaHome + File.separator + "lib" + File.separator + "management-agent.jar";  
  9.         virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");  


代理類的方法中的參數中的Instrumentation: 
通過參數中的Instrumentation inst,添加自己定義的ClassFileTransformer,來改變class文件。這裏自定義的Transformer實現了transform方法,在該方法中提供了對實際要執行的類的字節碼的修改,甚至可以達到執行另外的類方法的地步 

關於更多的Agent代理類的使用方法請參考下面的URI: 
http://blog.sina.com.cn/s/blog_605f5b4f0100qfvc.html 
http://mgoann.iteye.com/blog/1422680 

三、Instrumentation 
java.lang.Instrument包是在JDK5引入的,程序員通過修改方法的字節碼實現動態修改類代碼。在代理類的方法中的參數中,就有Instrumentation inst實例。通過該實例,我們可以調用Instrumentation提供的各種接口。比如調用inst.getAllLoadedClasses()得到所有已經加載過的類。調用inst.addTransformer(new SdlTransformer(), true)增加一個可重轉換轉換器。調用inst.retransformClasses(Class cls),向jvm發起重轉換請求。 

Java Instrutment只提供了JVM TI中非常小的一個功能子集,一個是允許在類加載之前,修改類字節(ClassFileTransformer)(JDK5中開始提供,即使隨JVM啓動的Agent),另外一個是在類加載之後,觸發JVM重新進行類加載(JDK6中開始提供,用於JVM啓動之後通過Attach去加載Agent)。這兩個功能表面看起來微不足道,但實際非常強大,AspectJ AOP的動態Weaving、Visual VM的性能剖析、JConsole支持Attach到進程上進行監控,都是通過這種方式來做的。除了這兩個功能外,JDK 6中還提供了動態增加BootstrapClassLoader/SystemClassLoader的搜索路徑、對Native方法進行instrutment(還記得JVM TI的Native Method Bind嗎?)。 
      1.主要API(java.lang.instrutment) 
      1)ClassFileTransformer:定義了類加載前的預處理類,可以在這個類中對要加載的類的字節碼做一些處理,譬如進行字節碼增強 
      2)Instrutmentation:增強器,由JVM在入口參數中傳遞給我們,提供瞭如下的功能 
  • addTransformer/ removeTransformer:註冊/刪除ClassFileTransformer
  • retransformClasses:對於已經加載的類重新進行轉換處理,不會觸發重新加載類定義,需要注意的是,新加載的類不能修改舊有的類聲明,譬如不能增加屬性、不能修改方法聲明
  • redefineClasses:與如上類似,但不是重新進行轉換處理,而是直接把處理結果(bytecode)直接給JVM
  • getAllLoadedClasses:獲得當前已經加載的Class,可配合retransformClasses使用
  • getInitiatedClasses:獲得由某個特定的ClassLoader加載的類定義
  • getObjectSize:獲得一個對象佔用的空間,包括其引用的對象
  • appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch:增加BootstrapClassLoader/SystemClassLoader的搜索路徑
  • isNativeMethodPrefixSupported/setNativeMethodPrefix:支持攔截Native Method


關於更多的Agent代理類的使用方法請參考下面的URI: 
http://ayufox.iteye.com/blog/655619 
http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html 

四、ClassFileTransformer 
Java代碼  收藏代碼
  1. byte[] transform(ClassLoader loader,String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer)throws IllegalClassFormatException  

該接口只定義個一個方法transform,該方法會在加載新class類或者重新加載class類時,調用。例如,inst.addTransformer(new SdlTransformer(), true)當代碼中增加了一個可重轉換轉換器後,每次類加載之前,就會調用transform方法。若該方法返回null,則不改變加載的class字節碼,若返回一個byte[]數組,則jvm將會用返回的byte[]數組替換掉原先應該加載的字節碼。 

下面將transform的官方說明貼出來: 
byte[] transform(ClassLoader loader, 
                 String className, 
                 Class<?> classBeingRedefined, 
                 ProtectionDomain protectionDomain, 
                 byte[] classfileBuffer) 
                 throws IllegalClassFormatException此方法的實現可以轉換提供的類文件,並返回一個新的替換類文件。 
有兩種裝換器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean) 的 canRetransform 參數確定: 

可重轉換 轉換器,將 canRetransform 設爲 true 可添加這種轉換器 
不可重轉換 轉換器,將 canRetransform 設爲 false 或者使用 Instrumentation.addTransformer(ClassFileTransformer) 可添加這種轉換器 
在轉換器使用 addTransformer 註冊之後,每次定義新類和重定義類時都將調用該轉換器。每次重轉換類時還將調用可重轉換轉換器。對新類定義的請求通過 ClassLoader.defineClass 或其本機等價方法進行。對類重定義的請求通過 Instrumentation.redefineClasses 或其本機等價方法進行。對類重轉換的請求將通過 Instrumentation.retransformClasses 或其本機等價方法進行。轉換器是在驗證或應用類文件字節之前的請求處理過程中調用的。 當存在多個轉換器時,轉換將由 transform 調用鏈組成。也就是說,一個 transform 調用返回的 byte 數組將成爲下一個調用的輸入(通過 classfileBuffer 參數)。 

轉換將按以下順序應用: 

不可重轉換轉換器 
不可重轉換本機轉換器 
可重轉換轉換器 
可重轉換本機轉換器 
對於重轉換,不會調用不可重轉換轉換器,而是重用前一個轉換的結果。對於所有其他情況,調用此方法。在每個這種調用組中,轉換器將按照註冊的順序調用。本機轉換器由 Java 虛擬機 Tool 接口中的 ClassFileLoadHook 事件提供。 

第一個轉換器的輸入(通過 classfileBuffer 參數)如下: 

對於新的類定義,是傳遞給 ClassLoader.defineClass 的 byte 
對於類重定義,是 definitions.getDefinitionClassFile(),其中 definitions 是 Instrumentation.redefineClasses 的參數 
對於類重轉換,是傳遞給新類定義的 byte,或者是最後一個重定義(如果有重定義),所有不可轉換轉換器進行的轉換都將自動重新應用並保持不變;有關細節,請參閱 Instrumentation.retransformClasses 
如果實現方法確定不需要進行轉換,則應返回 null。否則,它將創建一個新的 byte[] 數組,將輸入 classfileBuffer 連同所有需要的轉換複製到其中,並返回這個新數組。不得修改輸入 classfileBuffer。 

在重轉換和重定義中,轉換器必須支持重定義語義:如果轉換器在初始定義期間更改的類在以後要重轉換或重定義,那麼轉換器必須確保第二個輸出類文件是第一個輸出類文件的合法重定義文件。 

如果轉換器拋出異常(未捕獲的異常),後續轉換器仍然將被調用並加載,仍然將嘗試重定義或重轉換。因此,拋出異常與返回 null 的效果相同。若要使用轉換器代碼在生成未檢驗異常時防止不希望發生的行爲,可以讓轉換器捕獲 Throwable。如果轉換器認爲 classFileBuffer 不表示一個有效格式的類文件,則將拋出 IllegalClassFormatException;儘管這與返回 null 的效果相同,但它便於對格式毀壞進行記錄或調試。 


參數: 
loader - 定義要轉換的類加載器;如果是引導加載器,則爲 null 
className - 完全限定類內部形式的類名稱和 The Java Virtual Machine Specification 中定義的接口名稱。例如,"java/util/List"。 
classBeingRedefined - 如果是被重定義或重轉換觸發,則爲重定義或重轉換的類;如果是類加載,則爲 null 
protectionDomain - 要定義或重定義的類的保護域 
classfileBuffer - 類文件格式的輸入字節緩衝區(不得修改) 
返回: 
一個格式良好的類文件緩衝區(轉換的結果),如果未執行轉換,則返回 null。 
拋出: 
IllegalClassFormatException - 如果輸入不表示一個格式良好的類文件 
另請參見: 
Instrumentation.redefineClasses(java.lang.instrument.ClassDefinition...) 


參考文檔: 
http://ayufox.iteye.com/blog/653214 
http://ayufox.iteye.com/blog/655619 
http://blog.sina.com.cn/s/blog_605f5b4f0100qfvc.html 
http://mgoann.iteye.com/blog/1422680 

http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html


轉載自:http://zheng12tian.iteye.com/blog/1495037

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章