代理?哦,就是那個賺差價中間商啊!

程式設計師cxuan發表於2020-08-26

說在前面:今天我們來聊一聊 Java 中的代理,先來聊聊故事背景:

小明想購買法國某個牌子的香水送給女朋友,但是在國內沒有貨源售賣,親自去法國又大費周章了,而小紅現在正在法國玩耍,她和小明是好朋友,可以幫小明買到這個牌子的香水,於是小明就找到小紅,答應給她多加 5% 的辛苦費,小紅答應了,小明成功在中國買到了法國的香水。之後小紅開啟了瘋狂的代購模式,賺到了很多手續費。

在故事中,小明是一個客戶,它讓小紅幫忙購買香水,小紅就成了一個代理物件,而香水提供商是一個真實的物件,可以售賣香水,小明通過代理商小紅,購買到法國的香水,這就是一個代購的例子。我畫了一幅圖幫助理解這個故事的整個結構。

這個故事是最典型的代理模式,代購從供應商購買貨物後返回給呼叫者,也就是需要代理的小明。

代理可以分為靜態代理動態代理兩大類:

靜態代理

  • 優點:程式碼結構簡單,較容易實現
  • 缺點:無法適配所有代理場景,如果有新的需求,需要修改代理類,不符合軟體工程的開閉原則

小紅現在只是代理香水,如果小明需要找小紅買法國紅酒,那小紅就需要代理法國紅酒了,但是靜態代理去擴充套件代理功能必須修改小紅內部的邏輯,這會讓小紅內部程式碼越來越臃腫,後面會詳細分析。

動態代理

  • 優點:能夠動態適配特定的代理場景,擴充套件性較好,符合軟體工程的開閉原則
  • 缺點:動態代理需要利用到反射機制和動態生成位元組碼,導致其效能會比靜態代理稍差一些,但是相比於優點,這些劣勢幾乎可以忽略不計

如果小明需要找小紅代理紅酒,我們無需修改代理類小紅的內部邏輯,只需要關注擴充套件的功能點:代理紅酒,例項化新的類,通過一些轉換即可讓小紅既能夠代理香水也能夠代理紅酒了。

本文將會通過以下幾點,儘可能讓你理解 Java 代理中所有重要的知識點:

  1. 學習代理模式(實現故事的程式碼,解釋代理模式的類結構特點)
  2. 比較靜態代理與動態代理二者的異同
  3. Java 中常見的兩種動態代理實現(JDK Proxy 和 Cglib)
  4. 動態代理的應用(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);
    }
}

我們來看看執行結果,小紅在向小明售賣香水前可以執行額外的其它操作,如果良心點的代購就會打折、包郵···,如果黑心點的代購就會加手續費、售出不退還···,是不是很刺激。

image-20200819212607989

我們來看看上面 4 個類組成的類圖關係結構,可以發現小紅香奈兒提供商都實現了售賣香水這一介面,而小紅內部增加了對提供商的引用,用於呼叫提供商的售賣香水功能。

image-20200819214058888

實現代理模式,需要走以下幾個步驟:

  • 定義真實物件和代理物件的公共介面(售賣香水介面)
  • 代理物件內部儲存對真實目標物件的引用(小紅引用提供商)
  • 訪問者僅能通過代理物件訪問真實目標物件,不可直接訪問目標物件(小明只能通過小紅去購買香水,不能直接到香奈兒提供商購買)

代理模式很容易產生錯誤思維的一個地方:代理物件並不是真正提供服務的一個物件,它只是替訪問者訪問目標物件的一個中間人,真正提供服務的還是目標物件,而代理物件的作用就是在目標物件提供服務之前和之後能夠執行額外的邏輯。

從故事來說,小紅並不是真正賣香水的,賣香水的還是香奈兒提供商,而小紅只不過是在讓香奈兒賣香水之前和之後執行了一些自己額外加上去的操作。

講完這個代理模式的程式碼實現,我們來系統地學習它究竟是如何定義的,以及實現它需要注意什麼規範。

代理模式的定義:給目標物件提供一個代理物件,代理物件包含該目標物件,並控制對該目標物件的訪問。

代理模式的目的:

  • 通過代理物件的隔離,可以在對目標物件訪問前後增加額外的業務邏輯,實現功能增強。
  • 通過代理物件訪問目標物件,可以防止系統大量地直接對目標物件進行不正確地訪問,出現不可預測的後果

靜態代理與動態代理

你是否會有我一樣的疑惑:代理為什麼還要分靜態和動態的?它們兩個有啥不同嗎?

很明顯,所有人都會有這樣的疑惑,我們先來看看它們的相同點:

  • 都能夠實現代理模式(這不廢話嗎...)
  • 無論是靜態代理還是動態代理,代理物件和目標物件都需要實現一個公共介面

重點當然是它們的不同之處,動態代理在靜態代理的基礎上做了改進,極大地提高了程式的可維護性可擴充套件性。我先列出它們倆的不同之處,再詳細解釋為何靜態代理不具備這兩個特性:

  • 動態代理產生代理物件的時機是執行時動態生成,它沒有 Java 原始檔,直接生成位元組碼檔案例項化代理物件;而靜態代理的代理物件,在程式編譯時已經寫好 Java 檔案了,直接 new 一個代理物件即可。
  • 動態代理比靜態代理更加穩健,對程式的可維護性和可擴充套件性更加友好

目前來看,代理物件小紅已經能夠代理購買香水了,但有一天,小紅的另外一個朋友小何來了,他想購買最純正的法國紅酒,國內沒有這樣的購買渠道,小紅剛巧也在法國,於是小何就想找小紅幫他買紅酒啦,這和小明找小紅是一個道理的,都是想讓小紅做代理。

但問題是:在程式中,小紅只能代理購買香水,如果要代理購買紅酒,要怎麼做呢?

  • 建立售賣紅酒的介面

  • 售賣紅酒提供商和代理物件小紅都需要實現該介面

  • 小何訪問小紅,讓小紅賣給他紅酒

image-20200820082820488

OK,事已至此,程式碼就不重複寫了,我們來探討一下,面對這種新增的場景,上面的這種實現方法有沒有什麼缺陷呢?

我們不得不提的是軟體工程中的開閉原則

開閉原則:在編寫程式的過程中,軟體的所有物件應該是對擴充套件是開放的,而對修改是關閉的

靜態代理違反了開閉原則,原因是:面對新的需求時,需要修改代理類,增加實現新的介面和方法,導致代理類越來越龐大,變得難以維護。

雖然說目前代理類只是實現了2個介面,如果日後小紅不只是代理售賣紅酒,還需要代理售賣電影票、代購日本壽司······實現的介面會變得越來越多,內部的結構變得越來越複雜,整個類顯得愈發臃腫,變得不可維護,之後的擴充套件也會成問題,只要任意一個介面有改動,就會牽扯到這個代理類,維護的代價很高。

所以,為了提高類的可擴充套件性和可維護性,滿足開閉原則,Java 提供了動態代理機制。

常見的動態代理實現

動態代理最重要的當然是動態兩個字,學習動態代理的過程,最重要的就是理解何為動態,話不多說,馬上開整。

我們來明確一點:動態代理解決的問題是面對新的需求時,不需要修改代理物件的程式碼,只需要新增介面和真實物件,在客戶端呼叫即可完成新的代理。

這樣做的目的:滿足軟體工程的開閉原則,提高類的可維護性和可擴充套件性。

JDK Proxy

JDK Proxy 是 JDK 提供的一個動態代理機制,它涉及到兩個核心類,分別是ProxyInvocationHandler,我們先來了解如何使用它們。

以小紅代理賣香水的故事為例,香奈兒香水提供商依舊是真實物件,實現了SellPerfume介面,這裡不再重新寫了,重點是小紅代理,這裡的代理物件不再是小紅一個人,而是一個代理工廠,裡面會有許多的代理物件。我畫了一幅圖,你看了之後會很好理解:

image-20200820085642788

小明來到代理工廠,需要購買一款法國在售的香奈兒香水,那麼工廠就會找一個可以實際的代理物件(動態例項化)分配給小明,例如小紅或者小花,讓該代理物件完成小明的需求。該代理工廠含有無窮無盡的代理物件可以分配,且每個物件可以代理的事情可以根據程式的變化而動態變化,無需修改代理工廠。

如果有一天小明需要招待一個可以代購紅酒的代理物件,該代理工廠依舊可以滿足他的需求,無論日後需要什麼代理,都可以滿足,是不是覺得很神奇?我們來學習如何使用它。

我們看一下動態代理的 UML 類圖結構長什麼樣子。

image-20200820090834069

可以看到和靜態代理區別不大,唯一的變動是代理物件,我做了標註:由代理工廠生產

這句話的意思是:代理物件是在程式執行過程中,由代理工廠動態生成,代理物件本身不存在 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);
    }
}

執行結果和靜態代理的結果相同,但二者的思想是不一樣的,一個是靜態,一個是動態。那又如何體現出動態代理的優勢呢?別急,往下看就知道了。

注意看下圖,相比靜態代理的前置增強和後置增強,少了小紅二字,實際上代理工廠分配的代理物件是隨機的,不會針對某一個具體的代理物件,所以每次生成的代理物件都不一樣,也就不確定是不是小紅了,但是能夠唯一確定的是,這個代理物件能和小紅一樣幫小明買到香水!

image-20200820104319179

按照之前的故事線發展,小紅去代理紅酒,而小明又想買法國的名牌紅酒,所以去找代理工廠,讓它再分配一個人幫小明買紅酒,代理工廠說:“當然沒問題!我們是專業的!等著!”

我們需要實現兩個類:紅酒提供商類 和 售賣紅酒介面。

/** 售賣紅酒介面 */
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);
    }
}

期待一下執行結果,你會很驚喜地發現,居然也能夠代理售賣紅酒了,但是我們沒有修改代理工廠

image-20200820105337247

回顧一下我們新增紅酒代理功能時,需要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

image-20200821110425359

到了這裡,原始碼分析完了,是不是感覺被掏空了?哈哈哈哈,其實我當時也有這種感覺,不過現在你也感覺到,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類,前者是實現一個代理工廠的根介面,後者是建立動態代理物件的類,在這裡我再貼一次故事的結構圖,幫助你們理解。

image-20200821113009611

首先我們來定義代理工廠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 專題學習打下堅實的基礎。

你好,我是 cxuan,我自己手寫了四本 PDF,分別是 Java基礎總結、HTTP 核心總結、計算機基礎知識,作業系統核心總結,我已經整理成為 PDF,可以關注公眾號 Java建設者 回覆 PDF 領取優質資料。

相關文章