歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> JDK動態代理

JDK動態代理

日期:2017/3/1 9:14:13   编辑:Linux編程

一、基本概念

1.什麼是代理?

在闡述JDK動態代理之前,我們很有必要先來弄明白代理的概念。代理這個詞本身並不是計算機專用術語,它是生活中一個常用的概念。這裡引用維基百科上的一句話對代理進行定義:

A proxy is an agent or substitute authorized to act for another person or a document which authorizes the agent so to act.

意思是說:代理指的是一個代理人(或替代品),它被授權代表另外一個人(或文檔)。

從這個簡明扼要的定義中,可以看出代理的一些特性:1.代理存在的意義就是代表另一個事物。2.代理至少需要完成(或實現)它所代表的事物的功能。

2.什麼是JAVA靜態代理?

JAVA靜態代理是指由程序員創建或工具生成的代理類,這個類在編譯期就已經是確定了的,存在的。

典型的靜態代理模式一般包含三類角色:

1.抽象角色:它的作用是定義一組行為規范。抽象角色一般呈現為接口(或抽象類),這些接口(或抽象類)中定義的方法就是待實現的。

2.真實角色:實現了抽象角色所定義的行為。真實角色就是個普通的類,它需要實現抽象角色定義的那些接口。

3.代理角色:代表真實角色的角色。根據上面代理的定義,我們可以知道代理角色需要至少完成(或實現)真實角色的功能。為了完成這一使命,那麼代理角色也需要實現抽象角色所定義的行為(即代理類需要實現抽象角色所定義的接口),並且在實現接口方法的時候需要調用真實角色的相應方法。

上圖使用UML類圖解釋了靜態代理的數據模型。

1.接口IFunc代表了抽象角色,定義了一個行為,即方法doSomething()。

2.類RealFunc代表了真實角色,它實現了IFunc接口中定義的方法doSomething()。

3.類ProxyFunc代表了代理角色,它實現了IFunc接口中定義的方法doSomething()。它的實現方式是依賴RealFunc類的,通過持有RealFunc類對象的引用realObj,在ProxyFunc.doSomething()方法中調用了realObj.doSomething()。當然,代理類也可以做一些其他的事情,如圖中的doOtherthing()。

通過上面的介紹,可以看出靜態代理存在以下問題:

1.代理類依賴於真實類,因為代理類最根本的業務功能是需要通過調用真實類來實現的。那麼如果事先不知道真實類,該如何使用代理模式呢?

2.一個真實類必須對應一個代理類,即當有多個真實類RealA、RealB、RealC...的時候,就需要多個代理類ProxyA、ProxyB、ProxyC...。這樣的話如果大量使用靜態代理,容易導致類的急劇膨脹。該如何解決?

要想解決上述問題,就需要使用下面講解的JAVA動態代理。

3.什麼是JAVA動態代理?

JAVA動態代理與靜態代理相對,靜態代理是在編譯期就已經確定代理類和真實類的關系,並且生成代理類的。而動態代理是在運行期利用JVM的反射機制生成代理類,這裡是直接生成類的字節碼,然後通過類加載器載入JAVA虛擬機執行。現在主流的JAVA動態代理技術的實現有兩種:一種是JDK自帶的,就是我們所說的JDK動態代理,另一種是開源社區的一個開源項目CGLIB。本文主要對JDK動態代理做討論。

4.什麼是JDK動態代理?

JDK動態代理的實現是在運行時,根據一組接口定義,使用Proxy、InvocationHandler等工具類去生成一個代理類和代理類實例。

JDK動態代理的類關系模型和靜態代理看起來差不多。也是需要一個或一組接口來定義行為規范。需要一個代理類來實現接口。區別是沒有真實類,因為動態代理就是要解決在不知道真實類的情況下依然能夠使用代理模式的問題。

圖中高亮顯示的$Proxy0即為JDK動態代理技術生成的代理類,類名的生成規則是前綴"$Proxy"加上一個序列數。這個類繼承Proxy,實現一系列的接口Intf1,Intf2...IntfN。

既然要實現接口,那麼就要實現接口的各個方法,即圖中的doSomething1(),doSomething2()...doSomethingN()。我們上面介紹靜態代理的時候,知道靜態代理類本質上是調用真實類去實現接口定義的方法的。但是JDK動態代理中是沒有真實類這樣的概念的。那麼JDK動態代理類是如何實現這些接口方法的具體邏輯呢?答案就在圖中的InvocationHandler上。$Proxy0對外只提供一個構造函數,這個構造函數接受一個InvocationHandler實例h,這個構造函數的邏輯非常簡單,就是調用父類的構造函數,將參數h賦值給對象字段h。最終就是把所有的方法實現都分派到InvocationHandler實例h的invoke方法上。所以JDK動態代理的接口方法實現邏輯是完全由InvocationHandler實例的invoke方法決定的。

二、樣例分析

了解了JDK動態代理的概念後,現在我們動手寫個JDK動態代理的代碼樣例。直觀的認識下JDK動態代理技術為我們的做了什麼。上面說到了,JDK動態代理主要依靠Proxy和InvocationHandler這兩個類來生成動態代理類和類的實例。這兩個類都在jdk的反射包java.lang.reflect下面。Proxy是個工具類,有了它就可以為接口生成動態代理類了。如果需要進一步生成代理類實例,需要注入InvocationHandler實例。這點我們上面解釋過,因為代理類最終邏輯的實現是分派給InvocationHandler實例的invoke方法的。

閒話休絮,先開始我們的第一步,定義一個接口。這個接口裡面定義一個方法helloWorld()。

1 public interface MyIntf {
2     void helloWorld();
3 }

第二步,編寫一個我們自己的調用處理類,這個類需要實現InvocationHandler接口。

1 public class MyInvocationHandler implements InvocationHandler {
2     @Override
3     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
4         System.out.println(method);
5         return null;
6     }
7 }

InvocationHandler接口只有一個待實現的invoke方法。這個方法有三個參數,proxy表示動態代理類實例,method表示調用的方法,args表示調用方法的參數。在實際應用中,invoke方法就是我們實現業務邏輯的入口。這裡我們的實現邏輯就一行代碼,打印當前調用的方法(實際應用中不會這麼做),雖然實現簡單,但是不影響我們解釋JDK動態代理的技術。

第三步,直接使用Proxy提供的方法創建一個動態代理類實例。並調用代理類實例的helloWorld方法,檢測運行結果。

1 public class ProxyTest {
2     public static void main(String[] args) {
3         System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
4         MyIntf proxyObj = (MyIntf)Proxy.newProxyInstance(MyIntf.class.getClassLoader(),new Class[]{MyIntf.class},new MyInvocationHandler());
5         proxyObj.helloWorld();
6     }
7 }

第三行代碼是設置系統屬性,把生成的代理類寫入到文件。這裡再強調一下,JDK動態代理技術是在運行時直接生成類的字節碼,並載入到虛擬機執行的。這裡不存在class文件的,所以我們通過設置系統屬性,把生成的字節碼保存到文件,用於後面進一步分析。

第四行代碼就是調用Proxy.newProxyInstance方法創建一個動態代理類實例,這個方法需要傳入三個參數,第一個參數是類加載器,用於加載這個代理類。第二個參數是Class數組,裡面存放的是待實現的接口信息。第三個參數是InvocationHandler實例。

第五行調用代理類的helloWorld方法,運行結果:

public abstract void com.tuniu.distribute.openapi.common.annotation.MyIntf.helloWorld()

分析運行結果,就可以發現,方法的最終調用是分派到了MyInvocationHandler.invoke方法,打印出了調用的方法信息。

到這裡,對於JDK動態代理的基本使用就算講完了。我們做的事情很少,只是編寫了接口MyIntf和調用處理類MyInvocationHandler。其他大部分的工作都是Proxy工具類幫我們完成的。Proxy幫我們創建了動態代理類和代理類實例。上面的代碼我們設置了系統屬性,把生成的字節碼保存到class文件。下面我們通過反編譯軟件(如jd-gui),看下Proxy類為我們生成的代理類是什麼樣子的。

 1 package com.sun.proxy;
 2 import com.tuniu.distribute.openapi.common.annotation.MyIntf;
 3 import java.lang.reflect.InvocationHandler;
 4 import java.lang.reflect.Method;
 5 import java.lang.reflect.Proxy;
 6 import java.lang.reflect.UndeclaredThrowableException;
 7 
 8 public final class $Proxy0 extends Proxy implements MyIntf {
 9     private static Method m0;
10     private static Method m1;
11     private static Method m2;
12     private static Method m3;
13 
14     static {
15         try {
16             m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
17             m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
18             m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
19             m3 = Class.forName("com.tuniu.distribute.openapi.common.annotation.MyIntf").getMethod("helloWorld", new Class[0]);
20             return;
21         } catch (NoSuchMethodException localNoSuchMethodException) {
22             throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
23         } catch (ClassNotFoundException localClassNotFoundException) {
24             throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
25         }
26     }
27 
28     public $Proxy0(InvocationHandler paramInvocationHandler) {
29         super(paramInvocationHandler);
30     }
31 
32     public final void helloWorld() {
33         try {
34             this.h.invoke(this, m3, null);
35             return;
36         } catch (Error | RuntimeException localError) {
37             throw localError;
38         } catch (Throwable localThrowable) {
39             throw new UndeclaredThrowableException(localThrowable);
40         }
41     }
42 
43     // 後面省略equals(),hashCode(),toString()三個方法的代碼,因為這三個方法和helloWorld()方法非常相似
44 }

這裡Proxy為我們生成的代理類叫$Proxy0,繼承了Proxy,實現了我們定義的接口MyIntf。每一個JDK動態代理技術生成的代理類的名稱都是由$Proxy前綴加上一個序列數0,1,2...。並且都需要繼承Proxy類。

$Proxy0類中9-26行代碼定義了4個Method字段m0,m1,m2,m3,我們先來看下m3,它描述了我們定義的接口MyIntf中的方法helloWorld。

緊接著下面的32-41行代碼就是對helloWorld方法的實現,它的實現非常簡單就一句話this.h.invoke(this, m3, null);這行代碼就是調用當前對象的h實例的invoke方法,也就是把方法的實現邏輯分派給了h.invoke。這裡的h是繼承父類Proxy中的InvocationHandler字段(讀者可以結合上面的動態代理類圖模型或者Proxy源碼進一步理解)。

同時$Proxy0提供了一個構造函數(代碼28-30行),調用父類的構造函數來注入這個InvocationHandler實例。

$Proxy0中的另外3個Method對象m0,m1,m2分別代表了Object類的hashCode(),equals(),toString()方法,我們知道java中的所有類都是Object的子類(Object類本身除外),這裡$Proxy0重寫了Object中的這三個方法。這三個方法的實現和helloWorld方法很類似,所以筆者這裡就把這段代碼省略了,用一行注釋(43行代碼)解釋了下。

行文至此,我們已經感官的認識了運行時生成的代理類結構。揭開了這層面紗,其實JDK動態代理也沒什麼了。簡單的來說就是,JDK動態代理技術可以為一組接口生成代理類,這個代理類也就是一層殼,簡單的實現了接口中定義的方法。通過提供一個構造函數傳入InvocationHandler實例,然後將方法的具體實現交給它。

更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2016-07/133195p2.htm

Copyright © Linux教程網 All Rights Reserved