1、引言
開發者在編碼效率和快速疊代中的痛點場景包括:
1. 修改代碼後,需要頻繁重新開機應用,導緻開發效率低下;
2. 實時調試時,不能立即看到代碼修改的結果;
3. 大型項目中,重新開機的時間成本較高。
針對這些問題,本文将深入探讨如何利用Spring Loaded熱更新技術提高開發效率,減少編譯和重新開機時間。分析Spring Loaded的熱更新原理,以及實際應用過程中所需的操作和注意事項。
2、架構簡介
Spring Loaded is a JVM agent for reloading class file changes whilst a JVM is running. It transforms classes at loadtime to make them amenable to later reloading. Unlike 'hot code replace' which only allows simple changes once a JVM is running (e.g. changes to method bodies), Spring Loaded allows you to add/modify/delete methods/fields/constructors. The annotations on types/methods/fields/constructors can also be modified and it is possible to add/remove/change values in enum types.
Spring Loaded 是一個 JVM 代理,可以在 JVM 運作時重新加載類檔案的更改。它會在加載時轉換類,以便稍後重新加載。與“熱代碼替換”隻允許在 JVM 運作時進行簡單更改(例如更改方法體)不同,Spring Loaded 允許您添加/修改/删除方法/字段/構造函數。還可以修改類型/方法/字段/構造函數上的注解,并且可以添加/删除/更改枚舉類型中的值。
3、如何使用
3.1 下載下傳Agent插件
https://repo1.maven.org/maven2/org/springframework/springloaded/1.2.8.RELEASE/springloaded-1.2.8.RELEASE.jar
3.2 引入Agent插件
在jvm的啟動指令中添加以下參數
-javaagent:/Users/you/runtime/springloaded-1.2.8.RELEASE.jar -noverify
3.3 修改并重新編譯
修改代碼後執行Build->Recompile指令,可以看到在class reloaded完成後,程式的運作邏輯發生了變化
4、原理分析
4.1 代碼編譯分析
先來看一段源代碼,這是一個RpcService類,定義了target字段、targetStatic靜态字段和say方法,現在我們編譯它。
public class RpcService {
private String target = "rpc";
private static String targetStatic = "rpc static";
public String say() {
return "RpcService say hello SpringLoaded" + target;
}
}
SpringLoaded對類編譯後添加了一些跟蹤記錄字段,添加方法攔截判斷。
public static ReloadableType r$type = TypeRegistry.getReloadableType(0, 1);
public transient ISMgr r$fields;
public static final SSMgr r$sfields;
public String hello() {
if (r$type.changed(0) == 1) {
return r$type.fetchLatest()).say(this);
}
String targetNew = TypeRegistry.instanceFieldInterceptionRequired(1, "target") ? (String)r$get(this, "target") : this.target;
return "RpcService say hello SpringLoaded" + targetNew;
}
我們可以在代碼運作時,使用getDeclaredField、getDeclaredMethod等函數在運作時擷取類成員、方法資訊,此時可以看到增強後的類多了如下字段和方法。
在編譯後的代碼中,我們可以看到RpcService類包含了一些新的字段和方法,這些都是Spring Loaded架構增加的。
•r$type是一個靜态變量,其類型為ReloadableType。這個字段用于表示目前類的可重載類型,它包含了目前類的最新位元組碼和其他相關資訊。
- r$get、r$set方法是用于擷取執行個體字段的值的方法,處理字段的攔截和替換。
- ___clinit___方法是用于執行類的靜态初始化塊的方法。
- ___init___()方法是用于處理類的構造函數的方法。
- 在say()方法中增加了一個代碼片段用于判斷類是否發生了變更,如果變更了,則調用最新的可重載類型中的say()方法擷取結果。否則,繼續執行原有的方法體。在方法體中,也增加了一個代碼片段用于判斷本地變量是否需要攔截,如果需要,則使用r$get()方法擷取非靜态變量traget的值,并用它替換原有的變量值。
4.2 運作過程分析
1、在應用程式啟動時,Spring Loaded在目标類路徑中查找所有的類,并在ClassPreProcessor中使用自定義類加載器加載這些類,重新定義後存入TypeRegistry,用于緩存、變更對比和依賴關系維護。
2、注冊一個檔案變化監聽器FileChangeListener,當一個類檔案被修改後,Spring Loaded會檢測到這個變化,并重新加載該類檔案。
3、當一個類被重新加載時,Spring Loaded會嘗試對比類的簽名和繼承關系沒有改變,如果新的類定義與之前的類定義相容,那麼Spring Loaded會更新應用程式中的對象引用,以指向新的類定義。
5、總結
Spring-loaded 使用 Java 的 Instrumentation API 在 JVM 啟動時指定 Agent,使它能夠在目标類加載之前進行攔截,并将目标類的位元組碼通過 ASM 庫解析成抽象文法樹(AST),然後對 AST 進行修改。修改的内容包括增加、删除、替換方法,修改方法體,添加字段等,最終替換目标類,改變其邏輯,實作對代碼的熱更新。
6、擴充内容
- Jrebel也可以實作類似熱更新功能,并且它更高效、穩定。jrebel官網
- Spring-boot-devtools也可以提升開發速度,但是它的方案更像是熱重新開機。Spring Boot Devtools Restarter 原理
- 如何自己實作一個熱更新功能呢?思路大同小異,實作各有千秋。如何自己實作一個熱加載?如何定義自己的類加載器?
作者:京東零售 程嘯
來源:京東雲開發者社群