框架的意義
對於程式設計師來說,我們通常知道很多概念,例如元件、模組、系統、框架、架構等,而本文我們重點說 框架。
- 框架,本質上是一些實用經驗集合。即是前輩們在實際開發過程中積攢下來的實戰經驗,累積成一套實用工具,避免你在開發過程中重複去造輪子,特別是幫你把日常中能遇到的場景或問題都給遮蔽掉,框架的意義在於遮蔽掉開發的基礎複雜度、遮蔽掉此類共性的東西,同時建立嚴格的編碼規範,讓框架使用者開箱即用,並且只需要關注差異面,即業務層面的實現。簡而言之,框架只幹一件事,那就是 簡化開發。然後在此基礎上,可能會再考慮一些安全性、效率、效能、彈性、管理、擴充、解耦等等。
理解 Spring 核心
Spring 作為一個框架,目的也是:簡化開發 ,只不過在簡化開發的過程中 Spring 做了一個特別的設計,那就是 Bean管理,這也是 Spring 的設計核心,而 Bean 生命週期管理的設計巧妙的 解耦 了 Bean 之間的關係。
因此 Spring 核心特性就是 解耦 和 簡化。
Spring 框架圖示展示得很清晰,基本描繪出 Spring 框架的核心:
- 核心
- 外延
簡單說,就是 Spring 設計了一個 核心容器 Core Container,這裡頭主要就是管理 Bean 生命週期,然後為了服務這些業務 Bean ,引入了 Core , Context , SpEL 等工具到核心容器中。然後在核心容器基礎上,又為了把更多的能力整合進來,例如為了擴充 資料訪問 能力加入了 JDBC 、ORM 、OXM 、JMS 、Transactions 等,為了擴充 Web 能力加入了 WebSocket 、Servlet、Web、Portlet 等,其中為了把 RequestMapping 或 Servlet 等這些使用整合到業務 Bean 上,引入了 AOP ,包括還有引入(最終是提供) Aspects、Instrumentation、Messageing 等增強方式。
所以仔細一看,Spring 就是把像資料庫訪問、Web支援、快取、訊息傳送等等這些能力整合到業務 Bean 上,並提供一些測試支援。總結來說理解 Spring 就兩點:
- Bean管理: 解耦Bean關係。理解為核心,從 Bean 的定義、建立、管理等,這是業務Bean。
- 功能增強: 解耦功能、宣告式簡化。理解為外延,在業務Bean基礎上,需要訪庫等能力,那就是功能增強。
基本體現的就是兩個核心特性,一個 解耦、一個 簡化。
Bean管理 本身就是在做 解耦,解除耦合,這個解耦指 Bean 和 Bean 之間的關聯關係,Bean 之間通過介面協議互相串聯起來的,至於每個介面有多少個實現類,那都不會有任何影響,Bean 之間只保留單點通道,通過介面相互隔離,關係都交給 Spring 管理,這樣就避免了實現類和實現類之間出現一些耦合,就算方法增減了、引用變更了也不至於互相汙染。
功能增強 本身就是在做 簡化,例如宣告式簡化,像宣告式程式設計,使用者只需要告訴框架他要什麼,不用管框架是如何實現的。另外簡化方面還有 約定優於配置 (當然這個確切的說是 SpringBoot 裡的設計),約定優於配置其實就是約定好了無需去做複雜的配置,例如你引入一個什麼元件或能力就像 redis 或 kafka,你不需要提前配置,因為 springboot 已經為你預設配置,開箱即用。
因此 Spring 框架特性怎麼理解?就 解耦 和 簡化 。
而 SpringBoot,簡單理解就是在 Spring 框架基礎上新增了一個 SPI 可擴充機制 和 版本管理,讓易用性更高,簡化升級。
而 SpringCloud,簡單理解就是,由於 SpringBoot 的 依賴 可以被很好的管理,擴充 可以被可插拔的擴充,因此在 SpringBoot 基礎上整合了很多跟微服務架構相關的能力,例如整合了很多元件,便有了 SpringCloud 全生態。
基本瞭解了 Spring 特性之後,我們回到 Spring 的核心設計 IoC 與 AOP 。
IoC
我們說了 Spring 的其一特性是 解耦,那到底是使用什麼來解耦?
控制反轉(Inversion of Control,縮寫為 IoC),是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱 DI),還有一種方式叫“依賴查詢”(Dependency Lookup,EJB 和 Apache Avalon 都使用這種方式)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體將其所依賴的物件的引用傳遞給它。也可以說,依賴被注入到物件中。
簡單來說,就是原本 Bean 與 Bean 之間的這種互相呼叫,變成了由 IoC 容器去統一調配。如果沒使用 IoC 容器統一管理業務 Bean,你的應用在部署、修改、迭代的時候,業務 Bean 是會侵入程式碼實現並互相呼叫的。
那麼問題來了,所有系統都需要引入 IOC 嗎?
IoC 容器是面向 迭代 起作用,如果你的應用就 不存在迭代 的情況,即系統是萬年不變的,那沒必要引入 IoC,因為你每引入一項技術,都勢必會增加複雜度,所以額外引入 IoC 也一樣會增加你整體應用的複雜度,所以假如 不存在迭代,大可直接寫死A類引用B類,B類又寫死引用C類,無需引入 IoC。一定要理解每一項技術背後是為了解決什麼問題,同時在做架構設計的時候記住兩個原則:合適 、簡單。當然,實際上我們大部分應用是 持續迭代 的,在類實現上、互相引用上、甚至介面協議上都有可能變化,所以一般引入 IoC 是合適的(如果是介面協議變化,即引數或返回值發生變化,那還是需要改動類間的程式碼的)。
具體的,IoC 相當於是把 Bean 例項的建立過程交給 Spring 管理,無論是通過 XML、JavaConfig,還是註解方式,最終都是把例項化的工作交給 Spring 負責,之後 Bean 之間通過介面相互呼叫,而例項化過程中就涉及到 注入,無論採用什麼方式來例項化 Bean,注入 的類別就兩種:
- Setter注入 : 通過 setter 來設定,發生在物件 例項化之後 設定。
- 構造器注入 : 通過構造器注入,發生在物件 例項化之前 就得把引數/例項準備好。
setter注入:
- 與傳統的 JavaBean 的寫法更相似,程式開發人員更容易理解、接受。通過 setter 方法設定依賴關係顯得更加直觀、自然。
- 對於複雜的依賴關係,如果採用構造注入,會導致構造器過於臃腫,難以閱讀。Spring 在建立 Bean 例項時,需要同時例項化其依賴的全部例項,因而導致效能下降。而使用設值注入,則能避免這些問題。
- 尤其在某些成員變數可選的情況下,多引數的構造器更加笨重。
構造器注入:
- 構造器注入可以在構造器中決定依賴關係的注入順序,優先依賴的優先注入。
- 對於依賴關係無需變化的 Bean ,構造注入更有用處。因為沒有 setter 方法,所有的依賴關係全部在構造器內設定,無須擔心後續的程式碼對依賴關係產生破壞。
- 依賴關係只能在構造器中設定,則只有元件的建立者才能改變元件的依賴關係,對元件的呼叫者而言,元件內部的依賴關係完全透明,更符合高內聚的原則。
而這兩種方式的注入方式都使用了 反射。
反射
瞭解反射相關類以及含義:
- java.lang.Class: 代表整個位元組碼。代表一個型別,代表整個類。
- java.lang.reflect.Method: 代表位元組碼中的方法位元組碼。代表類中的方法。
- java.lang.reflect.Constructor: 代表位元組碼中的構造方法位元組碼。代表類中的構造方法。
- java.lang.reflect.Field: 代表位元組碼中的屬性位元組碼。代表類中的成員變數(靜態變數+例項變數)。
java.lang.reflect 包提供了許多反射類,用於獲取或設定例項物件。簡單來說,反射能夠:
- 在執行時 判斷任意一個物件所屬的類;
- 在執行時構造任意一個類的物件;
- 在執行時判斷任意一個類所具有的成員變數和方法;
- 在執行時呼叫任意一個物件的方法;
- 生成動態代理。
IoC 和 反射,只是把 Bean 的例項建立處理完,而後續還有 功能增強,功能增強靠的就是 AOP。
AOP
AOP全名 Aspect-Oriented Programming ,中文直譯為面向切面程式設計,當前已經成為一種比較成熟的程式設計思想,可以用來很好的解決應用系統中分佈於各個模組的交叉關注點問題。在輕量級的J2EE中應用開發中,使用AOP來靈活處理一些具有 橫切性質 的系統級服務,如事務處理、安全檢查、快取、物件池管理等,已經成為一種非常適用的解決方案。
為什麼需要AOP
當我們要進行一些日誌記錄、許可權控制、效能統計等時,在傳統應用程式當中我們可能在需要的物件或方法中進行編碼,而且比如許可權控制、效能統計大部分是重複的,這樣程式碼中就存在大量 重複程式碼,即使有人說我把通用部分提取出來,那必然存在呼叫還是存在重複,像效能統計我們可能只是在必要時才進行,在診斷完畢後要刪除這些程式碼;還有日誌記錄,比如記錄一些方法訪問日誌、資料訪問日誌等等,這些都會滲透到各個要訪問方法中;還有許可權控制,必須在方法執行開始進行稽核,想想這些是多麼可怕而且是多麼無聊的工作。如果採用 Spring,這些日誌記錄、許可權控制、效能統計從業務邏輯中分離出來,通過 Spring 支援的面向切面程式設計,在需要這些功能的地方動態新增這些功能,無需滲透到各個需要的方法或物件中;有人可能說了,我們可以使用“代理設計模式”或“包裝器設計模式”,你可以使用這些,但還是需要通過程式設計方式來建立代理物件,還是要 耦合 這些代理物件,而採用 Spring 面向 切面 程式設計能提供一種更好的方式來完成上述功能,一般通過 配置 方式,而且不需要在現有程式碼中新增任何額外程式碼,現有程式碼專注業務邏輯。
所以,AOP 以橫截面的方式插入到主流程中,Spring AOP 面向切面程式設計能幫助我們無耦合的實現:
- 效能監控,在方法呼叫前後記錄呼叫時間,方法執行太長或超時報警。
- 快取代理,快取某方法的返回值,下次執行該方法時,直接從快取裡獲取。
- 軟體破解,使用 AOP 修改軟體的驗證類的判斷邏輯。
- 記錄日誌,在方法執行前後記錄系統操作日誌。
- 工作流系統,工作流系統需要將業務程式碼和流程引擎程式碼混合在一起執行,那麼我們可以使用AOP將其分離,並動態掛接業務。
- 許可權驗證,方法執行前驗證是否有許可權執行當前方法,沒有則丟擲沒有許可權執行異常,有業務程式碼捕捉。
- 等等
AOP 其實就是從應用中劃分出來了一個切面,然後在這個切面裡面插入一些 “增強”,最後產生一個增加了新功能的 代理物件,注意,是代理物件,這是Spring AOP 實現的基礎。這個代理物件只不過比原始物件(Bean)多了一些功能而已,比如 Bean預處理、Bean後處理、異常處理 等。 AOP 代理的目的就是 將切面織入到目標物件。
AOP如何實現
前面我們說 IoC 的實現靠反射,然後解耦,那 AOP 靠啥實現?
AOP,簡單來說就是給物件增強一些功能,我們需要看 Java 給我們預留了哪些口或者在哪些階段,允許我們去織入某些增強功能。
我們可以從幾個層面來實現AOP。
編譯期
- 原理:在編譯器編譯之前注入原始碼,原始碼被編譯之後的位元組碼自然會包含這部分注入的邏輯。
- 代表作如:lombok, mapstruct(編譯期通過 pluggable annotation processing API 修改的)。
執行期,位元組碼載入前
- 原理:位元組碼要經過 classloader(類載入器)載入,那我們可以通過 自定義類載入器 的方式,在位元組碼被自定義類載入器 載入前 給它修改掉。
- 代表作如:javasist, java.lang.instrument ,ASM(操縱位元組碼)。
- 許多 agent 如 Skywaking, Arthas 都是這麼搞,注意區分 靜態agent 與 動態agent。
- JVMTI 是 JVM 提供操作 native 方法的工具,Instrument 就是提供給你操縱 JVMTI 的 java 介面,詳情見 java.lang.instrument.Instrumentation
執行期,位元組碼載入後
- 原理:位元組碼被類載入器載入後,動態構建位元組碼檔案生成目標類的 子類,將切面邏輯加入到子類中。
- 代表作如:jdk proxy, cglib。
按照類別分類,基本可以理解為:
類別 | 原理 | 優點 | 缺點 |
---|---|---|---|
靜態AOP | 在編譯期,切面直接以位元組碼的形式編譯到目標位元組碼檔案中 | 對系統無效能影響 | 靈活度不夠 |
動態AOP | 在執行期,目標類載入後,為介面動態生成代理類,將切面織入到代理類中 | 動態代理方式,相對於靜態AOP更加靈活 | 切入的關注點需要實現介面,對系統有一點效能影響 |
動態位元組碼生成 | 在執行期,目標類載入後,動態構建位元組碼檔案生成目標類的 子類,將切面邏輯加入到子類中 | 沒有介面也可以織入 | 擴充套件類的例項方法為final時,則無法進行織入。效能基本是最差的,因為需要生成子類巢狀一層,spring用的cglib就是這麼搞的,所以效能比較差 |
自定義類載入器 | 在執行期,在位元組碼被自定義類載入器載入前,將切面邏輯加到目標位元組碼裡,例如阿里的Pandora | 可以對絕大部分類進行織入 | 程式碼中如果使用了其他類載入器,則這些類將不會被織入 |
位元組碼轉換 | 在執行期,所有類載入器載入位元組碼前,進行攔截 | 可以對所有類進行織入 | - |
當然,理論上是越早織入,效能越好,像 lombok,mapstruct 這類靜態AOP,基本在編譯期之前都修改完,所以效能很好,但是靈活性方面當然會比較差,獲取不到執行時的一些資訊情況,所以需要權衡比較。
簡單說明5種類別:
當然我整理了一份詳細的腦圖,可以直接在網頁上開啟。
《腦圖:Java實現AOP思路》:
https://www.processon.com/emb...
<iframe id="embed_dom" name="embed_dom" frameborder="0" style="display:block;width:100%; height:250px;" src="https://www.processon.com/embed/62333d1ce0b34d074452eec2"></iframe>
1、靜態AOP
發生在 編譯期,通過 Pluggable Annotation Processing API 修改原始碼。
在 javac 進行編譯的時候,會根據原始碼生成抽象語法樹(AST),而 java 通過開放 Pluggable Annotation Processing API 允許你參與修改原始碼,最終生成位元組碼。典型的代表就是 lombok。
2、動態AOP (動態代理)
發生在 執行期,於 位元組碼載入後,類、方法已經都被載入到方法區中了。
典型的代表就是 JDK Proxy。
public static void main(String[] args) {
// 需要代理的介面,被代理類實現的多個介面,都必須在這裡定義
Class[] proxyInterface = new Class[]{IBusiness.class,IBusiness2.class};
// 構建AOP的Advice,這裡需要傳入業務類的例項
LogInvocationHandler handler = new LogInvocationHandler(new Business());
// 生成代理類的位元組碼載入器
ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
// 織入器,織入程式碼並生成代理類
IBusiness2 proxyBusiness =
(IBusiness2)Proxy.newProxyInstance(classLoader, proxyInterface, handler);
// 使用代理類的例項來呼叫方法
proxyBusiness.doSomeThing2();
((IBusiness)proxyBusiness).doSomeThing();
}
其中代理實現 InvocationHandler 介面,最終實現邏輯在 invoke 方法中。生成代理類之後,只要目標物件的方法被呼叫了,都會優先進入代理類 invoke 方法,進行增強驗證等行為。
public class LogInvocationHandler implements InvocationHandler{
private Object target; // 目標物件
LogInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 執行原有邏輯
Object rev = method.invoke(target,args);
// 執行織入的日誌,你可以控制那些方法執行切入邏輯
if (method.getName().equals("doSomeThing2")){
// 記錄日誌
}
return rev;
}
}
當然動態代理相對也是效能差,畢竟也多走了一層代理,每多走一層就肯定是越難以優化。
雖然,動態代理在執行期通過介面動態生成代理類,這為其帶來了一定的靈活性,但這個靈活性卻帶來了兩個問題:
- 第一代理類必須實現一個介面,如果沒實現介面會丟擲一個異常。
- 第二效能影響,因為動態代理使用反射的機制實現的,首先反射肯定比直接呼叫要慢,經過測試大概每個代理類比靜態代理多出10幾毫秒的消耗。其次使用反射大量生成類檔案可能引起 Full GC 造成效能影響,因為位元組碼檔案載入後會存放在JVM執行時區的方法區(或者叫持久代,JDK1.8 之後已經在元空間)中,當方法區滿的時候,會引起 Full GC ,所以當你大量使用動態代理時,可以將持久代設定大一些,減少 Full GC 次數。
關於動態代理的詳細原理和流程,推薦閱讀《一文讀懂Java動態代理》。
3、動態位元組碼生成
發生在 執行期,於 位元組碼載入後 ,生成目標類的子類,將切面邏輯加入到子類中,所以使用Cglib實現AOP不需要基於介面。
此時類、方法同樣已經都被載入到方法區中了。
典型的代表就是 Cglib(底層也是基於ASM操作位元組碼), Cglib 是一個強大的,高效能的 Code 生成類庫,它可以在執行期間擴充套件Java類和實現Java介面,它封裝了 Asm,所以使用 Cglib 前需要引入 Asm 的jar。
public static void main(String[] args) {
byteCodeGe();
}
/**
* 動態位元組碼生成
*/
public static void byteCodeGe() {
//建立一個織入器
Enhancer enhancer = new Enhancer();
//設定父類
enhancer.setSuperclass(Business.class);
//設定需要織入的邏輯
enhancer.setCallback(new LogIntercept());
//使用織入器建立子類
IBusiness2 newBusiness = (IBusiness2) enhancer.create();
newBusiness.doSomeThing2();
}
/**
* 記錄日誌
*/
public static class LogIntercept implements MethodInterceptor {
@Override
public Object intercept(
Object target,
Method method,
Object[] args,
MethodProxy proxy) throws Throwable {
//執行原有邏輯,注意這裡是invokeSuper
Object rev = proxy.invokeSuper(target, args);
//執行織入的日誌
if (method.getName().equals("doSomeThing")) {
System.out.println("recordLog");
}
return rev;
}
}
Spring 預設採取 JDK 動態代理 機制實現 AOP,當動態代理不可用時(代理類無介面)會使用 CGlib 機制,缺點是:
- 只能對方法進行切入,不能對介面、欄位、static靜態程式碼塊、private私有方法進行切入。
- 同類中的互相呼叫方法將不會使用代理類。因為要使用代理類必須從Spring容器中獲取Bean。同類中的互相呼叫方法是通過 this 關鍵字來呼叫,spring 基本無法去修改 jvm 裡面的邏輯。
- 使用 CGlib 無法對 final 類進行代理,因為無法生成子類了。
4、自定義類載入器
發生在 執行期,於 位元組碼載入前,在類載入到JVM之前直接修改某些類的 方法,並將 切入邏輯 織入到這個方法裡,然後將修改後的位元組碼檔案交給虛擬機器執行。
典型的代表就是 javasist,它可以獲得指定方法名的方法、執行前後插入程式碼邏輯。
Javassist是一個編輯位元組碼的框架,可以讓你很簡單地操作位元組碼。它可以在執行期定義或修改Class。使用Javassist實現AOP的原理是在位元組碼載入前直接修改需要切入的方法。這比使用Cglib實現AOP更加高效,並且沒太多限制,實現原理如下圖:
我們使用系統類載入器啟動我們自定義的類載入器,在這個類載入器里加一個類載入監聽器,監聽器發現目標類被載入時就織入切入邏輯,我們再看看使用Javassist 實現 AOP 的程式碼:
/***啟動自定義的類載入器****/
//獲取存放CtClass的容器ClassPool
ClassPool cp = ClassPool.getDefault();
//建立一個類載入器
Loader cl = new Loader();
//增加一個轉換器
cl.addTranslator(cp, new MyTranslator());
//啟動MyTranslator的main函式
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
// 類載入監聽器
public static class MyTranslator implements Translator {
public void start(ClassPool pool) throws
NotFoundException, CannotCompileException {
}
/**
* 類裝載到JVM前進行程式碼織入
*/
public void onLoad(ClassPool pool, String classname) {
if (!"model$Business".equals(classname)) {
return;
}
//通過獲取類檔案
try {
CtClass cc = pool.get(classname);
//獲得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法執行前插入程式碼
m.insertBefore("{ System.out.println(\"recordLog\"); }");
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
}
}
public static void main(String[] args) {
Business b = new Business();
b.doSomeThing2();
b.doSomeThing();
}
}
CtClass 是一個class檔案的抽象描述。也可以使用 insertAfter() 在方法的末尾插入程式碼,或者使用 insertAt() 在指定行插入程式碼。
使用自定義的類載入器實現AOP在效能上要優於動態代理和Cglib,因為它不會產生新類,但是它仍然存在一個問題,就是如果其他的類載入器來載入類的話,這些類將不會被攔截。
5、位元組碼轉換
自定義的類載入器實現AOP只能攔截自己載入的位元組碼,那麼有沒有一種方式能夠監控所有類載入器載入位元組碼呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,開發者可以構建一個位元組碼轉換器,在位元組碼載入前進行轉換。
發生在 執行期 ,於 位元組碼載入前,Java 1.5 開始提供的 Instrumentation API 。Instrumentation API 就像是 JVM 預先放置的後門,它可以攔截在JVM上執行的程式,修改位元組碼。
這種方式是 Java API 天然提供的,在 java.lang.instrumentation ,就算 javasist 也是基於此實現。
一個代理實現 ClassFileTransformer 介面用於改變執行時的位元組碼(class File),這個改變發生在 jvm 載入這個類之前,對所有的類載入器有效。class File 這個術語定義於虛擬機器規範3.1,指的是位元組碼的 byte 陣列,而不是檔案系統中的 class 檔案。介面中只有一個方法:
/**
* 位元組碼載入到虛擬機器前會進入這個方法
*/
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
// 把 classBeingRedefined 重定義之後再交還回去
ClassFileTransformer 需要新增到 Instrumentation 例項中才能生效。
安全點注意
當對 JVM 中的位元組碼進行修改的時候,虛擬機器也會通知所有執行緒通過安全點的方式停下來,因為修改會影響到類結構。
啟動流程
Bean生命週期管理,基本從無到有(IoC),從有到增強(AOP)。
任何Bean在Spring容器中只有三種形態,定義、例項、增強。
從Bean定義資訊觀察,通過 xml 定義 bean關係,properties、yaml、json定義 屬性,bean關係和屬性就構成Bean的定義,其中BeanDefinitionReader負責掃描定義資訊生成Bean定義物件 BeanDefinition。在此基礎上,允許對 BeanDefinition 定義進行增強(Mybatis與Spring存在很多使用場景)。
Bean定義完成之後,開始通過反射例項化物件、填充屬性等,同時又再次預留了很多增強的口,最終生成一個完整的物件。
例項化流程與三級快取
從定義到擴充套件,然後反射例項化,到增強,每種狀態都會存在引用。
所以Spring設計 三級快取,說白了是對應儲存Bean生命週期的三種形態:
- 定義
- 例項
- 增強
總結
Spring 就是 反射 + 位元組碼增強。
- 反射,為了 IoC 和 解耦
- 位元組碼增強,為了 簡化 和宣告式程式設計
深刻理解 Spring 這兩部分核心特性,關於 spring、springboot、springcloud 的所有語法糖設計與使用,就自然清楚。
參考
- Understanding Java Agents
- Java 1.5-java.lang.instrument
- ASM 位元組碼插樁
- arthas
- ASM
- cglib
- javassist
- Javassist/ASM Audit Log
- bytebuddy tutorial
- Performance Comparison of cglib, Javassist, JDK Proxy and Byte Buddy
- 控制反轉
- AOP 的實現機制
- Spring AOP 總結
- javaAgent、ASM、javassist、ByteBuddy 是什麼?
首發訂閱
這裡記錄技術內容,不定時釋出,首發在
- 潘深練個人網站
- 微信公眾號:潘潘和他的朋友們
(本篇完)