JavaAgent
啟動時加載的 JavaAgent 是 JDK1.5 之后引入的新特性,此特性為用戶提供了在 JVM 將字節(jié)碼文件讀入內(nèi)存之后,JVM 使用對應(yīng)的字節(jié)流在 Java 堆中生成一個 Class 對象之前,用戶可以對其字節(jié)碼進(jìn)行修改的能力,從而JVM也將會使用用戶修改過之后的字節(jié)碼進(jìn)行 Class 對象的創(chuàng)建。
JVM Tool Interface
JVMTI 是 JVM 暴露出來的一些供用戶進(jìn)行自定義擴(kuò)展的接口集合,每當(dāng) jvm 執(zhí)行到一些特定的邏輯的時間,就會去進(jìn)行觸發(fā)這些回調(diào)接口,用戶就恰好可以在此回調(diào)接口之中做一些自定義邏輯。
而對于此次所要描述的 JavaAgent 也恰恰是基于 JVMTI 的,JPLISAgent 就是用作實現(xiàn) javaagent 功能的動態(tài)庫。
JPLISAgent
JPLISAgent 實現(xiàn)了 Agent_OnLoad 方法,Agent_OnLoad 方法就是整個啟動時加載的 JavaAgent 的入口方法,后續(xù)也會說明整個運行流程。
如何使用
雖然大多數(shù)同學(xué)可能已經(jīng)使用過 JavaAgent 了,但是為了下面原理的平滑過渡,我這里還是大概寫一下使用:
編寫 premain 啟動類
編寫一個含有以下 premain 函數(shù)的類
public static void premain(String agentArgs, Instrumentation instrumentation);[1]
public static void premain(String agentArgs);[2]
上面的兩個方法只需要實現(xiàn)一個即可,且 [1] 的優(yōu)先級是高于 [2] 的,即如果上面的兩個方法同時出現(xiàn),則只會執(zhí)行 [1] 方法。
agentArgs是javaagent:xx.jar=yyy傳入yyy字符串是java.lang.instrument.Instrumentation 實例,由本地方法實例化并由 jvm自動傳入。
此類是 JavaAgent 的核心類。
public class Agent {
public static void premain(String args, Instrumentation inst){
System.out.println("Hi, This is a agent!");
inst.addTransformer(new TestTransformer());
// 將類轉(zhuǎn)換器添加到此`agent`的`instrumentation`實例之中
}
}
類轉(zhuǎn)換器
類轉(zhuǎn)換器的作用主要是在某個類的字節(jié)碼被 JVM 讀入之后,在 Java 堆上創(chuàng)建 Class 對象之前,JVM 會遍歷所有的 instrumentation 實例并執(zhí)行其中的所有的ClassFileTransformer 的 transform 方法,其中關(guān)于啟動時加載的 javaAgent 重點需要關(guān)注的入?yún)?/strong>:
- className:當(dāng)前類的限定類名。
- classfileBuffer:當(dāng)前類的以 byte 數(shù)組呈現(xiàn)的字節(jié)碼數(shù)據(jù)(可能跟 class 文件的數(shù)據(jù)不一致, 因為此處的 byte 數(shù)據(jù)是此類最新的字節(jié)碼數(shù)據(jù),即此數(shù)據(jù)可能是原始字節(jié)碼數(shù)據(jù)被其他增強(qiáng)方法增強(qiáng)之后的自己買數(shù)據(jù))
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 進(jìn)行對應(yīng)類字節(jié)碼的操作,并返回新字節(jié)碼數(shù)據(jù)的 byte 數(shù)組,如果返回 null,則代碼不對此字節(jié)碼作任何操作
return null;
}
}
然后將上述的代碼打成 jar 包并在 jvm 啟動時增加 -javaagent:xx.jar 即可以完成 javaAgent 的生效。
實現(xiàn)原理
上面關(guān)于 JavaAgent 的使用有涉及到這么幾個關(guān)鍵字:
premain,Instrumentation,ClassFileTransformer,MAINIFEST.MF 中的 Premain-Class
對于在啟動命令添加的 -javaagent:xx.jar,如果有多個加載順序就是從前往后,且每一個 -javaagent 都是獨立的。
以下的加載流程僅僅只是針對其中的一個javaagent描述的。
javaAgent 的入口方法就是 InvocationAdapter.c 中的 Agent_OnLoad 方法。經(jīng)過查看 openjdk 源碼,發(fā)現(xiàn)如下注釋
由注釋我們可以指定,每一個 -javaagent 都會有其自己的 agent 和 agent 數(shù)據(jù),且每一個 javaagent 都會調(diào)用一次 Agent_OnLoad 方法就會被調(diào)用一次,且每一次的調(diào)用都是獨立的。
在 Agent_OnLoad 方法中主要做的事情有下面三個:
1)初始化一個 JPLISAgent 對象,并給此對象設(shè)置 VMInit 事件的回調(diào)函數(shù) eventHandlerVMInit.
2)找到 jvm 啟動參數(shù)中 -javaagent:xx.jar=yyy 中的 xx.jar 文件添加到 classpath 之中,并獲取 yyy
3)找到 xx.jar 包中的 MAINIFEST.MF 中定義的 premainClass 并作為此 Agent 的入口類
4)并將 premainClass 和 yyy 設(shè)置到步驟 1 初始化的 JPLISAgent 對象之中。
當(dāng) VMInit 事件完成以后,會回調(diào) InvocationAdapter.c中的 eventHandlerVMInit 方法,eventHandlerVMInit 方法主要做的事情有下面:
1)實例化一個 InstrumentationImpl 對象,jvm 并依借此對象與 java 代碼進(jìn)行交互。
2)通過 JNI 執(zhí)行 MAINIFEST.MF 中定義的類中的 premain 方法(我們上面的例子之中在 premain 方法中給 Instrumentation 對象添加了一個 ClassFileTransformer)
3)去除 JPLISAgent 對象中的 VMInit 回調(diào)函數(shù),轉(zhuǎn)而設(shè)置一個 ClassFileLoadHook 事件的回調(diào)函數(shù)。
當(dāng) ClassFileLoadHook 事件 (在字節(jié)碼文件被 jvm 讀入之后,在 Class 對象創(chuàng)建之前) 完成后,進(jìn)行觸發(fā) eventHandlerClassFileLoadHook,此方法主要做的事情有下面幾件:
進(jìn)行調(diào)用 InstrumentationImpl 對象中的 mTransform 方法,而對于 mTransform 方法,最終會調(diào)用到我們在 Agent 的 premain 方法中給 Instrumentation 增加的 ClassFileTransformer。
此時,JVM 會通過 JNI 調(diào)用 java 代碼,對應(yīng)的類就是 sun.instrument.InstrumentationImpl 類之中,而對應(yīng)于 mTransform 的方法就是 byte[] transform(ClassLoader var1, String var2, Class<?> var3, ProtectionDomain var4, byte[] var5, boolean var6)
方法是上述中 c 代碼調(diào)用的地方,其中的 var5 是對應(yīng)當(dāng)前文件的字節(jié)碼數(shù)據(jù), 如果此接口返回的數(shù)據(jù)為 null,則認(rèn)為 transform 方法并未對此 class 文件有過修改,如果返回的數(shù)據(jù)不為 null,則會使用返回的新字節(jié)碼作為 jvm 中此類最新的字節(jié)碼并進(jìn)行下一個
Javaagent 的處理或者創(chuàng)建 Class 對象進(jìn)行鏈接以及初始化。
其中對于 c 代碼通過 JNI 反射調(diào)用 java 代碼的方法聲明都在 JPLISAgent.h 類中可以看見
struct _JPLISAgent;
typedef struct _JPLISAgent JPLISAgent;
typedef struct _JPLISEnvironment JPLISEnvironment;
7/* constants for class names and methods names and such
8 these all must stay in sync with Java code & interfaces
9*/
10#define JPLIS_INSTRUMENTIMPL_CLASSNAME "sun/instrument/InstrumentationImpl"
11#define JPLIS_INSTRUMENTIMPL_CONSTRUCTOR_METHODNAME "<init>"
12#define JPLIS_INSTRUMENTIMPL_CONSTRUCTOR_METHODSIGNATURE "(JZZ)V"
13#define JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODNAME "loadClassAndCallPremain"
14#define JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODSIGNATURE "(Ljava/lang/String;Ljava/lang/String;)V"
15#define JPLIS_INSTRUMENTIMPL_AGENTMAININVOKER_METHODNAME "loadClassAndCallAgentmain"
16#define JPLIS_INSTRUMENTIMPL_AGENTMAININVOKER_METHODSIGNATURE "(Ljava/lang/String;Ljava/lang/String;)V"
17#define JPLIS_INSTRUMENTIMPL_TRANSFORM_METHODNAME "transform"
18#define JPLIS_INSTRUMENTIMPL_TRANSFORM_METHODSIGNATURE \
19 "(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/Class;Ljava/security/ProtectionDomain;[BZ)[B"
對于啟動時加載的 JavaAgent 涉及到的 c 代碼主要為:
其中涉及到的 c 代碼在 /src/share/instrument 目錄下的 JPLISAgent.c,JPLISAgent.h,InvocationAdapter.c。
涉及到的 java 代碼為 sun.instrument.InstrumentationImpl。
字節(jié)碼工具
上面我們說過,javaAgent 提供了一些接口可以讓我們在某些特定的時機(jī)進(jìn)行對于 java 字節(jié)碼的操作,在不改變代碼的前提下,修改最終載入內(nèi)存的字節(jié)碼。但是由于直接操作字節(jié)碼需要對 java 的字節(jié)碼底層有較深入的研究。所以一些能幫助我們
不需要了解字節(jié)碼底層也能修改字節(jié)碼的工具就誕生了。其中較為流行的字節(jié)碼操作工具有 byteBuddy 和 javassist 等等。
Javassist 沖突
使用了兩種 JavaAgent(一種 JavaAgent 是基于 ByteBuddy,另外一種 JavaAgent 是基于 Javassist)同時去增強(qiáng)同一個類的同一個方法:
當(dāng)兩個 JavaAgent 的載入順序為:Javassist-ByteBuddy,這兩個 JavaAgent 對于同一個類的同一個方法的增強(qiáng)都生效了
當(dāng)兩個 JavaAgent 的載入順序為:ByteBuddy-Javassist,Javassist 對應(yīng)的增強(qiáng)生效了,而 ByteBuddy 對應(yīng)的增強(qiáng)卻沒生效
當(dāng)使用三個 JavaAgent(兩個 Javassist,一個 ByteBuddy)的載入順序為:Javassist-ByteBuddy-Javassist,兩個 Javassist 的增強(qiáng)都生效了,而 ByteBuddy 對應(yīng)的增強(qiáng)卻沒生效
沖突根因
針對上面結(jié)果的不同,我們來開始分析不同的 JavaAgent 的實現(xiàn)在不同加載順序下為什么會造成如此大的差異呢?
使用 Javassist 創(chuàng)建 JavaAgent
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass(className);
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
for (int i = 0; i < ctMethods.length; i++) {
CtMethod ctMethod = ctMethods[i];
if (!ctMethod.getName().equalsIgnoreCase("main")) {
continue
}
ctMethod.insertBefore("System.out.println(\" 這是第 2 個 javassist Agent Before~~~~~\");");
ctMethod.insertAfter("System.out.println(\" 這是第 2 個 javassist Agent After~~~~~\");");
}
return ctClass.toBytecode();
其中對于 Javassist 是使用 ClassPool 對象來存儲對應(yīng)的 class 對象,對于默認(rèn)的 defaultPool 是 static 屬性,故如果有多個基于 Javassist 實現(xiàn)的 JavaAgent,那么它們使用的 ClassPool 是同一個對象。
當(dāng)使用 classPool.getCtClass(className) 方法獲取一個 CtClass 對象,其中 Javassist 都做了哪些事情呢?
讓我們進(jìn)去看看
protected synchronized CtClass get0(String classname, boolean useCache) throws NotFoundException {
CtClass clazz = null;
if (useCache) {
clazz = this.getCached(classname);
if (clazz != null) {
return clazz;
}
}
if (!this.childFirstLookup && this.parent != null) {
clazz = this.parent.get0(classname, useCache);
if (clazz != null) {
return clazz;
}
}
clazz = this.createCtClass(classname, useCache);
if (clazz != null) {
if (useCache) {
this.cacheCtClass(clazz.getName(), clazz, false);
}
return clazz;
}else {
if (this.childFirstLookup && this.parent != null) {
clazz = this.parent.get0(classname, useCache);
}
return clazz;
}
}
由上面源碼可以當(dāng) ClassPool 的 Cache 中不存在對應(yīng)的 class 的時候,javassist 會調(diào)用 this.createCtClass(classname, useCache) 來實例化一個 CtClass 對象,那它是如何創(chuàng)建的呢?
protected CtClass createCtClass(String classname, boolean useCache) {
if (classname.charAt(0) == '[') {
classname = Descriptor.toClassName(classname);
}
if (!classname.endsWith("[]")) {
return this.find(classname) == null ? null : new CtClassType(classname, this);
} else {
String base = classname.substring(0, classname.indexOf(91));
return (!useCache || this.getCached(base) == null) && this.find(base) == null ? null : new CtArray(classname, this);
}
}
可以看出,對于此次,javassist 僅僅只是使用 className 用實例化了個 CtClassType 對象。
當(dāng)獲取了 CtClass 對象之后,我們想知道其中都有哪些方法,接下來使用 ctClass.getDeclaredMethods() 來獲取其中的方法,我們看看他是如何獲取的呢?
getDeclaredMethods()–>getMembers()–>makeBehaviorCache(cache)–>getClassFile2()–>openClassfile(classname)
public InputStream openClassfile(String classname) {
try {
char sep = File.separatorChar;
String filename = this.directory + sep + classname.replace('.', sep) + ".class";
return new FileInputStream(filename.toString());
} catch (FileNotFoundException var4) {
;
} catch (SecurityException var5) {
;
}
return null;
}
可以看出 javassist 獲取到對應(yīng)類的 class 文件,并以文件流的形式讀入以獲取到其 class 中所有的方法和屬性并緩存在 CtClassType 對象之中(由此,我們可以明確一個問題,第一個 javassist 對于字節(jié)碼的修改都是基于對應(yīng)類的源字節(jié)碼文件)
當(dāng) javasist 使用了 ctMethod.insertBefore 方法對某方法進(jìn)行增強(qiáng)的之后,會刷新其對應(yīng) ClassPool 之中緩存的 CtClassType 的屬性。
當(dāng)?shù)诙€基于 Javassist 實現(xiàn)的 JavaAgent 對相同的類進(jìn)行增強(qiáng)的時候,其也會去 ClassPool 中獲取 CtClass 對象,而這個 CtClass 恰恰就是上一個 JavaAgent 對原始字節(jié)碼增強(qiáng)之后刷新的結(jié)果,這樣可以說明第二個 JavaAgent 在增強(qiáng)的時候使用的字節(jié)碼的源文件跟第一個 JavaAgent 使用的字節(jié)碼的源文件的獲取方式不同。
沖突結(jié)論
根據(jù)上述現(xiàn)象與原理的分析,我們可以得出:
如果同時使用基于 Javassist 和基于其他字節(jié)碼工具的 JavaAgent 去增強(qiáng)同一個類,Javassist 的加載順序一定要在其他字節(jié)碼的 JavaAgent 之前,這樣才能保證兩個字節(jié)碼工具都可以進(jìn)行完整的增強(qiáng)。如果基于 Javassist 的 JavaAgent 最后增強(qiáng),那么之前的非Javassist 的 JavaAgent 對于字節(jié)碼的增強(qiáng)都會被丟棄掉,這也能會帶來不小的麻煩。