引子
相信大家都遇到過這種場景,線上出故障了,但是關鍵代碼裏面忘記打日誌了,導致無法復現和準確定位問題。這時候可能需要重寫加上日誌,部署到服務器,但這第一耗時間,第二可能破壞現場,比如可能是線程池的問題呢?所以如果可以不重啓服務器,就可以給代碼加上日誌,是多麼棒的一件事呀。那能不能實現,of course。
如何手動實現
當然市面上有很多工具可以實現熱部署,比如btrace,jvm-sandbox等。那如果我們想要自己實現如下圖的結果,思路是什麼?
我們知道Java對象的行爲(函數,方法)是存儲在方法區的,從下圖可以看到,方法區的數據是由類加載器把編譯好的class文件加載到jvm方法區的。所以我們可以得出簡單思路是:
1. 在對應類Java代碼中新增日誌代碼,並重新編譯得到新的class文件。
2. 讓jvm重新加載這個類的class文件到方法區
第一步倒是挺好實現,但是第二步,如何讓jvm加載一個已經加載過的類?
答案是“java.lang.instrument.Instrumentation”
那Instrumentation是做什麼的呢,翻譯了一下類的API註釋
Instrumentation類提供控制Java語言程序代碼的服務。Instrumentation可以實現在方法插入額外的字節碼從而達到收集使用中的數據到指定工具的目的。由於插入的字節碼是附加的,這些更變不會修改原來程序的狀態或者行爲。通過這種方式實現的良性工具包括監控代理、分析器、覆蓋分析程序和事件日誌記錄程序等等。
Instrumentation中有兩個方法都可以實現重新替換已經存在的class文件,它們是:redefineClasses 和 retransformClasses。區別是redefineClasses 是自己提供字節碼文件替換 掉已存在的 class 文件,retransformClasses 是在已存在的字節碼文件上修改後再替換之。
講到這裏,簡單的實現思路已浮現在眼前,先得到編譯好的class文件,然後調用redefineClasses進行替換。或者是使用ASM等可以直接修改字節碼的工具,然後調用retransformClasses進行替換。
實現案例:自己實現字節碼增強
Btrace
簡介
BTrace 是基於 Java 語言的一個安全的、可提供動態追蹤服務的工具。BTrace基於 ASM、Java Attach Api、Instruments,JavaCompile 開發,爲用戶提供了很多註解。依靠這 些註解,我們可以編寫 BTrace 腳本(簡單的 Java 代碼)達到我們想要的效果。
Btrace實現
-
使用技術
- JavaCompile
- Instrumentation
- ASM
- Javaagent
- attach api -
原理
Btrace主要包括jar包有:btrace-agent.jar、btrace-client.jar和btrace-boot.jar。
btrace-agent.jar主要功能是在目標jvm中植入BtraceAgent的實現,主要是使用instrumentation和asm字節碼處理技術,同時啓用一個server socket與客戶端進行交互,實現監控文件的發送和結果的返回。
btrace-client.jar在本地啓動一個jvm,內部創建socket與BtraceAgent連接,進行字節碼文件數據通訊,還有其他event事件等。同時,BtraceAgent的服務端將數據回傳給BtraceClient進行打印。 -
BTrace工作序列圖
小結: 其實BTrace就是使用了java attach api附加agent.jar,然後使用腳本解析引擎+asm來重寫指定類的字節碼,再使用instrument實現對原有類的替換。
使用案例:BTrace實現字節碼增強
jvm-sanbox
簡介
JVM SandBox 是阿里開源的一款 JVM 平臺非侵入式運行期 AOP 解決方案。可用於故障定位、方法請求錄製和結果回放、動態日誌打印等場景。
特性
可插拔:沙箱以及沙箱的模塊可以隨時加載和卸載,不會在目標應用留下痕跡
無侵入:目標應用無需重啓也無需感知沙箱的存在
類隔離:沙箱以及沙箱的模塊不會和目標應用的類相互干擾
多租戶:目標應用可以同時掛載不同租戶下的沙箱並獨立控制
高兼容:支持JDK[6,11]
核心原理
在沙箱的世界觀中,任何一個Java方法的調用都可以分解爲BEFORE、RETURN和THROWS三個環節,由此在三個環節上引申出對應環節的事件探測和流程控制機制。
// BEFORE
try {
/*
* do something...
*/
// RETURN
return;
} catch (Throwable cause) {
// THROWS
}
基於BEFORE、RETURN和THROWS三個環節事件分離,沙箱的模塊可以完成很多類AOP的操作。
- 可以感知和改變方法調用的入參
- 可以感知和改變方法調用返回值和拋出的異常
- 可以改變方法執行的流程
- 在方法體執行之前直接返回自定義結果對象,原有方法代碼將不會被執行
- 在方法體返回之前重新構造新的結果對象,甚至可以改變爲拋出異常
- 在方法體拋出異常之後重新拋出新的異常,甚至可以改變爲正常返回
特性實現
1.可拔插
JVM SandBox 可插拔至少有兩層含義:一層是 JVM 沙箱本身是可以被插拔的,可被動態地掛載到指定 JVM 進程上和可以被動態地卸載;另一層是 JVM 沙箱內部的模塊是可以被插拔的,在沙箱啓動期間,被加載的模塊可以被動態地啓用和卸載。
沙箱可拔插:
一個典型的沙箱使用流程如下:
/sandbox.sh -p 33342 #將沙箱掛載到進程號爲 33342 的 JVM 進程上
/sandbox.sh -p 33342 -d 'my-sandbox-module/addLog' #運行指定模塊, 模塊功能生效
/sandbox.sh -p 33342 -S #卸載沙箱
模塊可拔插:
模塊生命週期類型有模塊加載、模塊卸載、模塊激活、模塊凍結、模塊加載完成五個狀態。模塊可以通過實現com.alibaba.jvm.sandbox.api.ModuleLifecycle接口,對模塊生命週期進行控制。
2.無侵入
沙箱內部定義了一個 Spy 類,該類被稱爲“間諜類”,通過Spy類來實現修改和重定義業務類來實現對目標代碼的增強。
private static void run(){
System.out.println( "working..." );
}
private void run(){
try {
//開始產生before事件
Spy.spyMethodOnBefore(new Object[]{},
"namespace",
100,
1334,
"com.bj58.sandbox.Base",
"run",
"",
this);
System.out.println( "working..." );
//執行後(省略)
} catch( Throwable throwable ){
// 異常省略
}
}
沙箱通過事件驅動的方式,讓模塊開發者可以監聽到方法執行的某個事件並設置回調邏輯,這一切都可以通過實現AdviceListener 接口來做到,通過 AdviceListener 接口定義的行爲,我們可以瞭解沙箱支持的監聽事件如下:
3.類隔離:
JVM 沙箱有自己的工作代碼類,而這些代碼類在沙箱被掛在到目標 JVM 之後,其涉及到的相關功能實現類都要被加載到目標 JVM 中,沙箱代碼和業務代碼共享 JVM 進程,這裏有兩個問題:1)如何避免沙箱代碼和業務代碼之間產生衝突;2)如何避免不同沙箱模塊之間的代碼產生衝突。爲解決這兩個問題,JVM SandBox 定義了自己的類加載器,嚴格控制類的加載,沙箱的核心類加載器有兩個:SandBoxClassLoader 和 ModuleJarClassLoader。SandBoxClassLoader 用於加載沙箱自身的工作類,ModuleJarClassLoader 用於加載三方自己開發的模塊功能類。結構如下圖所示:
SandBox與Tomcat同級加載,通過自定義的SandboxClassLoader破壞了雙親委派的約定,實現了和觀察應用的類隔離。所以不用擔心加載沙箱會引起應用的類污染、衝突。
破壞後的雙親委派機制變成了什麼?要掛載一個類,它會先看我當前的ClassLoader是不是加載了,如果沒加載,它會讓當前的ClassLoader嘗試着去加載,也就是它不再向它的父類去詢問,除非它無法加載的時候,它纔會去問它的父ClassLoader說,你是不是已經加載了,如果父ClassLoader也沒有加載的話,它會讓父ClassLoader嘗試着去加載。這樣就完成了我的目標應用之間與Sandbox之間的隔離
4.多租戶
sandbox支持多個用戶對同一個 JVM 同時使用沙箱功能且他們之間互不影響。沙箱的這種機制是通過支持創建多個 SandBoxClassLoader 的方式來實現的,每個 SandBoxClassLoader 關聯唯一一個命名空間(namespace)用於標識不同的用戶,示意圖如下所示:
使用案例:jvm-sandbox實現字節碼增強
Btrace 和 JVM-sandbox比較
Btrace
優點:
- 無需修改原程序任何代碼,無需重啓直接使用
- 功能多,不僅能監控,還能直接修改線上應用代碼,十分方便。
缺點:
- 退出後注入的字節碼會一直保留,直到重啓
- 不當使用可能會影響應用,甚至導致jvm崩潰
- 爲了避免對正常應用造成影響,有非常多的限制
限制如下:
BTrace class不能新建類, 新建數組, 拋異常, 捕獲異常,
不能調用實例方法以及靜態方法(com.sun.btrace.BTraceUtils除外)
不能將目標程序和對象賦值給BTrace的實例和靜態field
不能定義外部, 內部, 匿名, 本地類
不能有同步塊和方法
不能有循環
不能實現接口, 不能擴展類
不能使用assert語句, 不能使用class字面值
JVM-sandbox
優點:
- 入門的門檻較低
- 使用通俗易懂的編碼方式動態替換Class。
- 沙箱和模塊可插拔
缺點:
部分開發文檔還未完善,但是項目開源並且註釋都是中文,所以這一點問題不是很大。