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 -javaagent:agent_jar_path[=options] java_app_name
1
可以在啟動名為java_app_name的應用之前啟動一個agent_jar_path指定位置的agent jar。 實現這樣一個agent jar包,必須滿足兩個條件:
1.在這個jar包的manifest文件中包含Premain-Class屬性,並且改屬性的值為代理類全路徑名。
2.代理類必須提供一個
public static void premain(String args, Instrumentation inst)
1
或
public static void premain(String args)
1
方法。
當在命令行啟動該代理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());
}
}
1
2
3
4
5
6
7
8
premain有兩個參數,args為自定義傳入的代理類參數,inst為VM自動傳入的Instrumentation實例。 premain方法的內容很簡單,除了標准輸出外,只有
inst.addTransformer(new TestTransformer());
1
這行代碼的意思是向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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
TestTransformer實現了ClassFileTransformer接口,該接口只有一個transform方法,參數傳入包括該類的類加載器,類名,原字節碼字節流等,返回被轉換后的字節碼字節流。 TestTransformer主要使用ASM實現在所有的類定義的方法中,在方法開始出添加了一段打印該類名和方法名的字節碼。在轉換完成后返回新的字節碼字節流。
設置MANIFEST.MF
設置MANIFEST.MF文件中的屬性,文件內容如下:
Manifest-Version: 1.0
Premain-Class: test.Agent
Created-By: 1.6.0_29
1
2
3
4
測試
代碼編寫完成后將代碼編譯打成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");
}
}
1
2
3
4
5
6
7
8
9
10
從命令行執行該類,並設置agent.jar
java -javaagent:agent.jar TestAgent
1
將打印出程序運行過程中實際執行過的所有方法名:
Hi, I'm agent!
Enter method-> test/TestAgent.main
Enter method-> test/TestAgent.test
I'm TestAgent
。。。。。
1
2
3
4
5
從輸出中可以看出,程序首先執行的是代理類中的premain方法(不過代理類自身不會被自己轉換,所以不能打印出代理類的方法名),然后是應用程序中的main方法。
agentmain方式
參見:http://blog.csdn.net/u010039929/article/details/70117018
代理 (agent) 是在你的main方法前的一個攔截器 (interceptor),也就是在main方法執行之前,執行agent的代碼。agent的代碼與你的main方法在同一個JVM中運行,並被同一個system classloader裝載,被同一的安全策略 (security policy) 和上下文 (context) 所管理。叫代理(agent)這個名字有點誤導的成分,它與我們一般理解的代理不大一樣。java agent使用起來比較簡單。
怎樣寫一個java agent? 只需要實現premain這個方法
public static void premain(String agentArgs, Instrumentation inst)
1
JDK 6 中如果找不到上面的這種premain的定義,還會嘗試調用下面的這種premain定義:
public static void premain(String agentArgs)
1
Agent 類必須打成jar包,然后里面的 META-INF/MAINIFEST.MF 必須包含 Premain-Class這個屬性。
下面是一個MANIFEST.MF的例子:
Manifest-Version: 1.0
Premain-Class:MyAgent1
Created-By:1.6.0_06
1
2
3
然后把MANIFEST.MF 加入到你的jar包中。
所有的這些Agent的jar包,都會自動加入到程序的classpath中。所以不需要手動把他們添加到classpath。 除非你想指定classpath的順序。
一個java程序中-javaagent這個參數的個數是沒有限制的,所以可以添加任意多個java agent。所有的java agent會按照你定義的順序執行。
例如:
java -javaagent:MyAgent1.jar -javaagent:MyAgent2.jar -jar MyProgram.jar
1
假設MyProgram.jar里面的main函數在MyProgram中。
MyAgent1.jar, MyAgent2.jar, 這2個jar包中實現了premain的類分別是MyAgent1, MyAgent2
程序執行的順序將會是
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
5. 另外,放在main函數之后的premain是不會被執行的,
例如
java -javaagent:MyAgent1.jar -jar MyProgram.jar -javaagent:MyAgent2.jar
1
MyAgent2 和MyAgent3 都放在了MyProgram.jar后面,所以MyAgent2的premain都不會被執行,
每一個java agent 都可以接收一個字符串類型的參數,也就是premain中的agentArgs,這個agentArgs是通過java option中定義的。
如:
java -javaagent:MyAgent2.jar=thisIsAgentArgs -jar MyProgram.jar
1
MyAgent2中premain接收到的agentArgs的值將是”thisIsAgentArgs” (不包括雙引號)
參數中的Instrumentation:
通過參數中的Instrumentation inst,添加自己定義的ClassFileTransformer,來改變class文件。
這里自定義的Transformer實現了transform方法,在該方法中提供了對實際要執行的類的字節碼的修改,甚至可以達到執行另外的類方法的地步
通過java agent就可以不用修改原有的java程序代碼,通過agent的形式來修改或者增強程序了,或者做熱啟動等等。
其實是執行在premain中添加的ClassFileTransformer的Transform方法(有可能需要對被執行的類的字節碼做修改)
最后才是執行類的main方法
Premain 加載:
想調用ASM API (用於字節碼處理的開源API)對字節碼進行處理,目標是實現對Java程序運行時各種對象的動態跟蹤,並進一步分析各個對象之間的關系(研究前提是目前的UML鎖闡釋的whole-part relation 是比較混亂的)。由於ASM相關內容又可以延伸很遠,在此文中略過。
在完成了能對字節碼進行處理的ASM調用以后,需要考慮如何將這些功能與正常的java程序整合到一起。
首先,我考慮到了自定義ClassLoader的方法即在程序的main入口處,首先加載自定義的classloader,然后通過reflect技術使用這個classloader加載並調用測試程序(就是被跟蹤的程序,姑且稱之為測試程序)的入口類。
這個方法一個很大的限制,在於每次都必須先找到測試程序的入口類,而對於有的封裝成jar的程序集合,這一點相對比較難控制。
於是,有了這里介紹的方法:通過 java.lang.instrument 實現的java agent對象操作字節碼,也就試所謂的AOP(面向方面編程)
原理
JVMTI(Java Virtual Machine Tool Interface)是一套本地編程接口集合,它提供了一套”代理”程序機制,可以支持第三方工具程序以代理的方式連接和訪問 JVM,並利用 JVMTI 提供的豐富的編程接口,完成很多跟 JVM 相關的功能。
java.lang.instrument 包的實現,也就是基於這種機制的:在 Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,通過調用 JVMTI 當中 Java 類相關的函數來完成 Java 類的動態操作。
Instrumentation 的最大作用,就是類定義動態改變和操作。在 Java SE 5 及其后續版本當中,開發者可以在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 – javaagent參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啟動 Instrumentation 的代理程序。
步驟
1.編寫java代理類。
這個類中,premain方法是關鍵,對比於一般的入口main一樣,這里的premain是在main之前執行的。它會告訴JVM如何處理加載上來的java字節碼。如下例:
public static void premain(String agentArgs, Instrumentation inst) {
Trace.BeginTrace(); // it's important for trace files
inst.addTransformer(new ASMAgent());
}
1
2
3
4
public static void premain(String agentArgs, Instrumentation inst) {
Trace.BeginTrace(); // it's important for trace files
inst.addTransformer(new ASMAgent());
}
1
2
3
4
值得注意的是,addTransformer實現了對字節碼處理的方法的回調。
inst.addTransformer(new ASMAgent());
1
類ASMAgent包含着實現對java字節碼處理的方法:transform()。它來自於ClassFileTransformer接口。為了方便,博主這里將對ClassFileTransformer接口的實現跟ASMAgent類放在了一起。
public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer)
throws IllegalClassFormatException {
byte[] retVal = null;
if(isInstrumentable(className)){
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ASMClassAdapter mca = new ASMClassAdapter(cw);
ClassReader cr = new ClassReader(classfileBuffer);
cr.accept(mca, 0);
retVal = cw.toByteArray();
}else{
retVal = classfileBuffer ;
}
return retVal;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2.打包代理類。
只有合理打包並在manifest文件中記錄下相應的鍵值對之后,才能正常執行premain的內容。
manifest文件中需要添加的鍵值對是:
Premain-Class : test.asm.ASMAgent
1
另外,如果對字節碼的處理有應用到了其他的類,需要在manifest中增加路徑。鍵值對為:
Class-Path: asm-3.0.jar
1
3.執行
執行測試程序時,添加“-javaagent:代理類的jar[=傳入premain的參數]”選項。
java -javaagent:ASMInstrument.jar -jar XXXX.jar xxxx
1
其中ASMInstrument.jar 是第二步中打包的程序, XXX.jar是需要測試的程序, xxx是XXX.jar 執行時可能的命令行參數。
如果只是執行某.class文件中的類,我們假設是在當前目錄下的一個XXXX類,則是:
java -javaagent:ASMInstrument.jar -cp ./ XXXX xxx
1
其中xxx是可能的命令行參數。