如何把Java代碼玩出花?JVM Sandbox入門教程與原理淺談

来源:https://www.cnblogs.com/rude3knife/archive/2022/11/14/jvm-sandbox-for-noob.html
-Advertisement-
Play Games

本文花了較短的篇幅重點介紹了JVM Sandbox的功能,實際用法,以及基礎原理。它通過封裝一些底層JVM控制的框架,使得對JVM層面的AOP開發變的異常簡單,就像作者自己所說“JVM-SANDBOX還能幫助你做很多很多,取決於你的腦洞有多大了。” ...


在日常業務代碼開發中,我們經常接觸到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,可以直接在不修改代碼的情況下,直接抓取介面的出入參。
  • 安全漏洞熱修複:假設某個三方包(例如出名的fastjson)又出現了漏洞,集團內那麼多應用,一個個發佈新版本修複,漏洞已經造成了大量破壞。通過JVM-Sandbox,直接修改替換有漏洞的代碼,及時止損。
  • 介面故障模擬:想要模擬某個介面超時5s後返回false的情況,JVM-Sandbox很輕鬆就能實現。
  • 故障定位:像Arthas類似的功能。
  • 介面限流:動態對指定的介面做限流。
  • 日誌列印
  • ...

可以看到,藉助JVM-Sandbox,你可以實現很多之前在業務代碼中做不了的事,大大拓展了可操作的範圍。

本文圍繞JVM SandBox展開,主要介紹如下內容:

  • JVM SandBox誕生背景
  • JVM SandBox架構設計
  • 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整體架構

本章節不詳細講述JVM SandBox的所有架構設計,只講其中幾個最重要的特性。詳細的架構設計可以看原框架代碼倉庫的Wiki。

類隔離

很多框架通過破壞雙親委派(我更願意稱之為直系親屬委派)來實現類隔離,SandBox也不例外。它通過自定義的SandboxClassLoader破壞了雙親委派的約定,實現了幾個隔離特性:

  • 和目標應用的類隔離:不用擔心載入沙箱會引起原應用的類污染、衝突。
  • 模塊之間類隔離:做到模塊與模塊之間、模塊和沙箱之間、模塊和應用之間互不幹擾。

無侵入AOP與事件驅動

JVM-SANDBOX屬於基於Instrumentation的動態編織類的AOP框架,通過精心構造了位元組碼增強邏輯,使得沙箱的模塊能在不違反JDK約束情況下實現對目標應用方法的無侵入運行時AOP攔截

從上圖中,可以看到一個方法的整個執行周期都被代碼“加強”了,能夠帶來的好處就是你在使用JVM SandBox只需要對於方法的事件進行處理。

// BEFORE
try {

   /*
    * do something...
    */

    // RETURN
    return;

} catch (Throwable cause) {
    // THROWS
}

在沙箱的世界觀中,任何一個Java方法的調用都可以分解為BEFORERETURNTHROWS三個環節,由此在三個環節上引申出對應環節的事件探測和流程式控制制機制。

基於BEFORERETURNTHROWS三個環節事件分離,沙箱的模塊可以完成很多類AOP的操作。

  1. 可以感知和改變方法調用的入參
  2. 可以感知和改變方法調用返回值和拋出的異常
  3. 可以改變方法執行的流程
    • 在方法體執行之前直接返回自定義結果對象,原有方法代碼將不會被執行
    • 在方法體返回之前重新構造新的結果對象,甚至可以改變為拋出異常
    • 在方法體拋出異常之後重新拋出新的異常,甚至可以改變為正常返回

一切都是事件驅動的,這一點你可能很迷糊,但是在下文的實戰環節中,可以幫助你理解。

JVM Sandbox代碼實戰

我將實戰章節提前到這裡,目的是方便大家快速瞭解使用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。

  • Agent:隨著JVM啟動一起啟動
  • Attach:在已經運行的JVM進程中,動態的插入

我們由於是監控JVM啟動數據,所以需要AGENT模式。

其次,繼承com.alibaba.jvm.sandbox.api.Module和com.alibaba.jvm.sandbox.api.ModuleLifecycle。

其中ModuleLifecycle包含了整個模塊的生命周期回調函數。

  • onLoad:模塊載入,模塊開始載入之前調用!模塊載入是模塊生命周期的開始,在模塊生命中期中有且只會調用一次。 這裡拋出異常將會是阻止模塊被載入的唯一方式,如果模塊判定載入失敗,將會釋放掉所有預申請的資源,模塊也不會被沙箱所感知
  • onUnload:模塊卸載,模塊開始卸載之前調用!模塊卸載是模塊生命周期的結束,在模塊生命中期中有且只會調用一次。 這裡拋出異常將會是阻止模塊被卸載的唯一方式,如果模塊判定卸載失敗,將不會造成任何資源的提前關閉與釋放,模塊將能繼續正常工作
  • onActive:模塊被激活後,模塊所增強的類將會被激活,所有com.alibaba.jvm.sandbox.api.listener.EventListener將開始收到對應的事件
  • onFrozen:模塊被凍結後,模塊所持有的所有com.alibaba.jvm.sandbox.api.listener.EventListener將被靜默,無法收到對應的事件。 需要註意的是,模塊凍結後雖然不再收到相關事件,但沙箱給對應類織入的增強代碼仍然還在。
  • loadCompleted:模塊載入完成,模塊完成載入後調用!模塊完成載入是在模塊完成所有資源載入、分配之後的回調,在模塊生命中期中有且只會調用一次。 這裡拋出異常不會影響模塊被載入成功的結果。模塊載入完成之後,所有的基於模塊的操作都可以在這個回調中進行

最常用的是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 Sandbox的入門使用基本上講完了,上文提到了一些JVM技術名詞,可能小伙伴們聽過但不是特別瞭解。這裡簡單闡述幾個重要的概念,理清楚這幾個概念之間的關係,以便大家更好的理解JVM Sandbox底層的實現。

JVMTI

JVMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程介面,JVMTI可以用來開發並監控虛擬機,可以查看JVM內部的狀態,並控制JVM應用程式的執行。可實現的功能包括但不限於:調試、監控、線程分析、覆蓋率分析工具等。

很多java監控、診斷工具都是基於這種形式來工作的。如果arthas、jinfo、brace等,雖然這些工具底層是JVM TI,但是它們還使用到了上層工具JavaAgent。

JavaAgent和Instrumentation

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的局限性:

  • 不能通過位元組碼文件和自定義的類名重新定義一個本來不存在的類
  • 增強類和老類必須遵循很多限制:比如新類和老類的父類必須相同;新類和老類實現的介面數也要相同,並且是相同的介面;新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;新類和老類新增或刪除的方法必須是private static/final修飾的;

更詳細的原理闡述可以看下文:

https://www.cnblogs.com/rickiyang/p/11368932.html

再談Attach和Agent

上面的實戰章節中已經提到了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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 該系列已更新文章: 分享一個實用的 vite + vue3 組件庫腳手架工具,提升開發效率 開箱即用 yyg-cli 腳手架:快速創建 vue3 組件庫和vue3 全家桶項目 Vue3 企業級優雅實戰 - 組件庫框架 - 1 搭建 pnpm monorepo Vue3 企業級優雅實戰 - 組件庫框架 ...
  • css裡面有個背景色漸變色的效果,我們能拿來做什麼呢 現在就演示下,我在開發此頁面時所實際實現的樣子 演示頁面-唯一線上客服系統 實現代碼很簡單,效果還是很不錯: background: linear-gradient(90deg, #EE884C 0%, #FFBA8E 100%); 首頁里也有個 ...
  • 摘要:Ajax是非同步JavaScript和XML可用於前後端交互。 本文分享自華為雲社區《Flask框架:運用Ajax輪詢動態繪圖》,作者:LyShark。 Ajax是非同步JavaScript和XML可用於前後端交互,在之前《Flask 框架:運用Ajax實現數據交互》簡單實現了前後端交互,本章將通 ...
  • 5. 文件服務開發 全套代碼及資料全部完整提供,點此處下載 5.1 環境搭建 5.1.1 資料庫環境搭建 第一步:創建pd_files資料庫 create database pd_files character set utf8mb4; 第二步:在pd_files資料庫中創建pd_attachmen ...
  • 前提 Tomcat 10.1.x Tomcat線程池介紹 Tomcat線程池,源於JAVA JDK自帶線程池。由於JAVA JDK線程池策略,比較適合處理 CPU 密集型任務,但是對於 I/O 密集型任務,如資料庫查詢,rpc 請求調用等,不是很友好,所以Tomcat在其基礎上進行了擴展。 任務處理 ...
  • 模板 c++另一種編程思想稱為泛型編程,主要利用的技術就是模板 c++提供兩種模板機制:函數模板和類模板 函數模板 建立一個通用函數,函數的返回值類型和形參類型可以不具體指定,用一個虛擬的類型來代表 語法: template<typename T> //或者 template<class T> 函數 ...
  • 渲染模板 我的客服系統後端使用的golang Gin 框架,想把頁面渲染出來,下麵就是載入html模板頁面 package router func InitViewRouter(engine *gin.Engine) { //關於頁面 engine.GET("/aboutus.html", func ...
  • 在看集合源碼的時候,因為對一些知識點有些混淆,導致看源碼比較吃力。所以重新回顧一下麵向對象的繼承和多態,順便記錄一下重點。 繼承 子類會繼承父類的所有屬性和方法,但私有屬性和方法在子類不能直接訪問,需要通過父類提供的公共方法訪問; 子類必須調用父類的構造器,完成父類的初始化(創建子類對象時會調用父類 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...