轉自:http://jiangbo.me/blog/2012/02/21/java-lang-instrument/ html
Instrumentation介紹: java
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 的代理程序。 web
premain方式
在Java SE5時代,Instrument只提供了premain一種方式,即在真正的應用程序(包含main方法的程序)main方法啓動前啓動一個代理程序。例如使用以下命令:
api
java -javaagent:agent_jar_path[=options] java_app_name
premain實例-打印全部的方法調用
下面實現一個打印程序執行過程當中全部方法調用的功能,這個功能能夠經過AOP其餘方式實現,這裏只是嘗試使用Instrumentation進行ClassFile的字節碼轉換實現:
構造agent類
premain方式的agent類必須提供premain方法,代碼以下:
oracle
package test; import java.lang.instrument.Instrumentation; public class Agent { public static void premain(String args, Instrumentation inst){ System.out.println("Hi, I'm agent!"); inst.addTransformer(new TestTransformer()); } }premain有兩個參數,args爲自定義傳入的代理類參數,inst爲VM自動傳入的Instrumentation實例。 premain方法的內容很簡單,除了標準輸出外,只有
inst.addTransformer(new TestTransformer());這行代碼的意思是向inst中添加一個類的轉換器。用於轉換類的行爲。
package test; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; public class TestTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader arg0, String arg1, Class<?> arg2, ProtectionDomain arg3, byte[] arg4) throws IllegalClassFormatException { ClassReader cr = new ClassReader(arg4); ClassNode cn = new ClassNode(); cr.accept(cn, 0); for (Object obj : cn.methods) { MethodNode md = (MethodNode) obj; if ("<init>".endsWith(md.name) || "<clinit>".equals(md.name)) { continue; } InsnList insns = md.instructions; InsnList il = new InsnList(); il.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")); il.add(new LdcInsnNode("Enter method-> " + cn.name+"."+md.name)); il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V")); insns.insert(il); md.maxStack += 3; } ClassWriter cw = new ClassWriter(0); cn.accept(cw); return cw.toByteArray(); } }
設置MANIFEST.MF
設置MANIFEST.MF文件中的屬性,文件內容以下:
app
Manifest-Version: 1.0 Premain-Class: test.Agent Created-By: 1.6.0_29測試
public class TestAgent { public static void main(String[] args) { TestAgent ta = new TestAgent(); ta.test(); } public void test() { System.out.println("I'm TestAgent"); } }
java -javaagent:agent.jar TestAgent將打印出程序運行過程當中實際執行過的全部方法名:
Hi, I'm agent! Enter method-> test/TestAgent.main Enter method-> test/TestAgent.test I'm TestAgent Enter method-> java/util/IdentityHashMap$KeySet.iterator Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext Enter method-> java/util/IdentityHashMap$KeyIterator.next Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext Enter method-> java/util/IdentityHashMap$KeySet.iterator Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.hasNext Enter method-> java/util/IdentityHashMap$KeyIterator.next Enter method-> java/util/IdentityHashMap$IdentityHashMapIterator.nextIndex Enter method-> com/apple/java/Usage$3.run 。。。
從輸出中能夠看出,程序首先執行的是代理類中的premain方法(不過代理類自身不會被本身轉換,因此不能打印出代理類的方法名),而後是應用程序中的main方法。 ide
premain時Java SE5開始就提供的代理方式,給了開發者諸多驚喜,不過也有些須不變,因爲其必須在命令行指定代理jar,而且代理類必須在main方法前啓動。所以,要求開發者在應用前就必須確認代理的處理邏輯和參數內容等等,在有些場合下,這是比較苦難的。好比正常的生產環境下,通常不會開啓代理功能,可是在發生問題時,咱們不但願中止應用就可以動態的去修改一些類的行爲,以幫助排查問題,這在應用啓動前是沒法肯定的。 爲解決運行時啓動代理類的問題,Java SE6開始,提供了在應用程序的VM啓動後在動態添加代理的方式,即agentmain方式。 與Permain相似,agent方式一樣須要提供一個agent jar,而且這個jar須要知足: 函數
不過如此設計的再運行時進行代理有個問題——如何在應用程序啓動以後再開啓代理程序呢? JDK6中提供了Java Tools API,其中Attach API能夠知足這個需求。 測試
Attach API中的VirtualMachine表明一個運行中的VM。其提供了loadAgent()方法,能夠在運行時動態加載一個代理jar。具體須要參考《Attach API》 spa
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實例獲取當前系統中已加載的類。
設置MANIFEST.MF文件,指定Agent-Class:
Manifest-Version: 1.0 Agent-Class: loaded.LoadedAgent Created-By: 1.6.0_29
將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 。。。
當一個代理jar包中的manifest文件中既有Premain-Class又有Agent-Class時,若是以命令行方式在VM啓動前指定代理jar,則使用Premain-Class;反之若是在VM啓動後,動態添加代理jar,則使用Agent-Class