透過JVM-SANDBOX源碼,瞭解字節碼增強技術

 

介紹

JVM 沙箱容器是一種 JVM 的非侵入式運行期 AOP 解決方案。通過 JVM-SANDBOX 可以在不重啓,不侵入目標 jvm 的前提下對目標方法進行代碼增強。

無侵入,類隔離,可插拔,多租戶,高兼容是它的特性,JVM-SANDBOX 是相對偏底層的代碼增強框架利用它可以搞很多事情,例如線上系統流控、線上系統的請求錄製、結果回放,線上故障定位等等。如開源項目 jvm-sandbox-repeaterchaosblade 都是在此基礎上構建的,詳細介紹可以到 github 中瞭解,項目地址: https://github.com/alibaba/jvm-sandbox

在本文中將通過分析 JVM-SANDBOX 的底層源碼,從而瞭解一個字節碼增強框架的核心實現。

架構設計

JVM Sandbox 內置 HTTP 服務器(Jetty)接受用戶指令從而對模塊進行管理,例如激活,凍結,刷新等。通過自定義的 ClassLoader 進行類隔離,基於 Java 虛擬機工具接口(JVMTI),利用 ASM 框架對目標方法進行代碼增強。

JVM-SANDBOX 分爲多個子項目,其中 agent,core,spy 是主程序庫。agent:沙箱啓動的代理,core:沙箱內核,spy:沙箱間諜類(代碼增強的埋點類)。下圖介紹了 sandbox 整體架構中最重要的模塊/功能,這些能力都是在 agent,core,spy 三個子項目中提供的。

  1. 模塊控制管理:負責管理 sandbox 自身模塊以及使用者自定義模塊,例如模塊的加載,激活,凍結,卸載

  2. 事件監聽處理:用戶自定義模塊實現 Event 接口對增強的事件進行自定義處理,等待事件分發處理器的觸發。

  3. 沙箱事件分發處理器:對目標方法增強後會對目標方法追加三個環節,分別爲方法調用前 BEFORE、調用後 RETURN、調用拋異常 THROWS、當代碼執行到這三個環節時則會由分發器分配到對應的事件監聽執行器中執行。

  4. 代碼編織框架:通過 ASM 框架依託於 JVMTI 對目標方法進行字節碼修改,從而完成代碼增強的能力。

  5. 檢索過濾器:當用戶對目標方法創建增強事件時,沙箱會對目標 jvm 中的目標類和方法進行匹配以及過濾。匹配到用戶設定目標類和方法進行增強,過濾掉 sandbox 內部的類以及 jvm 認爲不可修改的類。

  6. 加載類檢索: 獲取到需要增強的類集合依賴檢索過濾器模塊

  7. HTTP 服務器:通過 http 協議與客戶端進行通信(sandbox.sh 即可理解爲客戶端)本質是通過 curl 命令下發指令到沙箱的 http 服務器。例如模塊的加載,激活,凍結,卸載等指令

相關技術

在分析之前先簡單介紹一些字節碼增強框架中會使用到的底層技術,本質上任何一個字節碼增強框架都是圍繞這些底層技術在上層進行建設。

JVM TI

JVM TI(JVM TOOL INTERFACE,JVM 工具接口)是 JVM 提供的一套對 JVM 進行操作的工具接口。通過 JVMTI 可以實現對 JVM 的多種操作,它通過接口註冊各種事件勾子,在 JVM 事件觸發的同時觸發預定義的勾子,以實現對各個 JVM 事件的響應,事件包括類文件加載、異常產生與捕獲、線程啓動和結束、進入和退出臨界區、成員變量修改、GC 開始和結束、方法調用進入和退出、臨界區競爭與等待、VM 啓動與退出等等。

當 JVM 加載類文件時會觸發類文件加載鉤子事件 ClassFileLoadHook,從而觸發 Instrumentation 類庫中的 ClassFileTransformer (字節碼轉換器)的 transform 方法,在 transfrom 方法中可以對字節碼進行轉換

Instrumentation

Instrumentation 是 JVM 提供的可以在運行時動態修改已加載類的基礎庫。java agent 是 JVM TI 接口的一種實現,Instrumentation 實例只能通過 agent 的 premain 或者 agentmian 方法的參數中獲取。

加載 agent 常見的方式有兩種

  1. 在啓動腳本中增加-javaagent 參數這種方式會伴隨着 JVM 一起啓動,agent 中需要提供大名鼎鼎的 premain 方法,顧名思義 premain 是在 main 方法運行前執行的,然後纔會去運行主程序的 main 方法,這樣就要求開發者在應用啓動前就必須確認代理的處理邏輯和參數內容等等。這種掛在 agent 方式的好處是如果 agent 啓動需要加載大量的類,隨着 jvm 啓動時直接加載不會導致 JVM 在運行時卡頓或者 CPU 抖動,缺點是不夠靈活。

  2. 利用 Attach API 在 JVM 運行時不需要重啓的情況下即可完成掛載,agent 需要提供 agentmain 方法,即插即用的模式非常靈活,Attach API 提供了一種附加到 Java 虛擬機的機制,使用此 API 附加到目標虛擬機並將其工具代理加載到該虛擬機中,本質上就是提供了和目標 jvm 通訊的能力。例如我們常常使用的 jastck,jmap 等命令都是利用 attach api 先與目標 jvm 建立通訊再執行命令。但如果 agent 啓動需要加載大量的類可能會導致目標 jvm 出現卡頓,cpu 抖動等情況

在 Instrumentation 接口中提供了多個 api 用來管理和操作字節碼。我們重點關注以下幾個即可:

  1. addTransformer:註冊字節碼轉換器,當註冊一個字節碼轉換器後,所有的類加載都會經過字節碼轉換器進行處理。

  2. retransformClasses 重新對 JVM 已加載的類進行字節碼轉換

  3. removeTransformer 刪除已註冊的字節碼轉換器,刪除後新加載的類不會再經過字節碼轉換器處理,但是已經“增強”過的類還是會繼續保留

ClassFileTransformer

ClassFileTransformer(字節碼轉換器)是一個接口,接口中只有 transform 一個方法。

byte[] transform(  ClassLoader         loader,
            String              className,
            Class<?>            classBeingRedefined,
            ProtectionDomain    protectionDomain,
            byte[]              classfileBuffer)
    throws IllegalClassFormatException;

在 transform 可以返回轉換後的字節碼 byte[],可以通過 Instrumentation#addTransformer 方法將實現的字節碼轉換器進行註冊,一旦註冊後字節碼轉換器就會在合適的時機被觸發。

  1. 新加載類的時候,例如 ClassLoader.defineClass

  2. 重新定義類的時候,例如 Instrumentation.redefineClasses

  3. 對類重新轉換的時候,例如 Instrumentation.retransformClasses

字節碼生成

在 ClassFileTransformer#transform 方法會返回轉換後的字節碼 byte[],那如何動態生成字節碼呢,這就要提到大名鼎鼎的 ASM 框架了。

ASM 是一個通用的 Java 字節碼操作和分析框架。它能夠以二進制形式修改已有的類或是動態生成類。很多利用字節碼增強技術的開源項目都是基於 ASM 進行構建的,如 CGLIB,Groovy,Kotlin 編譯器等等,ASM 提供了一些常見的字節碼轉換和分析算法,從中可以構建定製的複雜轉換和代碼分析工具;幾個核心的類:

  • ClassReader:此類主要功能就是讀取字節碼文件,然後把讀取的數據通知 ClassVisitor;

  • ClassVisitor:用於生成和轉換編譯類的 ASM API 基於 ClassVisitor 抽象類,接收 ClassReader 發出的對 method 的訪問請求,並且替換爲另一個自定義的 MethodVisitor

  • ClassWriter:其繼承於 ClassVisitor,主要用來生成類;

大體的執行流程是首先需要加載原 Class 文件,然後通過訪問者模式訪問所有元素,在訪問的過程中對各元素進行改造,最後重新生成一個字節碼的 byte[],如圖:

ClassLoader

沙箱中另一個特性類隔離是通過實現自定義的 classLoader 來完成的,ClassLoader(類加載器),在 java 中所有的類必須通過類加載器正確加載後才能運行,類加載器雖然只用於實現類的加載動作,但它在 Java 程序中起到的作用卻遠超類加載階段。

對於任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在 Java 虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個 Class 文件,被同一個 Java 虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

加載器種類

在 java8 中默認 jvm 會提供三個類加載器,分別是

  1. 啓動類加載器(Bootstrap Class Loader):這個類加載器負責加載存放在 <JAVA_HOME>\lib 目錄,或者被-Xbootclasspath 參數所指定的路徑中存放的,而且是 Java 虛擬機能夠 識別的(按照文件名識別,如 rt .jar、t ools.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類 庫加載到虛擬機的內存中

  2. 擴展類加載器(Extension Class Loader):這個類加載器是在類 sun.misc.Launcher$ExtClassLoader 中以 Java 代碼的形式實現的。它負責加載<JAVA_HOM E>\lib\ext 目錄中,或者被 java.ext.dirs 系統變量所 指定的路徑中所有的類庫。

  3. 應用程序類加載器(Application Class Loader):這個類加載器由 sun.misc.Launcher$App ClassLoader 來實現。由於應用程序類加載器是 ClassLoader 類中的 getSystem- ClassLoader()方法的返回值,所以有些場合中也稱它爲“系統類加載器”。它負責加載用戶類路徑 (ClassPath)上所有的類庫,開發者同樣可以直接在代碼中使用這個類加載器。如果應用程序中沒有 自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

  4. 自定義類加載器:顧名思義是由開發者自定義的類加載器

雙親委派模型

上面介紹了三個系統提供的類加載器,那麼他們之間以及和自定義類加載器是如何協作的呢?

各種類加載器的協作關係如圖,這種層次關係就是類加載器的“雙親委派模型”,除了頂層的啓動類加載器以外,其餘的類加載器都應有自己的父類加載器。

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去完成加載。

使用雙親委派模型來組織類加載器之間的關係,一個顯而易見的好處就是 Java 中的類隨着它的類加載器一起具備了一 種帶有優先級的層次關係。例如類 java. lang.Object,它存放在 rt . jar 之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此 Object 類 在程序的各種類加載器環境中都能夠保證是同一個類。反之,如果沒有使用雙親委派模型,都由各個 類加載器自行去加載的話,如果用戶自己也編寫了一個名爲 java.lang.Object 的類,並放在程序的 ClassPath 中,那系統中就會出現多個不同的 Object 類,Java 類型體系中最基礎的行爲也就無從保證,應用程序將會變得一片混亂 。

自定義類加載器

通常情況下自定義類加載器只需要繼承 URLClassLoader,重寫 findClass 方法即可,當然也可以直接繼承 ClassLoader,不過 ClassLoader 只能加載 classpath 下面的類,而 URLClassLoader 可以加載任意路徑下的類。

URLClassLoader 是 ClassLoader 的子類,它用於從指向 JAR 文件和目錄的 URL 的搜索路徑加載類和資源。也就是說通過 URLClassLoader 就可以加載指定 jar 中的 class 到內存中。

沙箱類隔離策略

在瞭解了 classloader 相關知識後,我們看一下 jvm-sandbox 提供的官方類隔離策略圖

在沙箱中自定義了 SandboxClassLoader 以及 ModuleJarClassLoader 來分別加載沙箱內部類和模塊中的類,sandbox agent 則是由 AppClassLoader 進行加載的,而 sandbox spy 間諜類是用 BootstrapClassLoader 進行加載,目的就是利用雙親委派加載模型,保證間諜類可以正確的被目標 JVM 加載,從而植入到目標 jvm 中完成與業務代碼的交互。

SPI

SPI 全稱 Service Provider Interface,是 Java 提供的一套用來被第三方實現或者擴展的 API,它可以用來啓用框架擴展和替換組件。Java SPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制,提供了通過 interface 尋找 implement 的方法。類似於 IOC 的思想,將裝配的控制權移到程序之外,從而實現解耦。

關於 SPI 的底層實現在這裏就不分析了,在沙箱中自定義模塊的實現則是利用了 SPI 機制,當我們自定義模塊除了要實現 Module interface,還需要按照 SPI 的約定在 resource/META-services 下以 com.alibaba.jvm.sandbox.api.Module 爲文件名,文件內容則是 Module 的實現類路徑。當使用 SPI 加載實現類時需要傳遞一個 classLoader,這個 classLoader 是 moduleClassLoader,moduleClassLoader 中是繼承自 URLClassLoader,SPI 加載類則查找當前 classLoader 對應的資源路徑,從而找到匹配 Module interface 路徑的文件,然後加載對應的實現類。

適應場景:調用者根據需要,使用、擴展或替換實現策略。

啓動過程

在 sandbox 啓動過程中涉及到 Agent 掛載,自定義 classloader 加載 sandbox 內部類,初始化 http 服務器,模塊的加載以及初始化。

掛載 Agent

JVM-SANDBOX 對兩種 agent 的掛載模式都支持。

  1. -javaagent 啓動腳本中直接掛載

  2. 利用 attach api 在運行時掛載

如果啓動時直接加載則直接在目標 JVM 的啓動腳本中增加-javaagent 指定 sandbox-agent.jar 即可。當執行./sandbox -p pid(目標 jvm 進程 id)是使用的 attach api 方式進行 agent 的掛載

在運行上述指令時執行的是 sandbox.sh 中 attach_jvm func,在 attach_jvm 中是通過 java -jar 啓動 sandbox-core,並指定了 sandbox-agent.jar 的路徑地址等參數。

function attach_jvm() {
  # attach target jvm
  "${SANDBOX_JAVA_HOME}/bin/java" \
    ${SANDBOX_JVM_OPS} \
    -jar "${SANDBOX_LIB_DIR}/sandbox-core.jar" \
    "${TARGET_JVM_PID}" \
    "${SANDBOX_LIB_DIR}/sandbox-agent.jar" \
    "home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}" ||
    exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."
}

在 sandbox-core 項目的 pom.xml 文件中指定了 core 程序的啓動類 CoreLauncher.java

<manifest>
    <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass>
</manifest>

在 CoreLauncher 中利用 Attach api 對目標 jvm 進行 jvm-sandbox agent 掛載。

private void attachAgent(final String targetJvmPid,
                         final String agentJarPath,
                         final String cfg) throws Exception {
        vmObj = VirtualMachine.attach(targetJvmPid);
        if (vmObj != null) {
            vmObj.loadAgent(agentJarPath, cfg);
        }
}

加載 Agent

在 agent 項目中只有兩個 java 文件:AgentLaucher.java、SandboxClassLoader.java

在 agent 項目的 pom.xml 中指定了程序的 Premain-Class 以及 Agent-Class,分別對應上面兩種 agent 的掛載方式,當使用-javaagent 掛載 agent 時則會加載 Premain-Class 的 premain 方法,當使用 Attach api 掛載 agent 時則會加載 Agent-Class 的 agentmain 方法

<manifestEntries>
    <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
    <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

在兩個 main 方法中接收的參數是相同的分別爲:String featureString 和 Instrumentation inst

  1. String featureString 是腳本中的執行腳本執行 attach 時傳遞過來的參數 如 sandbox 路徑地址,token,namspace(租戶)等

  2. Instrumentation 是 JVM 提供的可以在運行時動態修改已加載類的基礎庫,獲取 Instrumentation 實例只能通過 premain 或者 agentmian 方法參數中獲取。

//啓動加載 -javaagent
public static void premain(String featureString, Instrumentation inst) {
    LAUNCH_MODE = LAUNCH_MODE_AGENT;
    install(toFeatureMap(featureString), inst);
}
//動態加載 attach api
public static void agentmain(String featureString, Instrumentation inst) {
    LAUNCH_MODE = LAUNCH_MODE_ATTACH;
    final Map<String, String> featureMap = toFeatureMap(featureString); //解析慘參數
    writeAttachResult(
            getNamespace(featureMap), //獲取租戶所屬 namespace
            getToken(featureMap), //獲取 token
            install(featureMap, inst)
    );
}

無論是哪種方式掛載 agent 核心都在 install 方法中,install 中是開始加載 agent 的業務邏輯

初始化 Agent

在 install 方法中完成對 agent 的初始化,在初始化的過程中使用到了自定義的 SandboxClassLoader 對沙箱類進行加載,實現沙箱內部類與業務類隔離。

Spy 間諜類

在 install 中首先會利用 Instrumentation 實例將 sandbox-spy.jar 添加到 BootstrapClassLoader 的搜索範圍內。

// 將 Spy 注入到 BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
        getSandboxSpyJarPath(home)
        // SANDBOX_SPY_JAR_PATH
)));

爲什麼這麼做,在解釋前我們先了解下 Spy 間諜類是什麼。在沙箱的世界觀中,任何一個 Java 方法的調用都可以分解爲 BEFORE、RETURN 和 THROWS 三個環節,由此在三個環節上引申出對應環節的事件探測和流程控制機制。

// BEFORE
try {
   /*
    * do something...
    */
    // RETURN
    return;
} catch (Throwable cause) {
    // THROWS
}

而 Spy 間諜類就是實現了 before,return,throws 等鉤子函數。當將 Spy 間諜類買點到業務代碼中時,觸發類加載機制時利用雙親委派模型則可以層層向上查找,在 BootstrapClassLoader 中就可以將 Spy 間諜類正確加載,而在 Spy 的間諜類中內置了沙箱的 SpyHandler。這樣就完成了目標類和沙箱內核的通訊。本質的類增強策略如下圖:

在加載 agent 執行 install 方法中首先會將 Spy 間諜類追加到 BootstrapClassLoader 的搜索範圍內,這樣當對業務代碼增強時通過 classloader 雙親委派機制,Spy 間諜類一定會被正確加載,Spy 間諜類會將 before,return,throws 等鉤子函數順利的注入到業務代碼中。

SandBoxClassLoader

在將 Spy 間諜類追加到 BootstrapClassLoader 中後創建 SandBoxClassLoader,目的是使用自定義的 classLoader 可以儘量減少對目標 JVM 的侵入。

final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
        namespace,
        getSandboxCoreJarPath(home)
        // SANDBOX_CORE_JAR_PATH
);

在自定義的 SandboxClassLoader 中加載類破壞了雙親委派模型,首先讓自身加載,如果自身加載失敗後再向上委託加載。這樣做的目的是明確知道 SandboxClassLoader 只會加載沙箱 jar 文件的類,而這些 jia 文件路徑並不在目標的 JVM 的 ClasssLoader 可搜索的路徑上,所以向上委託加載無任何意義,破壞掉雙親委派模型,優先自身加載性能更好。

 @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        final Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }
        try {
            Class<?> aClass = findClass(name);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception e) {
            return super.loadClass(name, resolve);
        }
    }

啓動 HTTP 服務

在創建完 SandboxClassLoader 後則會利用 SandboxClassLoader 加載 core.jar 中的代理類(ProxyCoreServer),然後反射調用 ProxyCoreServer 的 bind 方法來初始化 HTTP 服務以及加載所有模塊。

@Override
public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {
    this.cfg = cfg;
    try {
        initializer.initProcess(new Initializer.Processor() {
            @Override
            public void process() throws Throwable {
                logger.info("initializing server. cfg={}", cfg);
                jvmSandbox = new JvmSandbox(cfg, inst);
                initHttpServer(); //初始化 http server
                initJettyContextHandler(); //初始化 jetty context 處理器
                httpServer.start();// 啓動 http server
            }
        });
        // 初始化加載所有的模塊
        try {
            jvmSandbox.getCoreModuleManager().reset();
        } catch (Throwable cause) {
            logger.warn("reset occur error when initializing.", cause);
        }
        ......
}

在 bind 中會初始化 HttpServer 並初始化上下文處理器,在初始化上下文處理器中將會綁定/module/http/*的 Servlet 爲 ModuleHttpServlet,這樣的話當我們對模塊進行操作的話都會先經過 ModuleHttpServlet 進行匹配然後分發到具體的模塊中執行命令。

private void initJettyContextHandler() {
     ......
    // module-http-servlet
    final String pathSpec = "/module/http/*";
    logger.info("initializing http-handler. path={}", contextPath + pathSpec);
    context.addServlet(
            new ServletHolder(new ModuleHttpServlet(cfg, jvmSandbox.getCoreModuleManager())),
            pathSpec
    );

    httpServer.setHandler(context);
}

小結

在 sandbox 啓動時首先需要利用 Attach API 或者 java -javaagent 掛在 agent。agent 需要提供對應的 agetmain 方法或者 premain 方法,在 agent 對應的 main 方法中進行初始化。

首先將 spy 間諜類追加到 BootstrapClassLoader 中爲後面代碼增強做準備,最終目的是讓目標 JVM 業務類可以和沙箱進行通訊,然後創建自定義的 SandboxClassLoader 用來加載沙箱內部類從而實現類隔離,最後啓動 HttpServer 並初始化對應的 Servlet,啓動完成後加載並初始化所有 module

模塊管理

目前在 sandbox 中有兩個地方會存儲模塊:

  1. $sandbox_home/module/沙箱系統模塊目錄,由配置項 system_module 進行定義。用於存放沙箱通用的管理模塊,比如用於沙箱模塊管理功能的 module-mgr 模塊,未來的模塊運行質量監控模塊、安全校驗模塊也都將存放在此處,跟隨沙箱的發佈而分發。系統模塊不受刷新(-f)、**強制刷新(-F)功能的影響,只有容器重置(-R)**能讓沙箱重新加載系統模塊目錄下的所有模塊。

  2. $sandbox_home/sandbox-module/沙箱用戶模塊目錄,由 sandbox.properties 的配置項 user_module 進行定義,默認爲${HOME}/.sandbox-module/。一般用於存放用戶自研的模塊。自研的模塊經常要面臨頻繁的版本升級工作,當需要進行模塊動態熱插拔替換的時候,可以通過**刷新(-f)或強制刷新(-F)**來完成重新加載。

模塊的生命週期

在沙箱中模塊一共有四種狀態

  1. 【加載模塊】被沙箱正確加載,沙箱將會允許模塊進行命令相應、代碼插樁等動作

  2. 【激活模塊】加載成功後默認是凍結狀態,需要代碼主動進行激活。模塊只有在激活狀態下才能監聽到沙箱事件

  3. 【凍結模塊】進入到凍結狀態之後,之前偵聽的所有沙箱事件都將被屏蔽。需要注意的是,凍結的模塊不會退回事件偵聽的代碼插樁,只有 delete()、wathcing()或者模塊被卸載的時候插樁代碼纔會被清理

  4. 【卸載沙箱】不會再看到該模塊,之前給該模塊分配的所有資源都將會被回收,包括模塊已經偵聽事件的類都將會被移除掉偵聽插樁,乾淨利落不留後遺症

模塊的狀態分別對應着模塊的生命週期,一個模塊在沙箱中的生命週期如下:

MODULE_LOAD(模塊加載),MODULE_UNLOAD(模塊卸載),MODULE_ACTIVE(模塊激活),MODULE_FROZE(模塊凍結),MODULE_LOAD_COMPLETED(模塊加載完成)

模塊加載

在啓動過程中初始化 http server 的最後一步是加載並初始化所有 module

 jvmSandbox.getCoreModuleManager().reset();

我們來看一下加載 module 都做了哪些事情,因爲這裏是初始化操作,所以在 reset 中首先強制卸載所有模塊,避免之前有已加載的模塊。然後遍歷 moduleLibDirArray 加載模塊,moduleLibDirArray 指的是模塊的存儲路徑,

在 reset 中會遍歷這兩個路徑對路徑下的模塊進行加載。

public synchronized CoreModuleManager reset() throws ModuleException {

    logger.info("resetting all loaded modules:{}", loadedModuleBOMap.keySet());

    // 1. 強制卸載所有模塊
    unloadAll();

    // 2. 加載所有模塊
    for (final File moduleLibDir : moduleLibDirArray) {
        // 用戶模塊加載目錄,加載用戶模塊目錄下的所有模塊
        // 對模塊訪問權限進行校驗
        if (moduleLibDir.exists() && moduleLibDir.canRead()) {
            new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
                    .load(
                            new InnerModuleJarLoadCallback(),
                            new InnerModuleLoadCallback()
                    );
        } else {
            logger.warn("module-lib not access, ignore flush load this lib. path={}", moduleLibDir);
        }
    }

    return this;
}

在上面經過方法層層調用會走到 ModuleJarLoader.java 的 load 方法中,這裏面接收的參數則是上面的 InnerModuleLoadCallback 對象,在這個方法中我們可以看到又創建了一個 ModuleJarClassLoader,通過字面意思理解這個 ClassLoader 主要是爲了加載模塊的類.

創建後並指定了當前加載模塊的線程使用 ModuleJarClassLoader,這樣的話後面真正加載模塊中的類時即可默認使用 ModuleJarClassLoader 了。

void load(final ModuleLoadCallback mCb) throws IOException {

    boolean hasModuleLoadedSuccessFlag = false;
    ModuleJarClassLoader moduleJarClassLoader = null;
    logger.info("prepare loading module-jar={};", moduleJarFile);
    try {
        moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);

        final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(moduleJarClassLoader);

        try {
            hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
        } finally {
            Thread.currentThread().setContextClassLoader(preTCL);
        }

ModuleJarClassLoader

ModuleJarClassLoader 繼承自一個叫 RoutingURLClassLoader,它的構造方法則是調用父類 RoutingURLClassLoader 的構造方法並傳遞了兩個參數,兩個參數都是待加載模塊 jar 文件。

public class ModuleJarClassLoader extends RoutingURLClassLoader {

    private ModuleJarClassLoader(final File moduleJarFile,
                                 final File tempModuleJarFile) throws IOException {
        super(
                new URL[]{new URL("file:" + tempModuleJarFile.getPath())},
                new Routing(
                        ModuleJarClassLoader.class.getClassLoader(),
                        "^com\\.alibaba\\.jvm\\.sandbox\\.api\\..*",
                        "^javax\\.servlet\\..*",
                        "^javax\\.annotation\\.Resource.*$"
                )
        );
    }

RoutingURLClassLoader

RoutingURLClassLoader 中我們重點關注下構造方法和 loadClass 方法,在構造方法中接收了 ModuleJarClassLoader 傳遞過來的要加載的模塊 jar 路徑以及 Routing 對象,Routing 對象中包含了 ModuleJarClassLoader 的 ClassLoader(SandboxClassLoader)以及一些正則匹配的類路徑。

通過 loadClass 方法的實現不難看出,當要加載的 className 正則匹配成功則直接委託 SandboxClassLoader 加載,這樣的好處是不需要每個模塊都加載一遍這些通用的類。當要加載的 className 正則匹配失敗,則用自身進行加載,如果加載不成功則向上繼續委託加載。

public class RoutingURLClassLoader extends URLClassLoader {

    private static final Logger logger = LoggerFactory.getLogger(RoutingURLClassLoader.class);
    private final ClassLoadingLock classLoadingLock = new ClassLoadingLock();
    private final Routing[] routingArray;

    public RoutingURLClassLoader(final URL[] urls,
                                 final Routing... routingArray) {
        super(urls);
        this.routingArray = routingArray;
    }
    
    @Override
    protected Class<?> loadClass(final String javaClassName, final boolean resolve) throws ClassNotFoundException {
      return classLoadingLock.loadingInLock(javaClassName, new ClassLoadingLock.ClassLoading() {
        @Override
        public Class<?> loadClass(String javaClassName) throws ClassNotFoundException {
            // 優先查詢類加載路由表,如果命中路由規則,則優先從路由表中的 ClassLoader 完成類加載
            if (ArrayUtils.isNotEmpty(routingArray)) {
                for (final Routing routing : routingArray) {
                    if (!routing.isHit(javaClassName)) {
                        continue;
                    }
                    final ClassLoader routingClassLoader = routing.classLoader;
                    try {
                        return routingClassLoader.loadClass(javaClassName);
                    } catch (Exception cause) {
                        // 如果在當前 routingClassLoader 中找不到應該優先加載的類(應該不可能,但不排除有就是故意命名成同名類)
                        // 此時應該忽略異常,繼續往下加載
                        // ignore...
                    }
                }
            }
            // 先走一次已加載類的緩存,如果沒有命中,則繼續往下加載
            final Class<?> loadedClass = findLoadedClass(javaClassName);
            if (loadedClass != null) {
                return loadedClass;
            }
            try {
                Class<?> aClass = findClass(javaClassName);
                if (resolve) {
                    resolveClass(aClass);
                }
                return aClass;
            } catch (Exception cause) {
                DelegateBizClassLoader delegateBizClassLoader = BusinessClassLoaderHolder.getBussinessClassLoader();
                try {
                    if(null != delegateBizClassLoader){
                        return delegateBizClassLoader.loadClass(javaClassName,resolve);
                    }
                } catch (Exception e) {
                    //忽略異常,繼續往下加載
                }
                return RoutingURLClassLoader.super.loadClass(javaClassName, resolve);
            }
        }
    });
}
}


在 ModuleJarLoader.java 的 load 方法中除了創建 ModuleJarClassLoader,還調用了 loadModules 方法,在方法中利用 ServiceLoader(SPI)加載 Module 接口的實現,然後遍歷檢查接口實現是否符合要求,例如是否有@Infomation 註解,模塊唯一 id 是否合法,模塊的啓動方式是否和沙箱的啓動方式一致。當這些前置檢查都通過後,調用 ModuleLoadCallback.load 進行模塊的加載。

private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
                               final ModuleLoadCallback mCb) {

    final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>();
    final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);
    final Iterator<Module> moduleIt = moduleServiceLoader.iterator();
    while (moduleIt.hasNext()) {

        final Module module;
        try {
            module = moduleIt.next();
        } catch (Throwable cause) {
            logger.warn("loading module instance failed: instance occur error, will be ignored. module-jar={}", moduleJarFile, cause);
            continue;
        }

        final Class<?> classOfModule = module.getClass();

        // 判斷模塊是否實現了@Information 標記
        if (!classOfModule.isAnnotationPresent(Information.class)) {
            logger.warn("loading module instance failed: not implements @Information, will be ignored. class={};module-jar={};",
                    classOfModule,
                    moduleJarFile
            );
            continue;
        }

        final Information info = classOfModule.getAnnotation(Information.class);
        final String uniqueId = info.id();

        // 判斷模塊 ID 是否合法
        if (StringUtils.isBlank(uniqueId)) {
            logger.warn("loading module instance failed: @Information.id is missing, will be ignored. class={};module-jar={};",
                    classOfModule,
                    moduleJarFile
            );
            continue;
        }

        // 判斷模塊要求的啓動模式和容器的啓動模式是否匹配
        if (!ArrayUtils.contains(info.mode(), mode)) {
            logger.warn("loading module instance failed: launch-mode is not match module required, will be ignored. module={};launch-mode={};required-mode={};class={};module-jar={};",
                    uniqueId,
                    mode,
                    StringUtils.join(info.mode(), ","),
                    classOfModule,
                    moduleJarFile
            );
            continue;
        }

        try {
            if (null != mCb) {
                mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);
            }
        }
        ......
    }

@Infomation

註解@Infomation 是沙箱內部定義的,主要是用來描述模塊的信息。在上面 loadingModules 方法中使用 SPI 加載完對應的模塊實現類,會查找對應的@Infomation 信息做一些前置檢查操作。

@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module 

onLoad(真正的加載)

在上面的 loadingModules 方法中根據 SPI 機制已經獲取到模塊的實現類了,在 loadingModules 最後會調用 ModuleLoadCallback 的 onLoad 方法,onLoad 調用 DefaultCoreModuleManager#load 方法在這裏則是真正加載模塊實現類裏面的內容。

加載的過程主要分爲如下幾步:

  1. 初始化模塊信息 CoreModule 在 coreModule 中有 module 的實現類,moduleJar,ModuleClassLoader,以及最重要的模塊類轉換器集合 sandboxClassFileTransformers,類轉換器則是字節碼增強的關鍵,代碼增強章節會詳細介紹

  2. 注入註解@Resource 資源,例如 ModuleEventWatcher 事件觀察者。關於 ModuleEventWatcher 代碼增強章節會詳細介紹

  3. 通知生命週期中模塊加載的對應實現(module 實現類可以同時實現 Module 接口以及沙箱模塊生命週期接口 ModuleLifecyle 接口),在 ModuleLifecyle 接口對應的方法中,用戶可以自定義實現業務邏輯。

  4. 激活模塊並通知生命週期中模塊激活的對應實現,只有被激活的模塊才能響應模塊的增強事件。

  5. 將模塊唯一 id 和當前的 coreModule 實例存入模塊列表,模塊列表是一個全局的 ConcurrentHashMap。還記得在 Httpserver 啓動時會初始化 ModuleHttpServlet,在 ModuleHttpServlet 接收請求時會從參數中解析出模塊 id,從而獲取到對應的 coreModule.

  6. 通知生命週期中模塊加載完成的對應實現

private synchronized void load(final String uniqueId,
                               final Module module,
                               final File moduleJarFile,
                               final ModuleJarClassLoader moduleClassLoader) throws ModuleException {
    ......
    // 初始化模塊信息
    final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);
    // 注入@Resource 資源
    injectResourceOnLoadIfNecessary(coreModule);
    // 通知生命週期,模塊加載
    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);
    // 設置爲已經加載
    coreModule.markLoaded(true);
    // 如果模塊標記了加載時自動激活,則需要在加載完成之後激活模塊
    markActiveOnLoadIfNecessary(coreModule);
    // 註冊到模塊列表中
    loadedModuleBOMap.put(uniqueId, coreModule);
    // 通知生命週期,模塊加載完成
    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);


}

小結

在模塊加載過程中,首先會通過遍歷存儲 module 的路徑,然後通過自定義的模塊 ClassLoader(ModuleJarClassLoader)以 SPI 的方式加載模塊 jar 文件中的 Module 接口實現類,加載成功後會對模塊進行前置檢查,檢查通過後則會對實現類注入 Resouce 資源並通知對應的生命週期事件,最後將 CoreModule 註冊到模塊列表中。

模塊激活

模塊在激活後就可以接收用戶的自定義指令了,自定義指令其實就是通過@Command 註解在模塊中自定義的 http 接口。在 http 接口對應的實現方法中可以做任何事情,例如觸發代碼增強。

sandbox.sh 的啓動腳本中可以看到通過使用-a 參數激活指定模塊,本質將會發送請求調用"sandbox-module-mgr/active",sandbox-module-mgr 是通用的沙箱系統管理模塊

# -a active module
[[ -n ${OP_MODULE_ACTIVE} ]] &&
  sandbox_curl_with_exit "sandbox-module-mgr/active" "&ids=${ARG_MODULE_ACTIVE}"

在 sandbox-mgr-module 項目的 ModuleMgrModule 類中,找到 active 方法的定義,因爲 sandbox-mgr-module 屬於系統模塊,在上面介紹的模塊加載中也會將系統模塊進行加載,所以當使用-a 參數激活模塊時也會通過 ModuleHttpServlet 將請求分發到 ModuleMgrModule 的 active 方法中進行處理。

在 active 中會解析參數,獲取到要激活的模塊 id,通過模塊 id 找到對應的 module 實例(還記得在模塊加載章節中介紹的當模塊加載完成後會將模塊的 coreModule 存儲到 loadedModuleBOMap 中吧,這裏的 search 本質就是從 map 中查找對應的 coreModule)

獲取 module 實現類中的@Information 註解 id,判斷是否已經激活,如果沒激活則進行激活

@Command("active")
public void active(final Map<String, String> param,
                   final PrintWriter writer) throws ModuleException {
    int total = 0;
    final String idsStringPattern = getParamWithDefault(param, "ids", EMPTY);
    for (final Module module : search(idsStringPattern)) {
        final Information info = module.getClass().getAnnotation(Information.class);
        final boolean isActivated = moduleManager.isActivated(info.id());
        if (!isActivated) {
            try {
                moduleManager.active(info.id());
                total++;
            } catch (ModuleException me) {
                logger.warn("active module[id={};] occur error={}.", me.getUniqueId(), me.getErrorCode(), me);
            }// try
        } else {
            total++;
        }
    }// for
    output(writer, "total %s module activated.", total);
}

小結

激活的本質就是將 coreModule 實例的 isActivated 標記設置爲 true,這樣模塊纔可以接收到代碼增強觸發的事件

模塊凍結

模塊凍結是將已加載的模塊的打上凍結標記,凍結後用戶自定義的增強代碼還存在,只不過沙箱不會處理用戶自定義的代碼增強邏輯。

在啓動腳本中,凍結指令也是發往 sandbox-module-mgr 沙箱系統管理模塊

# -A frozen module
[[ -n ${OP_MODULE_FROZEN} ]] &&
  sandbox_curl_with_exit "sandbox-module-mgr/frozen" "&ids=${ARG_MODULE_FROZEN}

小結

模塊凍結的處理過程和模塊激活的處理過程基本相同,但是多了一步凍結事件處理器。

事件處理器在代碼增強和事件處理章節中將會介紹。

模塊卸載

模塊卸載將會把模塊整個從沙箱中清理掉,之前給該模塊分配的所有資源都將會被回收,包括模塊已經偵聽事件的類都將會被移除掉偵聽插樁,乾淨利落不留後遺症

在啓動腳本中,凍結指令也是發往 sandbox-module-mgr 沙箱系統管理模塊

# -u unload module
[[ -n ${OP_MODULE_UNLOAD} ]] &&
  sandbox_curl_with_exit "sandbox-module-mgr/unload" "&action=unload&ids=${ARG_MODULE_UNLOAD}"

模塊卸載的流程

  1. 嘗試凍結模塊,讓事件處理器暫停不在執行用戶自定義的增強邏輯。

  2. 通知生命週期模塊卸載的對應實現

  3. 從模塊註冊表中刪除對應的模塊

  4. 標記卸載,isLoaded=true

  5. 釋放資源,在模塊加載流程中有一步是注入@Resource 資源,在注入 ModuleEventWatcher 資源時,實際上是構建了 ReleaseResource 對象,並實現了 release 方法。在 ModuleEventWatcher.delete 中會利用 Instrumentation 類庫刪除類增強轉換器 SandboxClassFileTransformer,這樣有新的類加載時將不會植入 spy 間諜類了。但是已加載的類總還是有間諜類的代碼。通過 Instrumentation 類庫重新渲染目標類字節碼。

@Override
public void delete(final int watcherId,
                   final Progress progress) {
    final Set<Matcher> waitingRemoveMatcherSet = new LinkedHashSet<Matcher>();
    // 找出待刪除的 SandboxClassFileTransformer
    final Iterator<SandboxClassFileTransformer> cftIt = coreModule.getSandboxClassFileTransformers().iterator();
    int cCnt = 0, mCnt = 0;
    while (cftIt.hasNext()) {
        final SandboxClassFileTransformer sandboxClassFileTransformer = cftIt.next();
        if (watcherId == sandboxClassFileTransformer.getWatchId()) {
            // 凍結所有關聯代碼增強
            EventListenerHandler.getSingleton()
                    .frozen(sandboxClassFileTransformer.getListenerId());
            // 在 JVM 中移除掉命中的 ClassFileTransformer
            inst.removeTransformer(sandboxClassFileTransformer);
            // 計數
            cCnt += sandboxClassFileTransformer.getAffectStatistic().cCnt();
            mCnt += sandboxClassFileTransformer.getAffectStatistic().mCnt();
            // 追加到待刪除過濾器集合
            waitingRemoveMatcherSet.add(sandboxClassFileTransformer.getMatcher());
            // 清除掉該 SandboxClassFileTransformer
            cftIt.remove();
        }
    }
    // 查找需要刪除後重新渲染的類集合
    final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(
            new GroupMatcher.Or(waitingRemoveMatcherSet.toArray(new Matcher[0]))
    );
    logger.info("watch={} in module={} found {} classes for delete.",
            watcherId,
            coreModule.getUniqueId(),
            waitingReTransformClasses.size()
    );
    beginProgress(progress, waitingReTransformClasses.size());
    try {
        // 應用 JVM
        reTransformClasses(watcherId, waitingReTransformClasses, progress);
    } finally {
        finishProgress(progress, cCnt, mCnt);
    }
}

  1. 關閉 ModuleJarClassLoader,可以將其加載的模塊類全部從 jvm 清理掉。在關閉前會利用 SPI 找到模塊中 ModuleJarUnloadSPI 接口的實現類,通知模塊已經被卸載了可以做一些自定義的業務處理。
public void closeIfPossible() {
    onJarUnLoadCompleted();
    try {

        // 如果是 JDK7+的版本, URLClassLoader 實現了 Closeable 接口,直接調用即可
        if (this instanceof Closeable) {
            logger.debug("JDK is 1.7+, use URLClassLoader[file={}].close()", moduleJarFile);
            try {
                ((Closeable)this).close();
            } catch (Throwable cause) {
                logger.warn("close ModuleJarClassLoader[file={}] failed. JDK7+", moduleJarFile, cause);
            }
            return;
        }
        .......
      }
 }

小結

模塊卸載中首先會暫停事件的處理,然後利用 Instrumentation 卸載增強的字節碼以及恢復原始字節碼,最後關閉 ModuleClassLoader

代碼增強

本章節將重點介紹如何利用 sandbox 對代碼進行增強以及背後的實現原理

自定義模塊

參考官方 demo 對代碼增強前我們首先自定義模塊 如下所示:

在自定義模塊中需要實現 Module 接口,聲明@Information 註解指定唯一 id,定義 ModuleEventWatcher 註解,聲明自定義@Command 指令的處理方法。

@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module {

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    @Command("repairCheckState")
    public void repairCheckState() {

        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.taobao.demo.Clock")
                .onBehavior("checkState")
                .onWatch(new AdviceListener() {

                    /**
                     * 攔截{@code com.taobao.demo.Clock#checkState()}方法,當這個方法拋出異常時將會被
                     * AdviceListener#afterThrowing()所攔截
                     */
                    @Override
                    protected void afterThrowing(Advice advice) throws Throwable {

                        // 在此,你可以通過 ProcessController 來改變原有方法的執行流程
                        // 這裏的代碼意義是:改變原方法拋出異常的行爲,變更爲立即返回;void 返回值用 null 表示
                        ProcessController.returnImmediately(null);
                    }
                });

    }

}

demo 中會利用 EventWatchBuilder 對 Clock 類中的 checkState 方法進行代碼增強,當 checkState 方法拋出異常後會執行 afterThrowing 方法中的代碼,將異常忽略掉直接返回。

watch

重點關注下 onWatch 方法,在經過層層調用會到達 DefaultModuleEventWatch 的 watch 方法,而 watch 方法則是真正觸發代碼增強的入口。

  1. 在 watch 方法中首先會創建 SandboxClassFileTransformer 沙箱類轉換器,並將其註冊到 CoreModule 中

  2. 利用 Instrumentation 類庫的 addTransformer api 註冊 SandboxClassFileTransformer 沙箱類轉換器實例註冊類轉換器後,後面所有的類加載都會經過 SandboxClassFileTransformer

  3. 查找需要渲染的類集合,利用 matcher 對象查找當前 jvm 已加載的類matcher 對象則是 EventWatchBuilder 中指定的目標類和目標方法的包裝對象,在 matcher 對象中指定了匹配規則。

  4. 將查找到的類進行渲染(代碼增強),利用 Instrumentation 的 retransformClasses 方法重新對 JVM 已加載的類進行字節碼轉換。

private int watch(final Matcher matcher,
                  final EventListener listener,
                  final Progress progress,
                  final Event.Type... eventType) {
    final int watchId = watchIdSequencer.next();
    // 給對應的模塊追加 ClassFileTransformer
    final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer(
            watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace);

    // 註冊到 CoreModule 中
    coreModule.getSandboxClassFileTransformers().add(sandClassFileTransformer);

    //這裏 addTransformer 後,接下來引起的類加載都會經過 sandClassFileTransformer
    inst.addTransformer(sandClassFileTransformer, true);

    // 查找需要渲染的類集合
    final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(matcher);
    logger.info("watch={} in module={} found {} classes for watch(ing).",
            watchId,
            coreModule.getUniqueId(),
            waitingReTransformClasses.size()
    );

    int cCnt = 0, mCnt = 0;

    // 進度通知啓動
    beginProgress(progress, waitingReTransformClasses.size());
    try {

        // 應用 JVM
        reTransformClasses(watchId,waitingReTransformClasses, progress);

        // 計數
        cCnt += sandClassFileTransformer.getAffectStatistic().cCnt();
        mCnt += sandClassFileTransformer.getAffectStatistic().mCnt();


        // 激活增強類
        if (coreModule.isActivated()) {
            final int listenerId = sandClassFileTransformer.getListenerId();
            EventListenerHandler.getSingleton()
                    .active(listenerId, listener, eventType);
        }

    } finally {
        finishProgress(progress, cCnt, mCnt);
    }

    return watchId;

字節碼轉換

在基礎知識章節中介紹了對字節碼轉換主要是需要實現 ClassFileTransformer 接口,然後將實現類註冊到 Instrumentation 中即可在合適的時機觸發字節碼轉換,也可以通過 Instrumentation 的 api 例如 retransformClasses 手動觸發。

在 SandboxClassFileTransformer#_transform 方法中主要就是當類加載或者重新定義時匹配是不是我們要增強的目標類和目標方法,如果是的話則利用沙箱的代碼增強框架進行字節碼生成,如果不是則忽略不處理。

private byte[] _transform(final ClassLoader loader,
                          final String internalClassName,
                          final Class<?> classBeingRedefined,
                          final byte[] srcByteCodeArray) {
    // 如果未開啓 unsafe 開關,是不允許增強來自 BootStrapClassLoader 的類
    if (!isEnableUnsafe
            && null == loader) {
        logger.debug("transform ignore {}, class from bootstrap but unsafe.enable=false.", internalClassName);
        return null;
    }

    final ClassStructure classStructure = getClassStructure(loader, classBeingRedefined, srcByteCodeArray);
    final MatchingResult matchingResult = new UnsupportedMatcher(loader, isEnableUnsafe).and(matcher).matching(classStructure);
    final Set<String> behaviorSignCodes = matchingResult.getBehaviorSignCodes();

    // 如果一個行爲都沒匹配上也不用繼續了
    if (!matchingResult.isMatched()) {
        logger.debug("transform ignore {}, no behaviors matched in loader={}", internalClassName, loader);
        return null;
    }

    // 開始進行類匹配
    try {
        final byte[] toByteCodeArray = new EventEnhancer().toByteCodeArray(
                loader,
                srcByteCodeArray,
                behaviorSignCodes,
                namespace,
                listenerId,
                eventTypeArray
        );
        if (srcByteCodeArray == toByteCodeArray) {
            logger.debug("transform ignore {}, nothing changed in loader={}", internalClassName, loader);
            return null;
        }

        // statistic affect
        affectStatistic.statisticAffect(loader, internalClassName, behaviorSignCodes);

        logger.info("transform {} finished, by module={} in loader={}", internalClassName, uniqueId, loader);
        return toByteCodeArray;
    } catch (Throwable cause) {
        logger.warn("transform {} failed, by module={} in loader={}", internalClassName, uniqueId, loader, cause);
        return null;
    }
}

在 EventEnhancer#toByteCodeArray 方法中利用 ASM 框架對代碼進行增強,通過 ASM 的 ClassReader 讀取目標類字節碼,然後通過 ClassWriter 將轉換後的字節碼寫入 dump 文件夾,並將轉換後的字節碼返回。

@Override
public byte[] toByteCodeArray(final ClassLoader targetClassLoader,
                              final byte[] byteCodeArray,
                              final Set<String> signCodes,
                              final String namespace,
                              final int listenerId,
                              final Event.Type[] eventTypeArray) {
    // 返回增強後字節碼
    final ClassReader cr = new ClassReader(byteCodeArray);
    final ClassWriter cw = createClassWriter(targetClassLoader, cr);
    final int targetClassLoaderObjectID = ObjectIDs.instance.identity(targetClassLoader);
    cr.accept(
            new EventWeaver(
                    ASM7, cw, namespace, listenerId,
                    targetClassLoaderObjectID,
                    cr.getClassName(),
                    signCodes,
                    eventTypeArray
            ),
            EXPAND_FRAMES
    );
    return dumpClassIfNecessary(cr.getClassName(), cw.toByteArray());
}

轉換過程則是通過繼承 ClassVisitor 抽象類實現方法事件織入者 EventWeaver,在 EventWeaver 中會對目標方法進行匹配,如果匹配成功則織入 Spy 間諜類中的埋點方法。具體代碼在 EventWeaver#visitMethod 方法中,內容太長就不粘貼了。

小結

在代碼增強流程中利用 Instrumentation 類庫的 addTransformer api 註冊 SandboxClassFileTransformer 沙箱類轉換器實例,當類加載、類重新定義、類轉換的時候會觸發 SandboxClassFileTransformer 的 transformer 方法進行字節碼轉換,本質是使用 ASM 框架在對目標方法和類進行轉換時將 SPY 間諜類中的方法織入到目標方法中,最後輸出增強後的字節碼 byte[]

事件處理

在代碼增強章節中介紹了是如何將間諜類織入到目標方法中的生成的字節碼會生成類重新應用到 jvm 中,那麼增強後的方法是如何執行到用戶自定義的增強邏輯中呢?

拿 spyMethodOnBefore 舉例,假如我們對目標方法監聽的是 Before 事件,因爲增強後的代碼在目標方法執行前會執行 spyMethodOnBefore 方法,在 spyMethodOnBefore 中會找到模塊對應的 SpyHandler 間諜處理器實例 EventListenHandler。

public static Ret spyMethodOnBefore(final Object[] argumentArray,
                                    final String namespace,
                                    final int listenerId,
                                    final int targetClassLoaderObjectID,
                                    final String javaClassName,
                                    final String javaMethodName,
                                    final String javaMethodDesc,
                                    final Object target) throws Throwable {
    final Thread thread = Thread.currentThread();
    if (selfCallBarrier.isEnter(thread)) {
        return Ret.RET_NONE;
    }
    final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);
    try {
        final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
        if (null == spyHandler) {
            return Ret.RET_NONE;
        }
        return spyHandler.handleOnBefore(
                listenerId, targetClassLoaderObjectID, argumentArray,
                javaClassName,
                javaMethodName,
                javaMethodDesc,
                target
        );
    } catch (Throwable cause) {
        handleException(cause);
        return Ret.RET_NONE;
    } finally {
        selfCallBarrier.exit(thread, node);
    }
}

EventListenHandler

EventListenHandler 是 SpyHandler 接口的實現類,在 handleOnBefore 中將會通過 listenerID 找到事件處理器 processor,在事件處理器中有用戶自定義的 listenr 實例,這樣就可以回調 listener 的 OnEvent 方法,執行真正用戶在自定義模塊中編寫的代碼了。

@Override
public Spy.Ret handleOnBefore(int listenerId, int targetClassLoaderObjectID, Object[] argumentArray, String javaClassName, String javaMethodName, String javaMethodDesc, Object target) throws Throwable {

    // 在守護區內產生的事件不需要響應
    if (SandboxProtector.instance.isInProtecting()) {
        logger.debug("listener={} is in protecting, ignore processing before-event", listenerId);
        return newInstanceForNone();
    }

    // 獲取事件處理器
    final EventProcessor processor = mappingOfEventProcessor.get(listenerId);

    // 如果尚未註冊,則直接返回,不做任何處理
    if (null == processor) {
        logger.debug("listener={} is not activated, ignore processing before-event.", listenerId);
        return newInstanceForNone();
    }

    // 獲取調用跟蹤信息
    final EventProcessor.Process process = processor.processRef.get();

    // 如果當前處理 ID 被忽略,則立即返回
    if (process.isIgnoreProcess()) {
        logger.debug("listener={} is marked ignore process!", listenerId);
        return newInstanceForNone();
    }

    // 調用 ID
    final int invokeId = invokeIdSequencer.getAndIncrement();
    process.pushInvokeId(invokeId);

    // 調用過程 ID
    final int processId = process.getProcessId();

    final ClassLoader javaClassLoader = ObjectIDs.instance.getObject(targetClassLoaderObjectID);
    //放置業務類加載器
    BusinessClassLoaderHolder.setBussinessClassLoader(javaClassLoader);
    final BeforeEvent event = process.getEventFactory().makeBeforeEvent(
            processId,
            invokeId,
            javaClassLoader,
            javaClassName,
            javaMethodName,
            javaMethodDesc,
            target,
            argumentArray
    );
    try {
        return handleEvent(listenerId, processId, invokeId, event, processor);
    } finally {
        process.getEventFactory().returnEvent(event);
    }
}

EventProcessor

EventProcessor 事件處理器中包含了用戶自定義的 listener 以及 listenerId,在執行增強代碼時會記錄調用的堆棧。

在代碼增強 watch 章節中,watch 方法執行的最後會激活增強類,在這裏就是主要是將 listenerId 當作 key,將 EventProcessor 當作 value 存入到 map 中。這樣當接收到事件時則可以根據 listenerId 查找到對應的事件處理器

public void active(final int listenerId,
                   final EventListener listener,
                   final Event.Type[] eventTypes) {
    mappingOfEventProcessor.put(listenerId, new EventProcessor(listenerId, listener, eventTypes));
    logger.info("activated listener[id={};target={};] event={}",
            listenerId,
            listener,
            join(eventTypes, ",")
    );
}

listenerId

listenerId 可以理解爲自定義模塊每對一個目標方法進行 watch 都會生成一個全局唯一的 id,通過這個 id 來綁定對應的事件處理器

listener

事件處理器最終的目的就是回調 listener 中的方法,listener 是我們在自定義模塊中對目標方法進行 watch 時所傳遞的參數,在 listener 的 onEvent 或者 AdviceListener 的各個埋點方法中可以任意實現要對目標方法增強的邏輯。

小結

在事件處理流程中通過 SpyHandler 的實現類 EventListenHandler 進行事件的分發,匹配到當前事件對應的事件處理器 EventProcessor,在 EventProcessor 中會真正的回調用戶實現的 listener,從而完成真正的用戶自定義代碼增強邏輯。

總結

JVM-SANDBOX 是一種 JVM 的非侵入式運行期 AOP 解決方案,通過它我們可以很輕鬆的開發出很多有趣的項目如錄製回放、故障模擬、動態日誌、行鏈路獲取等等,在本文中通過源碼分析介紹了 JVM-SANDBOX 實現細節,瞭解到它的核心實現。

回顧下關鍵內容:

  1. JVM-SANDBOX 基於 JVM TI(JVM 工具接口)實現 agent 對目標 jvm 進行掛載,利用 Instrumentation 類庫註冊自定義的類轉換器(SandboxClassFileTransformer),在類轉換器中使用 ASM 框架對目標類的方法織入 Spy 間諜類中的埋點方法,從而實現字節碼增強功能。

  2. JVM-SANDBOX 利用 ClassLoader 雙親委派模型,自定義 SandboxClassLoader 加載沙箱內部類(spy-core),使用自定義 ModulerJarClassLoader 和 SPI 機制對自定義模塊進行加載,從而實現類隔離。

  3. 可插拔以及多租戶的特性更多的是偏於 JVM-SANDBOX 內部的業務邏輯,在模塊管理章節模塊的加載和卸載中有詳細描述。

最後在解釋下爲什麼要對 jvm-sandbox 進行源碼分析,是因爲在工作中以及開源社區中都有使用到 jvm-sandbox 也發現了一些 bug 例如多次模塊加載和卸載會導致 metaspace oom等。後面有精力也會對 bug 以及解決過程進行分享。

作者介紹

Github 賬號:binbin0325,公衆號:檸檬汁CodeSentinel-Golang Committer 、ChaosBlade Committer 、 Nacos PMC 、Apache Dubbo-Go Committer。目前主要關注於混沌工程、中間件以及雲原生方向。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章