AOP—JVM SandBox—底层原理解析
原文作者:陸晨
原文地址:JVM SandBox 的技術原理與應用分析
目錄
一、前言
二、JVM SandBox 簡介
2.1 AOP
2.2 JVM SandBox
三、JVM 核心技術
3.1 Java Agent
3.2 Attach
3.3 JVMTI
四、JVM SandBox 設計與實現
4.1 可插拔
4.2 無侵入
4.3 隔離
4.4 多租戶
五、JVM Sandbox 應用場景分析
5.1 故障模擬
5.2 動態黑名單
總結
參考文檔
一、前言
在開始之前,我們先來模擬一下以下的場景:
- 小李:“小明,你的接口沒有返回數據,麻煩幫忙看一下?”
- 小明:“我這邊的數據也是從別人的服務器中拿到的,但是我不確定是因為邏輯處理有問題導致沒有結果,還是因為我依賴的服務有問題而沒有返回結果,我需要確認一下。”
- 小明:“哎呀,線上沒有日志,我需要加個日志上個線。”
- 30 分鐘之后……
- 小明:“不好意思,日志加錯地方了……稍等……”
接來下隆重登場的就是本文的主角 JVM SandBox 了。基于 JVM SandBox,我們可以很容易地做到在不重新部署應用的情況下,給指定的某些類的某些方法加上日志功能。當然,動態加日志僅僅是 JVM SandBox 可以應用的一個小小的場景,JVM SandBox 的威力遠不在于此。那么,JVM SandBox 是什么?JVM SandBox 從哪里來?JVM SandBox 怎么用?本文在第二章會回答這幾個問題,如果你跟我一樣對 JVM SandBox 的底層實現原理感興趣,特別是 JVM 相關部分,那么第三章有相關的內容;如果你只想了解 JVM SandBox 自身具有哪些特性,以及 JVM SandBox 是如何設計實現的,那么可以跳過第三章,直接閱讀第四章;最后,在第五章會簡單地介紹其他兩個可以應用 JVM SandBox 的場景。
二、JVM SandBox 簡介
2.1 AOP
在介紹 JVM SandBox 之前,我們先來回顧一下 AOP 技術。AOP(面向切面編程,Aspect Oriented Programming)技術已被業界廣泛應用,其思想是面向業務處理過程的某個步驟或階段進行編程,這個步驟或階段被稱為切面,其目的是降低業務邏輯的各部分之間的耦合,常見的 AOP 實現基本原理有兩種:代理和行為注入。
1)代理模式
在代理模式下,我們會創建一個代理對象來代理原對象的行為,代理對象擁有原對象行為執行的控制權,在這種模式下,我們基于代理對象在原對象行為執行的前后插入代碼來實現 AOP。
圖 2-1 代理模式2)行為注入模式
在行為注入模式下,我們不會創建一個新的對象,而是修改原對象,在原對象行為的執行前后注入代碼來實現 AOP。
圖 2-2 行為注入模式2.2 JVM SandBox
JVM SandBox 是阿里開源的一款 基于JVM 平臺非侵入式運行期 AOP 解決方案,本質上是一種 AOP 落地形式。那么可能有同學會問:已有成熟的 Spring AOP 解決方案,阿里巴巴為什么還要“重復造輪子”?這個問題要回到 JVM SandBox 誕生的背景中來回答。在 2016 年中,天貓雙十一催動了阿里巴巴內部大量業務系統的改動,恰逢徐冬晨(阿里巴巴測試開發專家)所在的團隊調整,測試資源保障嚴重不足,迫使他們必須考慮更精準、更便捷的老業務測試回歸驗證方案。開發團隊面臨的是新接手的老系統,老的業務代碼架構難以滿足可測性的要求,很多現有測試框架也無法應用到老的業務系統架構中,于是需要新的測試思路和測試框架。
為什么不采用 Spring AOP 方案呢?Spring AOP 方案的痛點在于不是所有業務代碼都托管在 Spring 容器中,而且更底層的中間件代碼、三方包代碼無法納入到回歸測試范圍,更糟糕的是測試框架會引入自身所依賴的類庫,經常與業務代碼的類庫產生沖突,因此,JVM SandBox 應運而生。
JVM SandBox 本身是基于插件化的設計思想,允許以“模塊”的方式基于 JVM SandBox 提供的 AOP 能力開發新的功能。基于 JVM SandBox,我們不需要關心如何在 JVM 層實現 AOP 的技術細節,只需要通過 JVM SandBox 提供的編程結構告訴“沙箱”,我們希望對哪些類哪些方法進行 AOP,在切面點做什么即可,JVM SandBox 模塊功能編寫起來非常簡單。下面是一個示例模塊代碼:
@MetaInfServices(Module.class) @Information(id = "my-sandbox-module")//模塊名 public class MySandBoxModule implements Module { private Logger LOG = Logger.getLogger(MySandBoxModule.class.getName()); @Resource private ModuleEventWatcher moduleEventWatcher; @Command("addLog")//模塊命令名 public void addLog() { new EventWatchBuilder(moduleEventWatcher) .onClass("com.float.lu.DealGroupService")//想要對DealGroupService這個類進行切面 .onBehavior("loadDealGroup")//想要對上面類的loadDealGroup方法進行切面 .onWatch(new AdviceListener() { @Override protected void before(Advice advice) throws Throwable { LOG.info("方法名: " + advice.getBehavior().getName());//在方法執行前打印方法的名字 } }); } }如上面代碼所示,通過簡單常規的編碼即可實現對某個類的某個方法進行切面,不需要對底層技術有了解即可上手。上面的模塊被 JVM SandBox 加載和初始化之后便可以被使用了。比如,只需要告訴 JVM SandBox 我們要執行 my-sandbox-module 這個模塊的 addLog 這個方法,我們編寫的功能的調用就會被注入到目標地方。
JVM SandBox 使用起來非常很簡單,但是 JVM SandBox 背后所涉及到的底層技術原理、實現細節卻不簡單,比如 Java Agent、Attach、JVMTI、Instrument、Class 字節碼修改、ClassLoader、代碼鎖、事件驅動設計等等。如果要深究可能要究幾本書,但這不是本文的目的。本文僅僅概括性地介紹 JVM SandBox 實現涉及到的一些核心技術點,力求通過本文可以回答如 JVMTI 是什么?Instrument 是什么?Java Agent 是什么?它們之間有什么關系?他們和 JVM SandBox 又是什么關系等問題。
三、JVM 核心技術
3.1 Java Agent
JVM SandBox 容器的啟動依賴 Java Agent,Java Agent(Java 代理)是 JDK 1.5 之后引入的技術。開發一個 Java Agent 有兩種方式,一種是實現一個 premain 方法,但是這種方式實現的 Java Agent 只能在 JVM 啟動的時候被加載;另一種是實現一個 agentmain 方法,這種方式實現的 Java Agent 可以在 JVM 啟動之后被加載。當然,兩種實現方法各有利弊、各有適用場景,這里不再過多介紹,JVM SandBox Agent 對于這兩種方式都有實現,用戶可以自行選擇使用,因為在 JVM 層這兩種方式底層的實現原理大同小異,因此本文只選擇 agentmain 方式進行介紹,下文的脈絡也僅跟 agentmain 方式相關。下面先通過兩行代碼,來看看基于 agentmain 方式實現的 Java Agent 是如何被加載的:
VirtualMachine vmObj = VirtualMachine.attach(targetJvmPid);//targetJvmPid為目標JVM的進程ID vmObj.loadAgent(agentJarPath, cfg); // agentJarPath為agent jar包的路徑,cfg為傳遞給agent的參數在 Java Agent 被加載之后,JVM 會調用 Java Agent JAR 包中的 MANIFEST.MF 文件中的 Agent-Class 參數指定的類中的 agentmain 方法。下面兩節會對這兩行代碼的背后 JVM 實現技術進行探究。
3.2 Attach
1)Attach 工作機制
上面一節中第一行代碼的背后,有一個重要的 JVM 支撐機制——Attach,為什么說重要?比如大家最熟悉的 jstack 就是要依賴這個機制來工作,那么,Attach 機制是什么呢?我們先來看看 Attach 機制都做了什么事兒。首先,Attach 機制對外提供了一種進程間的通信能力,能讓一個進程傳遞命令給 JVM;其次,Attach 機制內置一些重要功能,可供外部進程調用。比如剛剛提到的 jstack,再比如上一節中提到的第二行代碼:vmObj.loadAgent(agentJarPath, cfg); 這行代碼實際上就是告訴 JVM 我們希望執行 load 命令,下面的代碼片段可以更直觀地看到 load 命令對應的行為是:JvmtiExport::load_agent_library,這行代碼的行為是對 agentJarPath 指定的 Java Agent 進行加載:
//來源:attachListener.cpp static AttachOperationFunctionInfo funcs[] = { { "agentProperties", get_agent_properties }, { "datadump", data_dump }, { "dumpheap", dump_heap }, { "load", JvmtiExport::load_agent_library }, { "properties", get_system_properties }, { "threaddump", thread_dump }, { "inspectheap", heap_inspection }, { "setflag", set_flag }, { "printflag", print_flag }, { "jcmd", jcmd }, { NULL, NULL } };那么,JVM Attach 機制是如何工作的呢?Attach 機制的核心組件是 Attach Listener,顧名思義,Attach Listener 是 JVM 內部的一個線程,這個線程的主要工作是監聽和接收客戶端進程通過 Attach 提供的通信機制發起的命令,如下圖所示:
圖 3-1 Attach Listener 工作機制Attach Listener 線程的主要工作是串流程,流程步驟包括:接收客戶端命令、解析命令、查找命令執行器、執行命令等等,下面附上相關代碼片段:
片段一:AttachListener::init(啟動 AttachListener 線程):
//來源:attachListener.cpp { MutexLocker mu(Threads_lock); // 啟動線程 JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry); // Check that thread and osthread were created if (listener_thread == NULL || listener_thread->osthread() == NULL) { vm_exit_during_initialization("java.lang.OutOfMemoryError", "unable to create new native thread"); } java_lang_Thread::set_thread(thread_oop(), listener_thread); java_lang_Thread::set_daemon(thread_oop()); listener_thread->set_threadObj(thread_oop()); Threads::add(listener_thread); Thread::start(listener_thread); }片段二:attach_listener_thread_entry(輪詢隊列):
//來源:attachListener.cpp static void attach_listener_thread_entry(JavaThread* thread, TRAPS) { os::set_priority(thread, NearMaxPriority); thread->record_stack_base_and_size(); if (AttachListener::pd_init() != 0) { return; } AttachListener::set_initialized(); for (;;) { AttachOperation* op = AttachListener::dequeue();// 展開 if (op == NULL) { return; // dequeue failed or shutdown }片段三:dequeue(讀取客戶端 socket 內容)
//來源:attachListener_bsd.cpp BsdAttachOperation* BsdAttachListener::dequeue() { for (;;) { int s; // wait for client to connect struct sockaddr addr; socklen_t len = sizeof(addr); RESTARTABLE(::accept(listener(), &addr, &len), s); if (s == -1) { return NULL; // log a warning? } // 省略…… // peer credential look okay so we read the request BsdAttachOperation* op = read_request(s); } }2)加載 Agent
回到上層,我們再看看 vmObj.loadAgent(agentJarPath, cfg);這行 Java 代碼代碼是如何工作的?其實,這行代碼背后主要做了一件事情:告訴 Attach 加載 instrument 庫,instrument 庫又是什么?instrument 庫是基于 JVMTI 編程接口編寫的一個 JVMTI Agent,其表現形式是一個動態鏈接庫,下面上兩個代碼片段:
//來源:HotSpotVirtualMachine.java //片段1 loadAgentLibrary("instrument", args); //片段2 InputStream in = execute("load",agentLibrary,isAbsolute ? "true" : "false",options);Attach 接收到命令之后執行 load_agent_library 方法,主要做兩件事情:1)加載 instrument 動態庫;2)找到 instrument 動態庫中實現的 Agent_OnAttach 方法并調用。Attach 的工作到這里就結束了,至于 Agent_OnAttach 這個方法做了什么事情,我們會在 JVMTI 部分進行介紹。下面先解釋 Attach 相關的另外一個問題,Attach Listener 并不是在 JVM 啟動的時候被啟動的,而是基于一種懶啟動策略實現。
3)Attach Listener 懶啟動
為方便理解下面引入代碼片段,這是從 JVM 啟動路徑上截取的兩片代碼:
//來源:thread.cpp // 片段1 os::signal_init(); if (!DisableAttachMechanism) { AttachListener::vm_start(); if (StartAttachListener || AttachListener::init_at_startup()) { AttachListener::init(); } } // 片段2 bool AttachListener::init_at_startup() { if (ReduceSignalUsage) { return true; } else { return false; } }復制代碼
?
DisableAttachMechanism 這個參數默認是關閉的,也就是說 JVM 默認情況下啟用 Attach 機制,但是 StartAttachListener 和 ReduceSignalUsage 這兩個參數默認都是關閉的,因此 Attach Listener 線程默認并不會被初始化。那么 Attach Listener 線程是在什么時候被初始化的呢?這就有必要了解一下 Signal Dispatcher 組件了,Signal Dispatcher 本質上也是 JVM 提供的一種進程間通信機制,只是這種機制是基于信號量來實現的。
?
我們先從 Signal Dispatcher 的服務端角度,來看看 Signal Dispatcher 是如何工作的,不知道大家有沒有注意到上面的 os::signal_init();這么一行代碼,其作用是初始化和啟動 Signal Dispatcher 線程,Signal Dispatcher 線程啟動之后就會進入等待信號狀態(os::signal_wait)。如下代碼片段所示,SIGBREAK 信號是 SIGQUIT 信號的別名,Signal Dispatcher 接收到這個信號之后會調用 AttachListener 的 is_init_trigger 的方法初始化和啟動 AttachListener 線程,同時會在 tmp 目錄下面創建/tmp/.attach_pid${pid}這樣的一個文件,代表進程號為 pid 的 JVM 已經初始化了 AttachListener 組件了。
?
片段一:os::signal_init();(啟動 Signal Dispatcher 線程)
?
//來源:os.cpp { MutexLocker mu(Threads_lock); JavaThread* signal_thread = new JavaThread(&signal_thread_entry);//展開 if (signal_thread == NULL || signal_thread->osthread() == NULL) { vm_exit_during_initialization("java.lang.OutOfMemoryError", "unable to create new native thread"); } java_lang_Thread::set_thread(thread_oop(), signal_thread); java_lang_Thread::set_priority(thread_oop(), NearMaxPriority); java_lang_Thread::set_daemon(thread_oop()); signal_thread->set_threadObj(thread_oop()); Threads::add(signal_thread); Thread::start(signal_thread); }復制代碼
?
片段二:signal_thread_entry(監聽信號)
?
//來源:os.cpp static void signal_thread_entry(JavaThread* thread, TRAPS) { os::set_priority(thread, NearMaxPriority); while (true) { int sig; { sig = os::signal_wait(); } switch (sig) { case SIGBREAK: { // Check if the signal is a trigger to start the Attach Listener - in that // case don't print stack traces. if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {//展開 continue; }復制代碼
?
片段三:is_init_trigger(啟動 AttachListener)
?
//來源:attachListener_bsd.cpp bool AttachListener::is_init_trigger() { char path[PATH_MAX + 1]; int ret; struct stat st; snprintf(path, PATH_MAX + 1, "%s/.attach_pid%d",os::get_temp_directory(), os::current_process_id()); RESTARTABLE(::stat(path, &st), ret); if (ret == 0) { if (st.st_uid == geteuid()) { init();//初始化Attach Listener return true; } } return false; }復制代碼
?
我們再從客戶端角度,來看看客戶端是如何通過 Signal Dispatcher 來啟動 AttachListener 線程的,這要又要回到 VirtualMachine.attach(pid)這行代碼,這行代碼的背后會執行具體 VirtualMachine 的初始化工作,我們拿 Linux 平臺下的 LinuxVirtualMachine 實現來看,下面是 LinuxVirtualMachine 初始化的核心代碼:
?
//來源:LinuxVirtualMachine.java //檢查目標JVM對否存在標識文件 path = findSocketFile(pid); if (path == null) { File f = createAttachFile(pid); try { mpid = getLinuxThreadsManager(pid); sendQuitToChildrenOf(mpid);復制代碼
?
上面提到目標 JVM 一旦啟動 attach 組件之后,會在/tmp 目錄下創建名為.java_pid${pid}的文件。因此,客戶端在每次初始化 LinuxVirtualMachine 對象的時候,會先查看目標 JVM 的這個文件是否存在,如果不存在則需要通過 SIGQUIT 信號來將 attach 組件拉起來。具體操作是進入 try 區域后,找到指定 pid 進程的父進程(Linux 平臺下線程是通過進程實現的),給父進程的所有子進程都發送一個 SIGQUIT 信號,而 Signal Dispatcher 組件恰好在監聽這個信號。
?
3.3 JVMTI
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機提供的,為 JVM 相關的工具提供的本地編程接口集合。JVMTI 是從 Java SE 5 開始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已經消失了。JVMTI 提供了一套“代理”程序機制,可以支持第三方工具程序以代理的方式連接和訪問 JVM,并利用 JVMTI 提供的豐富的編程接口,完成很多跟 JVM 相關的功能。JVMTI 的功能非常豐富,包括虛擬機中線程、內存/堆/棧,類/方法/變量,事件/定時器處理等等。使用 JVMTI 一個基本的方式就是設置回調函數,在某些事件發生的時候觸發并作出相應的動作,這些事件包括虛擬機初始化、開始運行、結束,類的加載,方法出入,線程始末等等。如果想對這些事件進行處理,需要首先為該事件寫一個函數,然后在 jvmtiEventCallbacks 這個結構中指定相應的函數指針。
上面提到的 Instrument 就是一個基于 JVMTI 接口的,以代理方式連接和訪問 JVM 的一個 Agent,Instrument 庫被加載之后 JVM 會調用其 Agent_OnAttach 方法,如下代碼片段:
//來源:InvocationAdapter.c //片段1:創建Instrument對象 success = createInstrumentationImpl(jni_env, agent); //片段2:監聽ClassFileLoadHook事件并設置回調函數為eventHandlerClassFileLoadHook callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook; jvmtierror = (*jvmtienv)->SetEventCallbacks(jvmtienv, &callbacks, sizeof(callbacks)); //片段3:調用java類的agentmain方法 success = startJavaAgent(agent, jni_env, agentClass, options, agent->mAgentmainCaller);Agent_OnAttach 方法被調用的時候主要做了幾件事情:1)創建 Instrument 對象,這個對象就是 Java Agent 中通過 agentmain 方法拿到的 Instrument 對象;2)通過 JVMTI 監聽 JVM 的 ClassFileLoadHook 事件并設置回調函數 eventHandlerClassFileLoadHook;3)調用 Java Agent 的 agentmain 方法,并將第 1)步創建的 Instrument 對象傳入。通過上面的內容可以知道,在 JVM 進行類加載的都會回調 eventHandlerClassFileLoadHook 方法,我們可以猜到 eventHandlerClassFileLoadHook 方法做的事情就是調用 Java Agent 內部傳入的 Instrument 的 ClassFileTransformer 的實現:
//來源Instrumentation.java void addTransformer(ClassFileTransformer transformer);通過 JVMTI 的事件回調機制,Instrument 可以捕捉到每個類的加載事件,從而調用用戶實現的 ClassFileTransformer 來對類進行轉換,那么已經被加載的類怎么辦呢?為解決這個問題,Instrument 提供了 retransformClasses 接口用于對已經加載的類進行轉換:
//來源Instrumentation.java void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;Instrument 底層的實現實際上也是調用 JVMTI 提供的 RetransformClasses 接口,RetransformClasses 實現對已經加載的類進行重新定義(redefine),而重新定義類也會觸發 ClassFileLoadHook 事件,Instrument 同樣會監聽到這個事件并對被加載的類進行處理。到這里,JVM SandBox 底層依賴 JVM 的核心機制已經介紹完了,下面通過一張時序圖將一個 JavaAgent 的加載過程涉及到的相關組件及行為串起來:
圖 3-2 Java Agent 加載流程四、JVM SandBox 設計與實現
4.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 #卸載沙箱JVM 沙箱可以被動態地掛載到某個正在運行的目標 JVM 進程之上(前提是目標 JVM 沒有禁止 attach 功能),沙箱工作完之后還可以被動態地從目標 JVM 進程卸載掉,沙箱被卸載之后,沙箱對對目標 JVM 進程產生的影響會隨即消失(這是沙箱的一個重要特性),沙箱工作示意圖如下:
圖 4-1 沙箱工作示意圖客戶端通過 Attach 將沙箱掛載到目標 JVM 進程上,沙箱的啟動實際上是依賴 Java Agent,上文已經介紹過,啟動之后沙箱會一直維護著 Instrument 對象引用,在沙箱中 Instrument 對象是一個非常重要的角色,它是沙箱訪問和操作 JVM 的唯一通道,后續修改字節碼和重定義類都要經過 Instrument。另外,沙箱啟動之后同時會啟動一個內部的 Jetty 服務器,這個服務器用于外部進程和沙箱進行通信,上面看到的./sandbox.sh -p 33342 -d ‘my-sandbox-module/addLog’ 這行代碼,實際上就是通過 HTTP 協議來告訴沙箱執行 my-sanbox-module 這個模塊的 addLog 這個功能的。
4.2 無侵入
沙箱內部定義了一個 Spy 類,該類被稱為“間諜類”,所有的沙箱模塊功能都會通過這個間諜類驅動執行。下面給出一張示意圖將業務代碼、間諜類和模塊代碼串起來來幫助理解:
圖 4-2 沙箱無侵入核心實現上圖是沙箱 AOP 核心實現的偽代碼,實際實現會比上圖更復雜一些,沙箱內部通過修改和重定義業務類來實現上述功能的。在接口設計方面,沙箱通過事件驅動的方式,讓模塊開發者可以監聽到方法執行的某個事件并設置回調邏輯,這一切都可以通過實現 AdviceListener 接口來做到,通過 AdviceListener 接口定義的行為,我們可以了解沙箱支持的監聽事件如下:
4.3 隔離
JVM 沙箱有自己的工作代碼類,而這些代碼類在沙箱被掛在到目標 JVM 之后,其涉及到的相關功能實現類都要被加載到目標 JVM 中,沙箱代碼和業務代碼共享 JVM 進程,這里有兩個問題:1)如何避免沙箱代碼和業務代碼之間產生沖突;2)如何避免不同沙箱模塊之間的代碼產生沖突。為解決這兩個問題,JVM SandBox 定義了自己的類加載器,嚴格控制類的加載,沙箱的核心類加載器有兩個:SandBoxClassLoader 和 ModuleJarClassLoader。SandBoxClassLoader 用于加載沙箱自身的工作類,ModuleJarClassLoader 用于加載三方自己開發的模塊功能類,如上面的 MySandBoxModule 類。在沙箱中類加載器繼承關系如下圖所示:
圖 4-3 沙箱類加載器繼承體系通過類加載器,沙箱將沙箱代碼和業務代碼以及不同沙箱模塊之間的代碼隔離開來。
4.4 多租戶
JVM 沙箱提供的隔離機制也有兩層含義,一層是沙箱容器和業務代碼之間隔離以及沙箱內部模塊之間隔離;另一層是不同用戶的沙箱之間的隔離,這一層隔離用來支持多租戶特性,也就是支持多個用戶對同一個 JVM 同時使用沙箱功能且他們之間互不影響。沙箱的這種機制是通過支持創建多個 SandBoxClassLoader 的方式來實現的,每個 SandBoxClassLoader 關聯唯一一個命名空間(namespace)用于標識不同的用戶,示意圖如下所示:
圖 4-4 多租戶實現示意圖五、JVM Sandbox 應用場景分析
JVM SandBox 讓動態無侵入地對業務代碼進行 AOP 這個事情實現起來非常容易,但是這個事情做起來非常容易只是前提條件,更重要的是我們基于 JVM SandBox 能做什么?可以做的很多,比如:故障模擬、動態黑名單,動態日志、動態開關、系統流控、熱修復,方法請求錄制和結果回放、動態去依賴、依賴超時時間動態修改、甚至是修改 JDK 基礎類的功能等等,當然不限于此,這里大家可以打開腦洞,天馬行空地思考一下,下面再給出兩個 JVM SandBox 應用場景的實現思路。
5.1 故障模擬
我們可以開發一個沙箱模塊,通過和前臺頁面的交互,我們可以對任意業務類的任意方法注入故障來達到故障模擬的效果,用戶交互示意圖如下:
圖 5-1 故障模擬交互示意圖用戶通過簡單的界面操作即可完成故障注入,應用代碼不需要提前埋點。
5.2 動態黑名單
我們還可以開發一個沙箱模塊實現 IP 黑名單功能,針對指定 IP 的客戶端,服務直接返回空結果,用戶交互示意圖如下:
圖 5-2 動態黑名單交互示意圖引用 JVM SandBox 官網的一句話:“JVM-SANDBOX 還能幫助你做很多很多,取決于你的腦洞有多大了。”
總結
JVM SandBox 是一種無侵入、可動態插拔、JVM 層的 AOP 解決方案,基于 JVM SandBox 我們可以很容易地開發出很多有意思的工具,這完全歸功于 JVM SandBox 為我們屏蔽了底層技術細節和實現復雜性。JVM SandBox 很強大,這里需要感謝 JVM SandBox 的作者。除了無侵入、可動態插拔這兩個優勢之外,JVM SandBox 在 JVM 層支持 AOP 這件事情本身就是一個絕對優勢,因為我們開發的 AOP 能力不再依賴應用層所使用的容器,比如不管你使用的是 Spring 容器還是 Plexus 容器,不管你的 Web 容器是 Tomcat 還是 Jetty,統統都沒有關系。
回顧一下本文的內容:
-
回顧 AOP 技術;
-
介紹 JVM SandBox 是什么、來自哪里、怎么用;
-
通過 Java Agent 的加載介紹涉及到的 JVM 相關核心技術如:Attach 機制、JVMTI、Instrument 等;
-
介紹 JVM SandBox 的核心特性的設計與實現如:可插拔、無侵入、隔離、多租戶;
-
介紹 JVM SandBox 可被應用的場景以及兩個小例子。
參考文檔
【1】http://developer.51cto.com/art/201803/568224.htm
【2】https://github.com/alibaba/jvm-sandbox
【3】https://www.jianshu.com/p/b72f66da679f
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的AOP—JVM SandBox—底层原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: K8s—使用教程
- 下一篇: AOP—JVM SandBox—快速上手