在日常業務程式碼開發中,我們經常接觸到AOP,比如熟知的Spring AOP。我們用它來做業務切面,比如登入校驗,紀錄檔記錄,效能監控,全域性過濾器等。但Spring AOP有一個侷限性,並不是所有的類都託管在 Spring 容器中,例如很多中介軟體程式碼、三方包程式碼,Java原生程式碼,都不能被Spring AOP代理到。如此一來,一旦你想要做的切面邏輯並不屬於Spring的管轄範圍,或者你想實現脫離Spring限制的切面功能,就無法實現了。
那對於Java後端應用,有沒有一種更為通用的AOP方式呢?答案是有的,Java自身提供了JVM TI,Instrumentation等功能,允許使用者以通過一系列API完成對JVM的複雜控制。自此衍生出了很多著名的框架,比如Btrace,Arthas等等,幫助開發者們實現更多更復雜的Java功能。
JVM Sandbox也是其中的一員。當然,不同框架的設計目的和使命是不一樣的,JVM-Sandbox的設計目的是實現一種在不重啟、不侵入目標JVM應用情況下的AOP解決方案。
是不是看到這裡還是不清楚我在講什麼?別急,我舉幾個典型的JVM-Sandbox應用場景:
可以看到,藉助JVM-Sandbox,你可以實現很多之前在業務程式碼中做不了的事,大大拓展了可操作的範圍。
本文圍繞JVM SandBox展開,主要介紹如下內容:
JVM Sandbox誕生的技術背景在引言中已經贅述完畢,下面是作者開發該框架的一些業務背景,以下描述參照自文章:
JVM SandBox 是阿里開源的一款 JVM 平臺非侵入式執行期 AOP 解決方案,本質上是一種 AOP 落地形式。那麼可能有同學會問:已有成熟的 Spring AOP 解決方案,阿里巴巴為什麼還要「重複造輪子」?這個問題要回到 JVM SandBox 誕生的背景中來回答。在 2016 年中,天貓雙十一催動了阿里巴巴內部大量業務系統的改動,恰逢徐冬晨(阿里巴巴測試開發專家)所在的團隊調整,測試資源保障嚴重不足,迫使他們必須考慮更精準、更便捷的老業務測試迴歸驗證方案。開發團隊面臨的是新接手的老系統,老的業務程式碼架構難以滿足可測性的要求,很多現有測試框架也無法應用到老的業務系統架構中,於是需要新的測試思路和測試框架。
為什麼不採用 Spring AOP 方案呢?Spring AOP 方案的痛點在於不是所有業務程式碼都託管在 Spring 容器中,而且更底層的中介軟體程式碼、三方包程式碼無法納入到迴歸測試範圍,更糟糕的是測試框架會引入自身所依賴的類庫,經常與業務程式碼的類庫產生衝突,因此,JVM SandBox 應運而生。
本章節不詳細講述JVM SandBox的所有架構設計,只講其中幾個最重要的特性。詳細的架構設計可以看原框架程式碼倉庫的Wiki。
很多框架通過破壞雙親委派(我更願意稱之為直系親屬委派)來實現類隔離,SandBox也不例外。它通過自定義的SandboxClassLoader破壞了雙親委派的約定,實現了幾個隔離特性:
JVM-SANDBOX屬於基於Instrumentation的動態編織類的AOP框架,通過精心構造了位元組碼增強邏輯,使得沙箱的模組能在不違反JDK約束情況下實現對目標應用方法的無侵入
執行時AOP攔截。
從上圖中,可以看到一個方法的整個執行週期都被程式碼「加強」了,能夠帶來的好處就是你在使用JVM SandBox只需要對於方法的事件進行處理。
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
在沙箱的世界觀中,任何一個Java方法的呼叫都可以分解為
BEFORE
、RETURN
和THROWS
三個環節,由此在三個環節上引申出對應環節的事件探測和流程控制機制。基於
BEFORE
、RETURN
和THROWS
三個環節事件分離,沙箱的模組可以完成很多類AOP的操作。
- 可以感知和改變方法呼叫的入參
- 可以感知和改變方法呼叫返回值和丟擲的異常
- 可以改變方法執行的流程
- 在方法體執行之前直接返回自定義結果物件,原有方法程式碼將不會被執行
- 在方法體返回之前重新構造新的結果物件,甚至可以改變為丟擲異常
- 在方法體丟擲異常之後重新丟擲新的異常,甚至可以改變為正常返回
一切都是事件驅動的,這一點你可能很迷糊,但是在下文的實戰環節中,可以幫助你理解。
我將實戰章節提前到這裡,目的是方便大家快速瞭解使用JVM SandBox開發是一件多麼舒服的事情(相比於自己使用位元組碼替換等工具)。
使用版本:JVM-Sandbox 1.2.0
官方原始碼:https://github.com/alibaba/jvm-sandbox
我們來實現一個小工具,在日常工作中,我們總會遇到一些巨大的Spring工程,裡面有茫茫多的Bean和業務程式碼,啟動一個工程可能需要5分鐘甚至更久,嚴重拖累開發效率。
我們嘗試使用JVM Sandbox來開發一個工具,對應用的Spring Bean啟動耗時進行一次統計。這樣能一目瞭然的發現工程啟動慢的主要原因,避免去盲人摸象的優化。
最終效果如圖:
圖中統計了一個應用從啟動開始到所有SpringBean的啟動耗時,按照從高到低排序,我由於是demo應用,Bean的耗時都偏低(也沒有太多業務Bean),但在實際應用中會有非常多幾秒甚至十幾秒才完成初始化的Bean,可以進行鍼對性優化。
在JVMSandBox中如何實現上面的工具?其實非常簡單。
先貼上思路的整體流程:
首先新建Maven工程,在Maven依賴中參照JVM SandBox,官方推薦獨立工程使用parent方式。
<parent>
<groupId>com.alibaba.jvm.sandbox</groupId>
<artifactId>sandbox-module-starter</artifactId>
<version>1.2.0</version>
</parent>
新建一個類作為一個JVM SandBox模組,如下圖:
使用@Infomation宣告mode為AGENT模式,一共有兩種模式Agent和Attach。
我們由於是監控JVM啟動資料,所以需要AGENT模式。
其次,繼承com.alibaba.jvm.sandbox.api.Module和com.alibaba.jvm.sandbox.api.ModuleLifecycle。
其中ModuleLifecycle包含了整個模組的生命週期回撥函數。
最常用的是loadCompleted,所以我們重寫loadCompleted類,在裡面開啟我們的監控類SpringBeanStartMonitor執行緒。
而SpringBeanStartMonitor的核心程式碼如下圖:
使用Sandbox的doClassFilter過濾出匹配的類,這裡我們是BeanFactory。
使用doMethodFilter過濾出要監聽的方法,這裡是initializeBean。
裡取initializeBean作為統計耗時的切入方法。具體為什麼選擇該方法,涉及到SpringBean的啟動生命週期,不在本文贅述範圍內。(本文作者:蠻三刀醬)
接著使用moduleEventWatcher.watch(springBeanFilter, springBeanInitListener, Event.Type.BEFORE, Event.Type.RETURN);
將我們的springBeanInitListener監聽器繫結到被觀測的方法上。這樣每次initializeBean被呼叫,都會走到我們的監聽器邏輯。
監聽器的主要邏輯如下:
程式碼有點長,不必細看,主要就是在原方法的BeforeEvent(進入前)和ReturnEvent(執行正常返回後)執行上述的切面邏輯,我這裡便是使用了一個MAP儲存每個Bean的初始化開始和結束時間,最終統計出初始化耗時。
最終,我們還需要一個方法來知道我們的原始Spring應用已經啟動完畢,這樣我們可以手動解除安裝我們的Sandbox模組,畢竟他已經完成了他的歷史使命,不需要再依附在主程序上。
我們通過一個簡陋的辦法,檢查http://127.0.0.1:8080/
是否會返回小於500的狀態碼,來判斷Spring容器是否已經啟動。當然如果你的Spring沒有使用Web框架,就不能用這個方法來判斷啟動完成,你也許可以通過Spring自己的生命週期勾點函數來實現,這裡我是偷了個懶。
整個SpringBean監聽模組的開發就完成了,你可以感受到,你的開發和日常業務開發幾乎沒有區別,這就是JVM Sandbox帶給你的最大好處。
上述原始碼放在了我的Github倉庫:
https://github.com/monitor4all/javaMonitor
整個JVM Sandbox的入門使用基本上講完了,上文提到了一些JVM技術名詞,可能小夥伴們聽過但不是特別瞭解。這裡簡單闡述幾個重要的概念,理清楚這幾個概念之間的關係,以便大家更好的理解JVM Sandbox底層的實現。
JVMTI(JVM Tool Interface)是 Java 虛擬機器器所提供的 native 程式設計介面,JVMTI可以用來開發並監控虛擬機器器,可以檢視JVM內部的狀態,並控制JVM應用程式的執行。可實現的功能包括但不限於:偵錯、監控、執行緒分析、覆蓋率分析工具等。
很多java監控、診斷工具都是基於這種形式來工作的。如果arthas、jinfo、brace等,雖然這些工具底層是JVM TI,但是它們還使用到了上層工具JavaAgent。
Javaagent是java命令的一個引數。引數 javaagent 可以用於指定一個 jar 包。
-agentlib:<libname>[=<選項>] 載入本機代理庫 <libname>, 例如 -agentlib:hprof
另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
按完整路徑名載入本機代理庫
-javaagent:<jarpath>[=<選項>]
載入 Java 程式語言代理, 請參閱 java.lang.instrument
在上面-javaagent
引數中提到了參閱java.lang.instrument
,這是在rt.jar
中定義的一個包,該包提供了一些工具幫助開發人員在 Java 程式執行時,動態修改系統中的 Class 型別。其中,使用該軟體包的一個關鍵元件就是 Javaagent。從名字上看,似乎是個 Java 代理之類的,而實際上,他的功能更像是一個Class 型別的轉換器,他可以在執行時接受重新外部請求,對Class型別進行修改。
Instrumentation的底層實現依賴於JVMTI。
JVM 會優先載入 帶 Instrumentation
簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。
Instrumentation支援的介面:
public interface Instrumentation {
//新增一個ClassFileTransformer
//之後類載入時都會經過這個ClassFileTransformer轉換
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
//將一些已經載入過的類重新拿出來經過註冊好的ClassFileTransformer轉換
//retransformation可以修改方法體,但是不能變更方法簽名、增加和刪除方法/類的成員屬性
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
//重新定義某個類
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
Instrumentation的侷限性:
更詳細的原理闡述可以看下文:
https://www.cnblogs.com/rickiyang/p/11368932.html
上面的實戰章節中已經提到了attach和agent兩者的區別,這裡再展開聊聊。
在Instrumentation中,Agent模式是通過-javaagent:<jarpath>[=<選項>]
從應用啟動時候就插樁,隨著應用一起啟動。它要求指定的類中必須要有premain()方法,並且對premain方法的簽名也有要求,簽名必須滿足以下兩種格式:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
一個java程式中-javaagent
引數的個數是沒有限制的,所以可以新增任意多個javaagent。所有的java agent會按照你定義的順序執行,例如:
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
上面介紹Agent模式的Instrumentation是在 JDK 1.5中提供的,在1.6中,提供了attach方式的Instrumentation,你需要的是agentmain方法,並且簽名如下:
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
這兩種方式各有不同用途,一般來說,Attach方式適合於動態的對程式碼進行功能修改,在排查問題的時候用的比較多。而Agent模式隨著應用啟動,所以經常用於提前實現一些增強功能,比如我上面實戰中的啟動觀測,應用防火牆,限流策略等等。
本文花了較短的篇幅重點介紹了JVM Sandbox的功能,實際用法,以及基礎原理。它通過封裝一些底層JVM控制的框架,使得對JVM層面的AOP開發變的異常簡單,就像作者自己所說「JVM-SANDBOX還能幫助你做很多很多,取決於你的腦洞有多大了。」
筆者在公司內部也通過它實現了很多小工具,比如上面的應用啟動資料觀測(公司內部是一個更為穩定複雜的版本,還監控了大量中介軟體的資料),幫助了很多部門同事,優化他們應用的啟動速度。所以如果對JVM感興趣,不妨大開腦洞,想一想JVM Sandbox還能在哪裡幫助到你的工作,給自己的工作添彩。
https://www.infoq.cn/article/tsy4lgjvsfweuxebw*gp
https://www.cnblogs.com/rickiyang/p/11368932.html
https://www.jianshu.com/p/eff047d4480a