給女同事講完代理后,女同事說:你好棒哦
說在前面:今天我們來聊一聊 Java 中的代理,先來聊聊故事背景:
❝小明想購買法國某個牌子的香水送給女朋友,但是在國內沒有貨源售賣,親自去法國又大費周章了,而小紅現在正在法國玩耍,她和小明是好朋友,可以幫小明買到這個牌子的香水,于是小明就找到小紅,答應給她多加 5% 的辛苦費,小紅答應了,小明成功在中國買到了法國的香水。之后小紅開啟了瘋狂的代購模式,賺到了很多手續費。❞
在故事中,「小明是一個客戶」,它讓小紅幫忙購買香水,「小紅就成了一個代理對象」,而「香水提供商是一個真實的對象」,可以售賣香水,小明通過代理商小紅,購買到法國的香水,這就是一個代購的例子。我畫了一幅圖幫助理解這個故事的整個結構。
這個故事是最典型的代理模式,代購從供應商購買貨物后返回給調用者,也就是需要代理的小明。
代理可以分為靜態代理和動態代理兩大類:
「靜態代理」
- 優點:代碼結構簡單,較容易實現
- 缺點:無法適配所有代理場景,如果有新的需求,需要修改代理類,「不符合軟件工程的開閉原則」
小紅現在只是代理香水,如果小明需要找小紅買法國紅酒,那小紅就需要代理法國紅酒了,但是靜態代理去擴展代理功能「必須修改小紅內部的邏輯,這會讓小紅內部代碼越來越臃腫」,后面會詳細分析。
「動態代理」
- 優點:能夠動態適配特定的代理場景,擴展性較好,「符合軟件工程的開閉原則」
- 缺點:動態代理需要利用到反射機制和動態生成字節碼,導致其性能會比靜態代理稍差一些,「但是相比于優點,這些劣勢幾乎可以忽略不計」
如果小明需要找小紅代理紅酒,我們「無需修改代理類小紅的內部邏輯」,只需要關注擴展的功能點:「代理紅酒」,實例化新的類,通過一些轉換即可讓小紅「既能夠代理香水也能夠代理紅酒」了。
本文將會通過以下幾點,盡可能讓你理解 Java 代理中所有重要的知識點:
學習代理模式(實現故事的代碼,解釋代理模式的類結構特點)
比較靜態代理與動態代理二者的異同
Java 中常見的兩種動態代理實現(JDK Proxy 和 Cglib)
動態代理的應用(Spring AOP)
代理模式
(1)我們定義好一個「售賣香水」的接口,定義好售賣香水的方法并傳入該香水的價格。
- public interface SellPerfume {
- void sellPerfume(double price);
- }
(2)定義香奈兒(Chanel)香水提供商,實現接口。
- public class ChanelFactory implements SellPerfume {
- @Override
- public void sellPerfume(double price) {
- System.out.println("成功購買香奈兒品牌的香水,價格是:" + price + "元");
- }
- }
(3)定義「小紅」代理類,她需要代購去售賣香奈兒香水,所以她是香奈兒香水提供商的代理對象,同樣實現接口,并在內部保存對目標對象(香奈兒提供商)的引用,控制其它對象對目標對象的訪問。
- public class XiaoHongSellProxy implements SellPerfume {
- private SellPerfume sellPerfumeFactory;
- public XiaoHongSellProxy(SellPerfume sellPerfumeFactory) {
- this.sellPerfumeFactory = sellPerfumeFactory;
- }
- @Override
- public void sellPerfume(double price) {
- doSomethingBeforeSell(); // 前置增強
- sellPerfumeFactory.sellPerfume(price);
- doSomethingAfterSell(); // 后置增強
- }
- private void doSomethingBeforeSell() {
- System.out.println("小紅代理購買香水前的額外操作...");
- }
- private void doSomethingAfterSell() {
- System.out.println("小紅代理購買香水后的額外操作...");
- }
- }
(4)小明是一個需求者,他需要去購買香水,只能通過小紅去購買,所以他去找小紅購買1999.99的香水。
- public class XiaoMing {
- public static void main(String[] args) {
- ChanelFactory factory = new ChanelFactory();
- XiaoHongSellProxy proxy = new XiaoHongSellProxy(factory);
- proxy.sellPerfume(1999.99);
- }
- }
我們來看看運行結果,小紅在向小明售賣香水前可以執行額外的其它操作,如果良心點的代購就會「打折、包郵···」,如果黑心點的代購就會「加手續費、售出不退還···」,是不是很刺激。
我們來看看上面 4 個類組成的類圖關系結構,可以發現「小紅」和「香奈兒提供商」都實現了「售賣香水」這一接口,而小紅內部增加了對提供商的引用,用于調用提供商的售賣香水功能。
實現代理模式,需要走以下幾個步驟:
- 「定義真實對象和代理對象的公共接口」(售賣香水接口)
- 「代理對象內部保存對真實目標對象的引用」(小紅引用提供商)
- 訪問者僅能通過代理對象訪問真實目標對象,「不可直接訪問目標對象」(小明只能通過小紅去購買香水,不能直接到香奈兒提供商購買)
❝代理模式很容易產生錯誤思維的一個地方:代理對象并不是真正提供服務的一個對象,它只是替訪問者訪問目標對象的一個「中間人」,真正提供服務的還是目標對象,而代理對象的作用就是在目標對象提供服務之前和之后能夠執行額外的邏輯。
從故事來說,小紅并不是真正賣香水的,賣香水的還是香奈兒提供商,而小紅只不過是在讓香奈兒賣香水之前和之后執行了一些自己額外加上去的操作。❞
講完這個代理模式的代碼實現,我們來系統地學習它究竟是如何定義的,以及實現它需要注意什么規范。
代理模式的定義:「給目標對象提供一個代理對象,代理對象包含該目標對象,并控制對該目標對象的訪問。」
代理模式的目的:
- 通過代理對象的隔離,可以在對目標對象訪問前后「增加額外的業務邏輯,實現功能增強。」
- 通過代理對象訪問目標對象,可以「防止系統大量地直接對目標對象進行不正確地訪問」,出現不可預測的后果
靜態代理與動態代理
你是否會有我一樣的疑惑:代理為什么還要分靜態和動態的?它們兩個有啥不同嗎?
很明顯,所有人都會有這樣的疑惑,我們先來看看它們的相同點:
- 都能夠實現代理模式(這不廢話嗎...)
- 無論是靜態代理還是動態代理,代理對象和目標對象都需要實現一個「公共接口」
重點當然是它們的不同之處,動態代理在靜態代理的基礎上做了改進,極大地提高了程序的「可維護性」和「可擴展性」。我先列出它們倆的不同之處,再詳細解釋為何靜態代理不具備這兩個特性:
- 動態代理產生代理對象的時機是「運行時動態生成」,它沒有 Java 源文件,「直接生成字節碼文件實例化代理對象」;而靜態代理的代理對象,在「程序編譯時」已經寫好 Java 文件了,直接 new 一個代理對象即可。
- 動態代理比靜態代理更加穩健,對程序的可維護性和可擴展性更加友好
目前來看,代理對象小紅已經能夠代理購買香水了,但有一天,小紅的另外一個朋友小何來了,「他想購買最純正的法國紅酒」,國內沒有這樣的購買渠道,小紅剛巧也在法國,于是小何就想找小紅幫他買紅酒啦,這和小明找小紅是一個道理的,都是想讓小紅做代理。
但問題是:在程序中,小紅只能代理購買香水,「如果要代理購買紅酒」,要怎么做呢?
- 創建售賣紅酒的接口
- 售賣紅酒提供商和代理對象小紅都需要實現該接口
- 小何訪問小紅,讓小紅賣給他紅酒
OK,事已至此,代碼就不重復寫了,我們來探討一下,面對這種新增的場景,上面的這種實現方法有沒有什么缺陷呢?
我們不得不提的是軟件工程中的「開閉原則」
❝開閉原則:在編寫程序的過程中,軟件的所有對象應該是對擴展是開放的,而對修改是關閉的❞
靜態代理違反了開閉原則,原因是:面對新的需求時,需要修改代理類,增加實現新的接口和方法,導致代理類越來越龐大,變得難以維護。
雖然說目前代理類只是實現了2個接口,**如果日后小紅不只是代理售賣紅酒,還需要代理售賣電影票、代購日本壽司······**實現的接口會變得越來越多,內部的結構變得越來越復雜,「整個類顯得愈發臃腫」,變得不可維護,之后的擴展也會成問題,只要任意一個接口有改動,就會牽扯到這個代理類,維護的代價很高。
「所以,為了提高類的可擴展性和可維護性,滿足開閉原則,Java 提供了動態代理機制。」
常見的動態代理實現
動態代理最重要的當然是「動態」兩個字,學習動態代理的過程,最重要的就是理解何為動態,話不多說,馬上開整。
我們來明確一點:「動態代理解決的問題是面對新的需求時,不需要修改代理對象的代碼,只需要新增接口和真實對象,在客戶端調用即可完成新的代理。」
這樣做的目的:滿足軟件工程的開閉原則,提高類的可維護性和可擴展性。
JDK Proxy
JDK Proxy 是 JDK 提供的一個動態代理機制,它涉及到兩個核心類,分別是Proxy和InvocationHandler,我們先來了解如何使用它們。
以小紅代理賣香水的故事為例,香奈兒香水提供商依舊是真實對象,實現了SellPerfume接口,這里不再重新寫了,重點是「小紅代理」,這里的代理對象不再是小紅一個人,而是一個「代理工廠」,里面會有許多的代理對象。我畫了一幅圖,你看了之后會很好理解:
小明來到代理工廠,需要購買一款法國在售的香奈兒香水,那么工廠就會**找一個可以實際的代理對象(動態實例化)**分配給小明,例如小紅或者小花,讓該代理對象完成小明的需求。「該代理工廠含有無窮無盡的代理對象可以分配,且每個對象可以代理的事情可以根據程序的變化而動態變化,無需修改代理工廠。」
如果有一天小明需要招待一個可以「代購紅酒」的代理對象,該代理工廠依舊可以滿足他的需求,無論日后需要什么代理,都可以滿足,是不是覺得很神奇?我們來學習如何使用它。
我們看一下動態代理的 UML 類圖結構長什么樣子。
可以看到和靜態代理區別不大,唯一的變動是代理對象,我做了標注:「由代理工廠生產」。
這句話的意思是:「代理對象是在程序運行過程中,由代理工廠動態生成,代理對象本身不存在 Java 源文件」。
那么,我們的關注點有2個:
- 如何實現一個代理工廠
- 如何通過代理工廠動態生成代理對象
首先,代理工廠需要實現InvocationHanlder接口并實現其invoke()方法。
- public class SellProxyFactory implements InvocationHandler {
- /** 代理的真實對象 */
- private Object realObject;
- public SellProxyFactory(Object realObject) {
- this.realObject = realObject;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- doSomethingBefore();
- Object obj = method.invoke(realObject, args);
- doSomethingAfter();
- return obj;
- }
- private void doSomethingAfter() {
- System.out.println("執行代理后的額外操作...");
- }
- private void doSomethingBefore() {
- System.out.println("執行代理前的額外操作...");
- }
- }
invoke() 方法有3個參數:
- Object proxy:代理對象
- Method method:真正執行的方法
- Object[] agrs:調用第二個參數 method 時傳入的參數列表值
invoke() 方法是一個代理方法,也就是說最后客戶端請求代理時,執行的就是該方法。代理工廠類到這里為止已經結束了,我們接下來看第二點:「如何通過代理工廠動態生成代理對象」。
生成代理對象需要用到Proxy類,它可以幫助我們生成任意一個代理對象,里面提供一個靜態方法newProxyInstance。
- Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
實例化代理對象時,需要傳入3個參數:
- ClassLoader loader:加載動態代理類的類加載器
- Class[] interfaces:代理類實現的接口,可以傳入多個接口
- InvocationHandler h:指定代理類的「調用處理程序」,即調用接口中的方法時,會找到該代理工廠h,執行invoke()方法
我們在客戶端請求代理時,就需要用到上面這個方法。
- public class XiaoMing {
- public static void main(String[] args) {
- ChanelFactory chanelFactory = new ChanelFactory();
- SellProxyFactory sellProxyFactory = new SellProxyFactory(chanelFactory);
- SellPerfume sellPerfume = (SellPerfume) Proxy.newProxyInstance(chanelFactory.getClass().getClassLoader(),
- chanelFactory.getClass().getInterfaces(),
- sellProxyFactory);
- sellPerfume.sellPerfume(1999.99);
- }
- }
執行結果和靜態代理的結果相同,但二者的思想是不一樣的,一個是靜態,一個是動態。那又如何體現出動態代理的優勢呢?別急,往下看就知道了。
❝注意看下圖,相比靜態代理的前置增強和后置增強,少了「小紅」二字,實際上代理工廠分配的代理對象是隨機的,不會針對某一個具體的代理對象,所以每次生成的代理對象都不一樣,也就不確定是不是小紅了,但是能夠唯一確定的是,「這個代理對象能和小紅一樣幫小明買到香水!」❞
按照之前的故事線發展,小紅去代理紅酒,而「小明又想買法國的名牌紅酒」,所以去找代理工廠,讓它再分配一個人幫小明買紅酒,代理工廠說:“當然沒問題!我們是專業的!等著!”
我們需要實現兩個類:紅酒提供商類 和 售賣紅酒接口。
- /** 售賣紅酒接口 */
- public interface SellWine {
- void sellWine(double price);
- }
- /** 紅酒供應商 */
- public class RedWineFactory implements SellWine {
- @Override
- public void sellWine(double price) {
- System.out.println("成功售賣一瓶紅酒,價格:" + price + "元");
- }
- }
然后我們的小明在請求代理工廠時,就可以「實例化一個可以售賣紅酒的代理」了。
- public class XiaoMing {
- public static void main(String[] args) {
- // 實例化一個紅酒銷售商
- RedWineFactory redWineFactory = new RedWineFactory();
- // 實例化代理工廠,傳入紅酒銷售商引用控制對其的訪問
- SellProxyFactory sellProxyFactory = new SellProxyFactory(redWineFactory);
- // 實例化代理對象,該對象可以代理售賣紅酒
- SellWine sellWineProxy = (SellWine) Proxy.newProxyInstance(redWineFactory.getClass().getClassLoader(),
- redWineFactory.getClass().getInterfaces(),
- sellProxyFactory);
- // 代理售賣紅酒
- sellWineProxy.sellWine(1999.99);
- }
- }
期待一下執行結果,你會很驚喜地發現,居然也能夠代理售賣紅酒了,但是我們「沒有修改代理工廠」。
回顧一下我們新增紅酒代理功能時,需要2個步驟:
- 創建新的紅酒提供商SellWineFactory和售賣紅酒接口SellWine在客戶端實例化一個代理對象,然后向該代理對象購買紅酒
- 再回想「開閉原則:面向擴展開放,面向修改關閉」。動態代理正是滿足了這一重要原則,在面對功能需求擴展時,只需要關注擴展的部分,不需要修改系統中原有的代碼。
如果感興趣想深究的朋友,把注意力放在Proxy.newProxyInstance()這個方法上,這是整個 JDK 動態代理起飛的一個方法。
講到這里,JDK 提供的動態代理已經到尾聲了,我們來總結一下 JDK 的動態代理:
(1)JDK 動態代理的使用方法
- 代理工廠需要實現 InvocationHandler接口,調用代理方法時會轉向執行invoke()方法
- 生成代理對象需要使用Proxy對象中的newProxyInstance()方法,返回對象可強轉成傳入的其中一個接口,然后調用接口方法即可實現代理
(2)JDK 動態代理的特點
目標對象強制需要實現一個接口,否則無法使用 JDK 動態代理
- 「(以下為擴展內容,如果不想看可跳過)」
Proxy.newProxyInstance() 是生成動態代理對象的關鍵,我們可來看看它里面到底干了些什么,我把重要的代碼提取出來,一些對分析無用的代碼就省略掉了。
- private static final Class<?>[] constructorParams ={ InvocationHandler.class };
- public static Object newProxyInstance(ClassLoader loader,
- Class<?>[] interfaces,
- InvocationHandler h) {
- // 獲取代理類的 Class 對象
- Class<?> cl = getProxyClass0(loader, intfs);
- // 獲取代理對象的顯示構造器,參數類型是 InvocationHandler
- final Constructor<?> cons = cl.getConstructor(constructorParams);
- // 反射,通過構造器實例化動態代理對象
- return cons.newInstance(new Object[]{h});
- }
我們看到第 6 行獲取了一個動態代理對象,那么是如何生成的呢?接著往下看。
- private static Class<?> getProxyClass0(ClassLoader loader,
- Class<?>... interfaces) {
- // 去代理類對象緩存中獲取代理類的 Class 對象
- return proxyClassCache.get(loader, interfaces);
- }
發現里面用到一個緩存 「proxyClassCache」,從結構來看類似于是一個 map結構,根據類加載器loader和真實對象實現的接口interfaces查找是否有對應的 Class 對象,我們接著往下看 get() 方法。
- public V get(K key, P parameter) {
- // 先從緩存中查詢是否能根據 key 和 parameter 查詢到 Class 對象
- // ...
- // 生成一個代理類
- Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
- }
在 get() 方法中,如果沒有從緩存中獲取到 Class 對象,則需要利用「subKeyFactory」 去實例化一個動態代理對象,而在 「Proxy」 類中包含一個 「ProxyClassFactory」 內部類,由它來創建一個動態代理類,所以我們接著去看 ProxyClassFactory 中的 apply() 方法。
- private static final class ProxyClassFactory
- implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
- // 非常重要,這就是我們看到的動態代理的對象名前綴!
- private static final String proxyClassNamePrefix = "$Proxy";
- @Override
- public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
- Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
- // 一些狀態校驗
- // 計數器,該計數器記錄了當前已經實例化多少個代理對象
- long num = nextUniqueNumber.getAndIncrement();
- // 動態代理對象名拼接!包名 + "$Proxy" + 數字
- String proxyName = proxyPkg + proxyClassNamePrefix + num;
- // 生成字節碼文件,返回一個字節數組
- byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
- proxyName, interfaces, accessFlags);
- try {
- // 利用字節碼文件創建該字節碼的 Class 類對象
- return defineClass0(loader, proxyName,
- proxyClassFile, 0, proxyClassFile.length);
- } catch (ClassFormatError e) {
- throw new IllegalArgumentException(e.toString());
- }
- }
- }
apply() 方法中注意有「兩個非常重要的方法」:
- 「ProxyGenerator.generateProxyClass()」:它是生成字節碼文件的方法,它返回了一個字節數組,字節碼文件本質上就是一個字節數組,所以 proxyClassFile數組就是一個字節碼文件
- 「defineClass0()」:生成字節碼文件的 Class 對象,它是一個 native 本地方法,調用操作系統底層的方法創建類對象
而 proxyName 是代理對象的名字,我們可以看到它利用了「proxyClassNamePrefix + 計數器」 拼接成一個新的名字。所以在 DEBUG 時,停留在代理對象變量上,你會發現變量名是$Proxy0。
到了這里,源碼分析完了,是不是感覺被掏空了?哈哈哈哈,其實我當時也有這種感覺,不過現在你也感覺到,JDK 的動態代理其實并不是特別復雜吧(只要你有毅力)
CGLIB
CGLIB(Code generation Library) 不是 JDK 自帶的動態代理,它需要導入第三方依賴,它是一個字節碼生成類庫,能夠在運行時動態生成代理類對 「Java類 和 Java接口」 擴展。
CGLIB不僅能夠為 Java接口 做代理,而且「能夠為普通的 Java類 做代理」,而 JDK Proxy 「只能為實現了接口」的 Java類 做代理,所以 CGLIB 為 Java 的代理做了很好的擴展。「如果需要代理的類沒有實現接口,可以選擇 Cglib 作為實現動態代理的工具。」
廢話太多,一句話概括:「CGLIB 可以代理沒有實現接口的 Java 類」
下面我們來學習它的使用方法,以「小明找代理工廠買法國香水」這個故事背景為例子。
(1)導入依賴
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib-nodep</artifactId>
- <version>3.3.0</version>
- <scope>test</scope>
- </dependency>
❝還有另外一個 CGLIB 包,二者的區別是帶有-nodep的依賴內部已經包括了ASM字節碼框架的相關代碼,無需額外依賴ASM❞
(2)CGLIB 代理中有兩個核心的類:MethodInterceptor接口 和 Enhancer類,前者是實現一個代理工廠的根接口,后者是創建動態代理對象的類,在這里我再貼一次故事的結構圖,幫助你們理解。
首先我們來定義代理工廠SellProxyFactory。
- public class SellProxyFactory implements MethodInterceptor {
- // 關聯真實對象,控制對真實對象的訪問
- private Object realObject;
- /** 從代理工廠中獲取一個代理對象實例,等價于創建小紅代理 */
- public Object getProxyInstance(Object realObject) {
- this.realObject = realObject;
- Enhancer enhancer = new Enhancer();
- // 設置需要增強類的類加載器
- enhancer.setClassLoader(realObject.getClass().getClassLoader());
- // 設置被代理類,真實對象
- enhancer.setSuperclass(realObject.getClass());
- // 設置方法攔截器,代理工廠
- enhancer.setCallback(this);
- // 創建代理類
- return enhancer.create();
- }
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- doSomethingBefore(); // 前置增強
- Object object = methodProxy.invokeSuper(o, objects);
- doSomethingAfter(); // 后置增強
- return object;
- }
- private void doSomethingBefore() {
- System.out.println("執行方法前額外的操作...");
- }
- private void doSomethingAfter() {
- System.out.println("執行方法后額外的操作...");
- }
- }
intercept() 方法涉及到 4 個參數:
- Object o:被代理對象
- Method method:被攔截的方法
- Object[] objects:被攔截方法的所有入參值
- MethodProxy methodProxy:方法代理,用于調用原始的方法
對于 methodProxy 參數調用的方法,在其內部有兩種選擇:invoke() 和 invokeSuper() ,二者的區別不在本文展開說明,感興趣的讀者可以參考本篇文章:Cglib源碼分析 invoke和invokeSuper的差別
在 getInstance() 方法中,利用 Enhancer 類實例化代理對象(可以看作是小紅)返回給調用者小明,即可完成代理操作。
- public class XiaoMing {
- public static void main(String[] args) {
- SellProxyFactory sellProxyFactory = new SellProxyFactory();
- // 獲取一個代理實例
- SellPerfumeFactory proxyInstance =
- (SellPerfumeFactory) sellProxyFactory.getProxyInstance(new SellPerfumeFactory());
- // 創建代理類
- proxyInstance.sellPerfume(1999.99);
- }
- }
我們關注點依舊放在可擴展性和可維護性上,Cglib 依舊符合「開閉原則」,如果小明需要小紅代理購買紅酒,該如何做呢?這里礙于篇幅原因,我不再將完整的代碼貼出來了,可以自己試著手動實現一下,或者在心里有一個大概的實現思路即可。
我們來總結一下 CGLIB 動態代理:
(1)CGLIB 的使用方法:
- 代理工廠需要「實現 MethodInterceptor 接口」,并重寫方法,「內部關聯真實對象」,控制第三者對真實對象的訪問;代理工廠內部暴露 getInstance(Object realObject) 方法,「用于從代理工廠中獲取一個代理對象實例」。
- Enhancer 類用于從代理工廠中實例化一個代理對象,給調用者提供代理服務。
JDK Proxy 和 CGLIB 的對比
(2)仔細對比一下,JDK Proxy 和 CGLIB 具有相似之處:
JDK Proxy | CGLIB | |
---|---|---|
代理工廠實現接口 | InvocationHandler | MethodInterceptor |
構造代理對象給 Client 服務 | Proxy | Enhancer |
二者都是用到了兩個核心的類,它們也有不同:
- 最明顯的不同:CGLIB 可以代理「大部分類」(第二點說到);而 JDK Proxy 「僅能夠代理實現了接口的類」
- CGLIB 采用動態創建被代理類的子類實現方法攔截,子類內部重寫被攔截的方法,所以 CGLIB 不能代理被 final 關鍵字修飾的類和方法
細心的讀者會發現,講的東西都是「淺嘗輒止」(你都沒有給我講源碼,水文實錘),動態代理的精髓在于「程序在運行時動態生成代理類對象,攔截調用方法,在調用方法前后擴展額外的功能」,而生成動態代理對象的原理就是「反射機制」,在上一篇文章中,我詳細講到了如何利用反射實例化對象,調用方法······在代理中運用得淋漓盡致,所以反射和代理也是天生的一對,談到其中一個,必然會涉及另外一個。
動態代理的實際應用
傳統的 OOP 編程符合從上往下的編碼關系,卻不符合從左往右的編碼關系,如果你看不懂,可以參考下面的動圖,OOP 滿足我們一個方法一個方法從上往下地執行,但是卻不能「從左往右嵌入代碼」,而 AOP 的出現很好地彌補了這一點,它「允許我們將重復的代碼邏輯抽取出來形成一個單獨的覆蓋層」,在執行代碼時可以將該覆蓋層毫無知覺的嵌入到原代碼邏輯里面去。
Spring AOP
如下圖所示,method1 和 method2 都需要在方法執行前后「記錄日志」,實際上會有更多的方法需要記錄日志,傳統的 OOP 只能夠讓我們在每個方法前后手動記錄日志,大量的Log.info存在于方法內部,導致代碼閱讀性下降,方法內部無法專注于自己的邏輯。
「AOP 可以將這些重復性的代碼包裝到額外的一層,監聽方法的執行,當方法被調用時,通用的日志記錄層會攔截掉該方法,在該方法調用前后記錄日志,這樣可以讓方法專注于自己的業務邏輯而無需關注其它不必要的信息。」
2
Spring AOP 有許多功能:提供緩存、提供日志環繞、事務處理······在這里,我會以「事務」作為例子向你講解 Spring 底層是如何使用動態代理的。
Spring 的事務涉及到一個核心注解@Transactional,相信很多人在項目中都用到過,加上這個注解之后,在執行方法時如果發生異常,該方法內所有的事務都回滾,否則全部提交生效,這是最宏觀的表現,它內部是如何實現的呢?今天就來簡單分析一下。
每個有關數據庫的操作都要保證一個事務內的所有操作,要么全部執行成功,要么全部執行失敗,傳統的事務失敗回滾和成功提交是使用try...catch代碼塊完成的
- SqlSession session = null;
- try{
- session = getSqlSessionFactory().openSession(false);
- session.update("...", new Object());
- // 事務提交
- session.commit();
- }catch(Exception e){
- // 事務回滾
- session.rollback();
- throw e;
- }finally{
- // 關閉事務
- session.close();
- }
如果多個方法都需要寫這一段邏輯非常冗余,所以 Spring 給我們封裝了一個注解 @Transactional,使用它后,調用方法時會監視方法,如果方法上含有該注解,就會自動幫我們把數據庫相關操作的代碼包裹起來,最終形成類似于上面的一段代碼原理,當然這里并不準確,只是給你們一個大概的總覽,了解Spring AOP 的本質在干什么,這篇文章講解到這里,知識量應該也非常多了,好好消化上面的知識點,為后面的 Spring AOP 專題學習打下堅實的基礎。
本文轉載自微信公眾號「 Java建設者」,可以通過以下二維碼關注。轉載本文請聯系 Java建設者公眾號。