Java 代理模式與 AOP

jaychen發表於2017-09-15

本文首發於 jaychen.cc
作者 jaychen

最近在學 Spring,研究了下 AOP 和代理模式,寫點心得和大家分享下。

AOP

先說下AOP,AOP 全稱 Aspect Oriented Programming,面向切面程式設計,和 OOP 一樣也是一種程式設計思想。AOP 出現的原因是為了解決 OOP 在處理 侵入性業務上的不足。

那麼,什麼是侵入性業務?類似日誌統計、效能分析等就屬於侵入性業務。本來原本的業務邏輯程式碼優雅大氣,正常執行,突然說需要在這段邏輯裡面加上效能分析,於是程式碼就變成了下面這個樣子


long begin = System.currentTimeMillis(); 

// 原本的業務
doSomething();

long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("執行花費 :" + step);複製程式碼

從上面的程式碼看到,效能分析的業務程式碼和原本的業務程式碼混在了一起,好端端的程式碼就這麼被糟蹋了。所以,侵入性業務必須有一個更好的解決方案,這個解決方案就是 AOP。

那麼,AOP 是如何解決這類問題?

代理模式

通常,我們會使用代理模式來實現 AOP,這就意味著代理模式可以優雅的解決侵入性業務問題。所以下面來重點分析下代理模式。

這個是代理模式的類圖。很多人可能看不懂類圖,但是說實話有時候一圖勝千言,這裡稍微解釋下類圖的含義,尤其是類圖中存在的幾種連線符。

  • 矩形代表一個類,矩形內部的資訊有:類名,屬性和方法。
  • 虛線 + 三角空心箭頭為 is=a 的關係,表示繼承,所以上圖中 TestSQLPerformance 都實現 IDatabase 介面。
  • 實線 + 箭頭為關聯關係,一般在程式碼中以成員變數的形式體現,所以上圖中 Performance 類有一個 TestSQL 的成員變數。

有了類圖,我們可以根據類圖直接寫出代理模式的程式碼了。這裡代理模式分為靜態代理和動態代理兩種,我們分別來看下。

靜態代理

假設一個場景,我們需要測試一條 sql query 執行所花費的時間。

如果按照普通的方式,程式碼邏輯應該如下

long begin = System.currentTimeMillis(); 

query();

long end = System.currentTimeMillis();
long step = end - begin;
System.out.println("執行花費 :" + step);複製程式碼

上面說過了,這種會導致查詢邏輯和效能測試邏輯混淆在一塊,那麼來看看使用代理模式是如何解決這個問題的。

代理模式,代理,意味著有一方代替另一方完成一件事。這裡,我們會編寫兩個類:TestSQL 為query 執行邏輯,Performance 為效能測試類。這裡 Performance 會代替 TestSQL 去執行 query 邏輯。

要想 Performance 能夠代替 TestSQL 執行 query 邏輯,那麼這兩個類應該是有血緣關係的,即這兩個必須實現同一個介面。

// 介面
public interface IDatabase {
    void query();
}


public class TestSQL implements IDatabase {

    @Override
    public void query() {
        System.out.println("執行 query。。。。");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


// 代理類
public class PerformanceMonitor implements IDatabase {
    TestSQL sql;

    public PerformanceMonitor(TestSQL sql) {
        this.sql = sql;
    }


    @Override
    public void query() {
        long begin = System.currentTimeMillis();

        // 業務邏輯。
        sql.query();

        long end = System.currentTimeMillis();
        long step = end - begin;
        System.out.println("執行花費 : " + step);
    }
}


// 測試程式碼
public class Main {
    public static void main(String[] strings) {
        TestSQL sql = new TestSQL();

        PerformanceMonitor performanceMonitor = new PerformanceMonitor(sql);
        // 由 Performance 代替 testSQL 執行
        performanceMonitor.query();
    }
}複製程式碼

從上面的示例程式碼可以分析出來代理模式是如何運作的,這裡我們可以很明顯看出代理模式的優越性,TestSQL 的邏輯很純粹,沒有混入其他無關的業務程式碼。

動態代理

回顧靜態代理的程式碼,發現代理類 Performance 必須實現 IDatabase 介面。如果有很多業務需要用到代理來實現,那麼每個業務都需要定義一個代理類,這會導致類迅速膨脹,為了避免這點,Java 提供了動態代理。

為何稱之為動態代理,動態代理底層是使用反射實現的,是在程式執行期間動態的建立介面的實現。在靜態代理中,我們需要在編碼的時候編寫 Performance 類實現 IDatabase 介面。而使用動態代理,我們不必編寫 Performance 實現 IDatabase 介面,而是 JDK 在底層通過反射技術動態建立一個 IDatabase 介面的實現。

使用動態代理需要使用到 InvocationHandlerProxy 這兩個類。

// 代理類,不再實現 IDatabase 介面,而是實現 InvocationHandler 介面
public class Performance implements InvocationHandler {

    private TestSQL sql;

    public Performance(TestSQL sql) {
        this.sql = sql;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long begin = System.currentTimeMillis();

        // method.invoke 實際上就是呼叫 sql.query()
        Object object = method.invoke(sql, args);

        long end = System.currentTimeMillis();
        long step = end - begin;
        System.out.println("執行花費 :" + step);
        return object;
    }
}



public class Main {
    public static void main(String[] strings) {
        TestSQL sql = new TestSQL();
        Performance performance = new Performance(sql);

        IDatabase proxy = (IDatabase) Proxy.newProxyInstance(
                sql.getClass().getClassLoader(),
                sql.getClass().getInterfaces(),
                performance
        );
        proxy.query();
    }
}複製程式碼

先來看看 newProxyInstance 函式,這個函式的作用就是用來動態建立一個代理物件的類,這個函式需要三個引數:

  • 第一個引數為類載入器,如果不懂是什麼玩意,先套著模板寫,等我寫下一篇文章拯救你。
  • 第二個引數為要代理的介面,在這個例子裡面就是 IDatabase 介面。
  • 第三個引數為實現 InvocationHandler 介面的物件。

執行 newProxyInstance 之後,Java 會在底層自動生成一個代理類,其程式碼大概如下:

public final class $Proxy1 extends Proxy implements IDatabase{
    private InvocationHandler h;

    private $Proxy1(){}

    public $Proxy1(InvocationHandler h){
        this.h = h;
    }

    public void query(){
        ////建立method物件
        Method method = Subject.class.getMethod("query");
        //呼叫了invoke方法
        h.invoke(this, method, new Object[]{}); 
    }
}複製程式碼

你會發現,這個類很像在靜態代理中的 Performance 類,是的,動態代理其本質是 Java 自動為我們生成了一個 $Proxy1 代理類。在 mian 函式中 newProxyInstance 的返回值就是該類的一個例項。並且,$Proxy1 中的 h 屬性就是 newProxyInstance 的第三個引數。所以,當我們在 main 函式中執行 proxy.query(),實際上是呼叫 $proxy1#query 方法,進而再呼叫 Performance#invoke 方法。而在 Performance#invoke 通過 Object object = method.invoke(sql, args); 呼叫了 TestSQL#query 方法。

回顧上面的流程,理解動態代理的核心在於理解 Java 自動生成的代理類。這裡還有一點要說明,JDK 的動態代理有一個不足:它只能為介面建立代理例項。這句話體現在程式碼上就是 newProxyInstance 的第二個引數是一個介面陣列。為什麼會存在這個不足?其實看 $Proxy1 代理類就知道了,這個由 JDK 生成的代理類需要繼承 Proxy 類,而 Java 只支援單繼承,所以就限制了 JDK 的動態代理只能為介面建立代理。

相關文章