java.lang.instrument 學習(一)

Instrumentation介紹:

java Instrumentation指的是可以用獨立於應用程序之外的代理(agent)程序來監測和協助運行在JVM上的應用程序。這種監測和協助包括但不限於獲取JVM運行時狀態,替換和修改類定義等。 Java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理機制,支持獨立於JVM應用程序之外的程序以代理的方式連接和訪問JVM。Instrumentation 的最大作用就是類定義的動態改變和操作。在 Java SE 5 及其後續版本當中,開發者可以在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 – javaagent 參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啓動 Instrumentation 的代理程序。

premain方式
在Java SE5時代,Instrument只提供了premain一種方式,即在真正的應用程序(包含main方法的程序)main方法啓動前啓動一個代理程序。例如使用如下命令:

[java] view plaincopy
  1. java -javaagent:agent_jar_path[=options] java_app_name  

可以在啓動名爲java_app_name的應用之前啓動一個agent_jar_path指定位置的agent jar。 實現這樣一個agent jar包,必須滿足兩個條件:


  1. 在這個jar包的manifest文件中包含Premain-Class屬性,並且改屬性的值爲代理類全路徑名。
  2. 代理類必須提供一個public static void premain(String args, Instrumentation inst)或 public static void premain(String args) 方法。
當在命令行啓動該代理jar時,VM會根據manifest中指定的代理類,使用於main類相同的系統類加載器(即ClassLoader.getSystemClassLoader()獲得的加載器)加載代理類。在執行main方法前執行premain()方法。如果premain(String args, Instrumentation inst)和premain(String args)同時存在時,優先使用前者。其中方法參數args即命令中的options,類型爲String(注意不是String[]),因此如果需要多個參數,需要在方法中自己處理(比如用";"分割多個參數之類);inst是運行時由VM自動傳入的Instrumentation實例,可以用於獲取VM信息。

premain實例-打印所有的方法調用
下面實現一個打印程序執行過程中所有方法調用的功能,這個功能可以通過AOP其他方式實現,這裏只是嘗試使用Instrumentation進行ClassFile的字節碼轉換實現:

構造agent類

premain方式的agent類必須提供premain方法,代碼如下:

[java] view plaincopy
  1. package test;  
  2.   
  3. import java.lang.instrument.Instrumentation;  
  4.   
  5. public class Agent {  
  6.   
  7.     public static void premain(String args, Instrumentation inst){  
  8.         System.out.println("Hi, I'm agent!");  
  9.         inst.addTransformer(new TestTransformer());  
  10.     }  
  11. }  
premain有兩個參數,args爲自定義傳入的代理類參數,inst爲VM自動傳入的Instrumentation實例。 premain方法的內容很簡單,除了標準輸出外,只有

[java] view plaincopy
  1. inst.addTransformer(new TestTransformer());  
這行代碼的意思是向inst中添加一個類的轉換器。用於轉換類的行爲。

構造Transformer

下面來實現上述過程中的TestTransformer來完成打印調用方法的類定義轉換。

[java] view plaincopy
  1. package test;  
  2.   
  3. import java.lang.instrument.ClassFileTransformer;  
  4. import java.lang.instrument.IllegalClassFormatException;  
  5. import java.security.ProtectionDomain;  
  6.   
  7. import org.objectweb.asm.ClassReader;  
  8. import org.objectweb.asm.ClassWriter;  
  9. import org.objectweb.asm.Opcodes;  
  10. import org.objectweb.asm.tree.ClassNode;  
  11. import org.objectweb.asm.tree.FieldInsnNode;  
  12. import org.objectweb.asm.tree.InsnList;  
  13. import org.objectweb.asm.tree.LdcInsnNode;  
  14. import org.objectweb.asm.tree.MethodInsnNode;  
  15. import org.objectweb.asm.tree.MethodNode;  
  16.   
  17. public class TestTransformer implements ClassFileTransformer {  
  18.   
  19.     @Override  
  20.     public byte[] transform(ClassLoader arg0, String arg1, Class<?> arg2,  
  21.             ProtectionDomain arg3, byte[] arg4)  
  22.             throws IllegalClassFormatException {  
  23.         ClassReader cr = new ClassReader(arg4);  
  24.         ClassNode cn = new ClassNode();  
  25.         cr.accept(cn, 0);  
  26.         for (Object obj : cn.methods) {  
  27.             MethodNode md = (MethodNode) obj;  
  28.             if ("<init>".endsWith(md.name) || "<clinit>".equals(md.name)) {  
  29.                 continue;  
  30.             }  
  31.             InsnList insns = md.instructions;  
  32.             InsnList il = new InsnList();  
  33.             il.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System",  
  34.                     "out""Ljava/io/PrintStream;"));  
  35.             il.add(new LdcInsnNode("Enter method-> " + cn.name+"."+md.name));  
  36.             il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,  
  37.                     "java/io/PrintStream""println""(Ljava/lang/String;)V"));  
  38.             insns.insert(il);  
  39.             md.maxStack += 3;  
  40.   
  41.         }  
  42.         ClassWriter cw = new ClassWriter(0);  
  43.         cn.accept(cw);  
  44.         return cw.toByteArray();  
  45.     }  
  46.   
  47. }  

TestTransformer實現了ClassFileTransformer接口,該接口只有一個transform方法,參數傳入包括該類的類加載器,類名,原字節碼字節流等,返回被轉換後的字節碼字節流。 TestTransformer主要使用ASM實現在所有的類定義的方法中,在方法開始出添加了一段打印該類名和方法名的字節碼。在轉換完成後返回新的字節碼字節流。詳細的ASM使用請參考ASM手冊。

設置MANIFEST.MF

設置MANIFEST.MF文件中的屬性,文件內容如下:

[java] view plaincopy
  1. Manifest-Version: 1.0  
  2. Premain-Class: test.Agent  
  3. Created-By: 1.6.0_29  
測試
代碼編寫完成後將代碼編譯打成agent.jar。 編寫測試代碼:
[java] view plaincopy
  1. public class TestAgent {  
  2.   
  3.     public static void main(String[] args) {  
  4.         TestAgent ta = new TestAgent();  
  5.         ta.test();  
  6.     }  
  7.   
  8.     public void test() {  
  9.         System.out.println("I'm TestAgent");  
  10.     }  
  11.   
  12. }  

從命令行執行該類,並設置agent.jar

[java] view plaincopy
  1. java -javaagent:agent.jar TestAgent  
將打印出程序運行過程中實際執行過的所有方法名:
[java] view plaincopy
  1. Hi, I'm agent!  
  2. Enter method-> test/TestAgent.main  
  3. Enter method-> test/TestAgent.test  
  4. I'm TestAgent  
  5. Enter method-> java/util/IdentityHashMap$KeySet.iterator  
  6. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext  
  7. Enter method-> java/util/IdentityHashMap$KeyIterator.next  
  8. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex  
  9. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext  
  10. Enter method-> java/util/IdentityHashMap$KeySet.iterator  
  11. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext  
  12. Enter method-> java/util/IdentityHashMap$KeyIterator.next  
  13. Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex  
  14. Enter method-> com/apple/java/Usage$3.run  
  15. 。。。  

從輸出中可以看出,程序首先執行的是代理類中的premain方法(不過代理類自身不會被自己轉換,所以不能打印出代理類的方法名),然後是應用程序中的main方法。

agentmain方式

premain時Java SE5開始就提供的代理方式,給了開發者諸多驚喜,不過也有些須不變,由於其必須在命令行指定代理jar,並且代理類必須在main方法前啓動。因此,要求開發者在應用前就必須確認代理的處理邏輯和參數內容等等,在有些場合下,這是比較苦難的。比如正常的生產環境下,一般不會開啓代理功能,但是在發生問題時,我們不希望停止應用就能夠動態的去修改一些類的行爲,以幫助排查問題,這在應用啓動前是無法確定的。 爲解決運行時啓動代理類的問題,Java SE6開始,提供了在應用程序的VM啓動後在動態添加代理的方式,即agentmain方式。 與Permain類似,agent方式同樣需要提供一個agent jar,並且這個jar需要滿足:

  1. 在manifest中指定Agent-Class屬性,值爲代理類全路徑
  2. 代理類需要提供public static void agentmain(String args, Instrumentation inst)或public static void agentmain(String args)方法。並且再二者同時存在時以前者優先。args和inst和premain中的一致。

不過如此設計的再運行時進行代理有個問題——如何在應用程序啓動之後再開啓代理程序呢? JDK6中提供了Java Tools API,其中Attach API可以滿足這個需求。

Attach API中的VirtualMachine代表一個運行中的VM。其提供了loadAgent()方法,可以在運行時動態加載一個代理jar。具體需要參考《Attach API》

agentmain實例-打印當前已加載的類

構造agent類

agentmain方式的代理類必須提供agentmain方法:

package loaded;

import java.lang.instrument.Instrumentation;

public class LoadedAgent {
    @SuppressWarnings("rawtypes")
    public static void agentmain(String args, Instrumentation inst){
        Class[] classes = inst.getAllLoadedClasses();
        for(Class cls :classes){
            System.out.println(cls.getName());
        }
    }
}

agentmain方法通過傳入的Instrumentation實例獲取當前系統中已加載的類。

設置MANNIFEST.MF

設置MANIFEST.MF文件,指定Agent-Class:

Manifest-Version: 1.0
Agent-Class: loaded.LoadedAgent
Created-By: 1.6.0_29

綁定到目標VM

將agent類和MANIFEST.MF文件編譯打成loadagent.jar後,由於agent main方式無法向pre main方式那樣在命令行指定代理jar,因此需要藉助Attach Tools API。

package attach;

import java.io.IOException;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

public class Test {
    public static void main(String[] args) throws AttachNotSupportedException,
            IOException, AgentLoadException, AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        vm.loadAgent("/Users/jiangbo/Workspace/code/java/javaagent/loadagent.jar");

    }

}

該程序接受一個參數爲目標應用程序的進程id,通過Attach Tools API的VirtualMachine.attach方法綁定到目標VM,並向其中加載代理jar。

構造目標測試程序

構造一個測試用的目標應用程序:

package attach;

public class TargetVM {
    public static void main(String[] args) throws InterruptedException{
        while(true){
            Thread.sleep(1000);
        }
    }
}

這個測試程序什麼都不做,只是不停的sleep。:) 運行該程序,獲得進程ID=33902。 運行上面綁定到VM的Test程序,將進程id作爲參數傳入:

java attach.Test 33902

觀察輸出,會打印出系統當前所有已經加載類名

java.lang.NoClassDefFoundError
java.lang.StrictMath
java.security.SignatureSpi
java.lang.Runtime
java.util.Hashtable$EmptyEnumerator
sun.security.pkcs.PKCS7
java.lang.InterruptedException
java.io.FileDescriptor$1
java.nio.HeapByteBuffer
java.lang.ThreadGroup
[Ljava.lang.ThreadGroup;
java.io.FileSystem
。。。

參考文檔

附:agent jar中manifest的屬性

  • Premain-Class: 當在VM啓動時,在命令行中指定代理jar時,必須在manifest中設置Premain-Class屬性,值爲代理類全類名,並且該代理類必須提供premain方法。否則JVM會異常終止。
  • Agent-Class: 當在VM啓動之後,動態添加代理jar包時,代理jar包中manifest必須設置Agent-Class屬性,值爲代理類全類名,並且該代理類必須提供agentmain方法,否則無法啓動該代理。
  • Boot-Class-Path: Bootstrap class loader加載類時的搜索路徑,可選。
  • Can-Redefine-Classes: true/false;標示代理類是否能夠重定義類。可選。
  • Can-Retransform-Classes: true/false;標示代理類是否能夠轉換類定義。可選。
  • Can-Set-Native-Prefix::true/false;標示代理類是否需要本地方法前綴,可選。

當一個代理jar包中的manifest文件中既有Premain-Class又有Agent-Class時,如果以命令行方式在VM啓動前指定代理jar,則使用Premain-Class;反之如果在VM啓動後,動態添加代理jar,則使用Agent-Class


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