Java基礎--動態代理是基於什麼原理?

1. 動態代理

反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至可以運行時修改類定義。

動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 調用、面向切面的編程(AOP)。

實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其他的實現方式,比如利用傳說中更高性能的字節碼操作機制,類似 ASM、cglib(基於 ASM)、Javassist 等。

2.反射機制及其演進

對於 Java 語言的反射機制本身,如果你去看一下 java.lang 或 java.lang.reflect 包下的相關抽象,就會有一個很直觀的印象了。Class、Field、Method、Constructor 等,這些完全就是我們去操作類和對象的元數據對應。

就是反射提供的 AccessibleObject.setAccessible​(boolean flag)。它的子類也大都重寫了這個方法,這裏的所謂 accessible 可以理解成修飾成員的 public、protected、private,這意味着我們可以在運行時修改成員訪問限制!

setAccessible 的應用場景非常普遍,遍佈我們的日常開發、測試、依賴注入等各種框架中。比如,在 O/R Mapping 框架中,我們爲一個 Java 實體對象,運行時自動生成 setter、getter 的邏輯,這是加載或者持久化數據非常必要的,框架通常可以利用反射做這個事情,而不需要開發者手動寫類似的重複代碼。

另一個典型場景就是繞過 API 訪問控制。我們日常開發時可能被迫要調用內部 API 去做些事情,比如,自定義的高性能 NIO 框架需要顯式地釋放 DirectBuffer,使用反射繞開限制是一種常見辦法。

但是,在 Java 9 以後,這個方法的使用可能會存在一些爭議,因爲 Jigsaw 項目新增的模塊化系統,出於強封裝性的考慮,對反射訪問進行了限制。Jigsaw 引入了所謂 Open 的概念,只有當被反射操作的模塊和指定的包對反射調用者模塊 Open,才能使用 setAccessible;否則,被認爲是不合法(illegal)操作。如果我們的實體類是定義在模塊裏面,我們需要在模塊描述符中明確聲明:


module MyEntities {
    // Open for reflection
    opens com.mycorp to java.persistence;
}

反射最大的作用之一就在於我們可以不在編譯時知道某個對象的類型,而在運行時通過提供完整的”包名+類名.class”得到。注意:不是在編譯時,而是在運行時。

功能:
•在運行時能判斷任意一個對象所屬的類。
•在運行時能構造任意一個類的對象。
•在運行時判斷任意一個類所具有的成員變量和方法。
•在運行時調用任意一個對象的方法。
說大白話就是,利用Java反射機制我們可以加載一個運行時才得知名稱的class,獲悉其構造方法,並生成其對象實體,能對其fields設值並喚起其methods。

應用場景:
反射技術常用在各類通用框架開發中。因爲爲了保證框架的通用性,需要根據配置文件加載不同的對象或類,並調用不同的方法,這個時候就會用到反射——運行時動態加載需要加載的對象。

特點:
由於反射會額外消耗一定的系統資源,因此如果不需要動態地創建一個對象,那麼就不需要用反射。另外,反射調用方法時可以忽略權限檢查,因此可能會破壞封裝性而導致安全問題。

3. 動態代理

首先,它是一個代理機制。如果熟悉設計模式中的代理模式,我們會知道,代理可以看作是對調用目標的一個包裝,這樣我們對目標代碼的調用不是直接發生的,而是通過代理完成。其實很多動態代理場景,我認爲也可以看作是裝飾器(Decorator)模式的應用,我會在後面的專欄設計模式主題予以補充。

通過代理可以讓調用者與實現者之間解耦。比如進行 RPC 調用,框架內部的尋址、序列化、反序列化等,對於調用者往往是沒有太大意義的,通過代理,可以提供更加友善的界面。

代理的發展經歷了靜態到動態的過程,源於靜態代理引入的額外工作。類似早期的 RMI 之類古董技術,還需要 rmic 之類工具生成靜態 stub 等各種文件,增加了很多繁瑣的準備工作,而這又和我們的業務邏輯沒有關係。利用動態代理機制,相應的 stub 等類,可以在運行時生成,對應的調用操作也是動態完成,極大地提高了我們的生產力。改進後的 RMI 已經不再需要手動去準備這些了,雖然它仍然是相對古老落後的技術,未來也許會逐步被移除。

jdk動態代理的一個小demo
1.創建一個接口(jdk動態代理必須實現接口)

public interface Hello {

    void sayHello();

    String getHello();

    void sayHelloTo(String name);

    String helloWith(String name);

}

2.創建接口的實現

public class HelloImpl implements Hello {
    @Override
    public void sayHello() {
        System.out.println("hello for hello impl");
    }

    @Override
    public String getHello() {
        return "hello for hello impl";
    }

    @Override
    public void sayHelloTo(String name) {
        System.out.println("hello for hello impl , " + name);
    }

    @Override
    public String helloWith(String name) {
        return "hello for hello impl, " + name;
    }
}

3.創建代理類

public class MyProxy<T> implements InvocationHandler {

    private T target;

    public MyProxy(T target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("target class is " + target.getClass() + ", method is " + method.getName() +
                ", before, args is " + (args == null ? "" : args.toString()));
        Object result = method.invoke(target, args);
        System.out.println("target class is " + target.getClass() + ", method result is " + (result == null ? "" : result.toString()));
        return result;
    }
}

使用泛型,在其構造函數中初始化目標類對象。
然後在invoke方法中實現方法環繞日誌打印
4.Main方法調用

public class ProxyMain {

    public static void main(String[] args){
        Hello hello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(),
                HelloImpl.class.getInterfaces(), new MyProxy<Hello>(new HelloImpl()));
        hello.sayHello();
        System.out.println();
        hello.sayHelloTo("小美");
        System.out.println();
        System.out.println(hello.getHello());
        System.out.println();
        System.out.println(hello.helloWith("小美"));
    }

}

5.輸出

F:\JDK\jdk\jdk8u111\bin\java.exe "-javaagent:F:\idea\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=55941:F:\idea\IntelliJ IDEA 2019.1.3\bin" -Dfile.encoding=UTF-8 -classpath F:\JDK\jdk\jdk8u111\jre\lib\charsets.jar;F:\JDK\jdk\jdk8u111\jre\lib\deploy.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\access-bridge-64.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\cldrdata.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\dnsns.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\jaccess.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\jfxrt.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\localedata.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\nashorn.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\sunec.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\sunjce_provider.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\sunmscapi.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\sunpkcs11.jar;F:\JDK\jdk\jdk8u111\jre\lib\ext\zipfs.jar;F:\JDK\jdk\jdk8u111\jre\lib\javaws.jar;F:\JDK\jdk\jdk8u111\jre\lib\jce.jar;F:\JDK\jdk\jdk8u111\jre\lib\jfr.jar;F:\JDK\jdk\jdk8u111\jre\lib\jfxswt.jar;F:\JDK\jdk\jdk8u111\jre\lib\jsse.jar;F:\JDK\jdk\jdk8u111\jre\lib\management-agent.jar;F:\JDK\jdk\jdk8u111\jre\lib\plugin.jar;F:\JDK\jdk\jdk8u111\jre\lib\resources.jar;F:\JDK\jdk\jdk8u111\jre\lib\rt.jar;G:\studyjdk\out\production\study com.study.com.study.myporxy.ProxyMain
target class is class com.study.com.study.myporxy.HelloImpl, method is sayHello, before, args is 
hello for hello impl
target class is class com.study.com.study.myporxy.HelloImpl, method result is 

target class is class com.study.com.study.myporxy.HelloImpl, method is sayHelloTo, before, args is [Ljava.lang.Object;@12a3a380
hello for hello impl , 小美
target class is class com.study.com.study.myporxy.HelloImpl, method result is 

target class is class com.study.com.study.myporxy.HelloImpl, method is getHello, before, args is 
target class is class com.study.com.study.myporxy.HelloImpl, method result is hello for hello impl
hello for hello impl

target class is class com.study.com.study.myporxy.HelloImpl, method is helloWith, before, args is [Ljava.lang.Object;@29453f44
target class is class com.study.com.study.myporxy.HelloImpl, method result is hello for hello impl, 小美
hello for hello impl, 小美

Process finished with exit code 0

從 API 設計和實現的角度,這種實現仍然有侷限性,因爲它是以接口爲中心的,相當於添加了一種對於被調用者沒有太大意義的限制。我們實例化的是 Proxy 對象,而不是真正的被調用類型,這在實踐中還是可能帶來各種不便和能力退化。

如果被調用者沒有實現接口,而我們還是希望利用動態代理機制,那麼可以考慮其他方式。我們知道 Spring AOP 支持兩種模式的動態代理,JDK Proxy 或者 cglib,如果我們選擇 cglib 方式,你會發現對接口的依賴被克服了。

JDK Proxy 的優勢:

  • 最小化依賴關係,減少依賴意味着簡化開發和維護,JDK 本身的支持,可能比 cglib 更加可靠。
  • 平滑進行 JDK 版本升級,而字節碼類庫通常需要進行更新以保證在新版 Java 上能夠使用。
  • 代碼實現簡單。

基於類似 cglib 框架的優勢:

  • 有的時候調用目標可能不便實現額外接口,從某種角度看,限定調用者實現接口是有些侵入性的實踐,類似 cglib 動態代理就沒有這種限制。
  • 只操作我們關心的類,而不必爲其他相關類增加工作量。

有人曾經得出結論說 JDK Proxy 比 cglib 或者 Javassist 慢幾十倍。坦白說,不去爭論具體的 benchmark 細節,在主流 JDK 版本中,JDK Proxy 在典型場景可以提供對等的性能水平,數量級的差距基本上不是廣泛存在的。而且,反射機制性能在現代 JDK 中,自身已經得到了極大的改進和優化,同時,JDK 很多功能也不完全是反射,同樣使用了 ASM 進行字節碼操作。

動態代理應用非常廣泛,雖然最初多是因爲 RPC 等使用進入我們視線,但是動態代理的使用場景遠遠不僅如此,它完美符合 Spring AOP 等切面編程。我在後面的專欄還會進一步詳細分析 AOP 的目的和能力。簡單來說它可以看作是對 OOP 的一個補充,因爲 OOP 對於跨越不同對象或類的分散、糾纏邏輯表現力不夠,比如在不同模塊的特定階段做一些事情,類似日誌、用戶鑑權、全局性異常處理、性能監控,甚至事務處理等。

AOP 通過(動態)代理機制可以讓開發者從這些繁瑣事項中抽身出來,大幅度提高了代碼的抽象程度和複用度。從邏輯上來說,我們在軟件設計和實現中的類似代理,如 Facade、Observer 等很多設計目的,都可以通過動態代理優雅地實現。

爲其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在兩者之間起到中介的作用(可類比房屋中介,房東委託中介銷售房屋、簽訂合同等)。
所謂動態代理,就是實現階段不用關心代理誰,而是在運行階段才指定代理哪個一個對象(不確定性)。如果是自己寫代理類的方式就是靜態代理(確定性)。

組成要素:
(動態)代理模式主要涉及三個要素:
其一:抽象類接口
其二:被代理類(具體實現抽象接口的類)
其三:動態代理類:實際調用被代理類的方法和屬性的類

實現方式:
實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了反射機制。還有其他的實現方式,比如利用字節碼操作機制,類似 ASM、CGLIB(基於 ASM)、Javassist 等。
舉例,常可採用的JDK提供的動態代理接口InvocationHandler來實現動態代理類。其中invoke方法是該接口定義必須實現的,它完成對真實方法的調用。通過InvocationHandler接口,所有方法都由該Handler來進行處理,即所有被代理的方法都由InvocationHandler接管實際的處理任務。此外,我們常可以在invoke方法實現中增加自定義的邏輯實現,實現對被代理類的業務邏輯無侵入。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章