Java技術(shù)專題-探針Agent底層運作原理和分析(2)

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)都會被丟棄掉,這也能會帶來不小的麻煩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容