java位元組碼增強技術實現過程
什麼是Instrumentation?
查閱java api可知,
軟體包 java.lang.instrument 的描述
提供允許 Java 程式語言代理監測執行在 JVM 上的程式的服務。監測的機制是對方法的位元組碼的修改。包規範
在啟動 JVM 時,通過指示代理類及其代理選項 啟動一個代理程式。
該代理類必須實現公共的靜態premain
方法,該方法原理上類似於
main
應用程式入口點:
public static void premain(String agentArgs, Instrumentation inst);
JVM 被初始化之後,每個premain
方法將按照指定代理的順序被呼叫。然後,呼叫實際的應用程式
main
premain
方法必須按順序返回,以便啟動序列能夠繼續。代理類將由載入包含應用程式main
方法的類的同一類載入器載入。premain
方法將在與應用程式
main
方法相同的安全性和類載入器規則下執行。不存在代理premain
方法可以執行的建模限制。應用程式
main
可以執行的任何事情(包括生成執行緒)從 premain
來看都是合法的。
每個代理程式通過agentArgs
引數傳遞其代理選項。代理選項作為單個字串傳遞,任何其他分析應由代理程式本身執行。
如果該代理程式不能被解析(例如,由於無法載入代理類,或由於代理類沒有一致的premain
方法),則 JVM 將中止。如果
premain
啟動命令列介面
在帶有命令列介面的 JVM 上,通過向 JVM 命令列新增此開關來指定代理程式:jarpath 是指向代理程式 JAR 檔案的路徑。options 是代理選項。此開關可以在同一命令列上多次使用,從而建立多個代理程式。多個代理程式可以使用同一jarpath。代理 JAR 檔案必須符合 JAR 檔案規範。下面的清單屬性是針對代理 JAR 檔案定義的:-javaagent:
jarpath[=
options]
Premain-Class
代理類。即包含
premain
方法的類。此屬性是必需的,如果它不存在,JVM 將中止。注:這是類名,而不是檔名或路徑。Boot-Class-Path
由引導類載入器搜尋的路徑列表。路徑表示目錄或庫(在許多平臺上通常作為 jar 或 zip 庫被引用)。查詢類的特定於平臺的機制出現故障之後,引導類載入器會搜尋這些路徑。按列出的順序搜尋路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑元件的語法。如果該路徑以斜槓字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 檔案的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。此屬性是可選的。
Can-Redefine-Classes
布林值(
true
或false
,與大小寫無關)。能夠重定義此代理所需的類。值如果不是true
,則被認為是false
。此屬性是可選的,預設值為false
。
代理 JAR 檔案附加到類路徑之後。
Java Instrumentation指的是可以用獨立於應用程式之外的代理(agent)程式來監測和協助執行在JVM上的應用程式。這種監測和協助包括但不限於獲取JVM執行時狀態,替換和修改類定義等。 java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理機制,支援獨立於JVM應用程式之外的程式以代理的方式連線和訪問JVM。Java.lang.instrument是在JVM TI的基礎上提供的Java版本的實現。 Instrumentation提供的主要功能是修改jvm中類的行為。 Java SE6中由兩種應用Instrumentation的方式,premain(命令列)和agentmain(執行時)
premain
在Java SE5時代,Instrument只提供了premain一種方式,即在真正的應用程式(包含main方法的程式)main方法啟動前啟動一個代理程式。例如使用如下命令:
java -javaagent:agent_jar_path[=options] java_app_name
可以在啟動名為java_app_name的應用之前啟動一個agent_jar_path指定位置的agent jar。 實現這樣一個agent jar包,必須滿足兩個條件:
- 在這個jar包的manifest檔案中包含Premain-Class屬性,並且改屬性的值為代理類全路徑名。
- 代理類必須提供一個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方法,程式碼如下:
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中新增一個類的轉換器。用於轉換類的行為。
構造Transformer
下面來實現上述過程中的TestTransformer來完成列印呼叫方法的類定義轉換。
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();
}
}
TestTransformer實現了ClassFileTransformer介面,該介面只有一個transform方法,引數傳入包括該類的類載入器,類名,原位元組碼位元組流等,返回被轉換後的位元組碼位元組流。 TestTransformer主要使用ASM實現在所有的類定義的方法中,在方法開始出添加了一段列印該類名和方法名的位元組碼。在轉換完成後返回新的位元組碼位元組流。詳細的ASM使用請參考ASM手冊。
設定MANIFEST.MF
設定MANIFEST.MF檔案中的屬性,檔案內容如下:
Manifest-Version: 1.0
Premain-Class: test.Agent
Created-By: 1.6.0_29
測試
程式碼編寫完成後將程式碼編譯打成agent.jar。 編寫測試程式碼:
public class TestAgent {
public static void main(String[] args) {
TestAgent ta = new TestAgent();
ta.test();
}
public void test() {
System.out.println("I'm TestAgent");
}
}
從命令列執行該類,並設定agent.jar
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方法。