JAVA面試50講之9:動態代理的原理是什么?

關于Java中的動態代理,我們首先需要了解的是一種常用的設計模式—代理模式,而對于代理,根據創建代理類的時間點,又可以分為靜態代理和動態代理。

一、代理模式

代理模式是常用的java設計模式,他的特征是代理類與委托類有同樣的接口,代理類主要負責為委托類預處理消息、過濾消息、把消息轉發給委托類,以及事后處理消息等。代理類與委托類之間通常會存在關聯關系,一個代理類的對象與一個委托類的對象關聯,代理類的對象本身并不真正實現服務,而是通過調用委托類的對象的相關方法,來提供特定的服務。簡單的說就是,我們在訪問實際對象時,是通過代理對象來訪問的,代理模式就是在訪問實際對象時引入一定程度的間接性,因為這種間接性,可以附加多種用途。在后面我會

解釋這種間接性帶來的好處。代理模式結構圖(圖片來自《大話設計模式》):

image

二、靜態代理

1、靜態代理

靜態代理:由程序員創建或特定工具自動生成源代碼,也就是在編譯時就已經將接口,被代理類,代理類等確定下來。在程序運行之前,代理類的.class文件就已經生成。

2、靜態代理簡單實現

根據上面代理模式的類圖,來寫一個簡單的靜態代理的例子。我這兒舉一個比較粗糙的例子,假如一個班的同學要向老師交班費,但是都是通過班長把自己的錢轉交給老師。這里,班長就是代理學生上交班費,

班長就是學生的代理。

首先,我們創建一個Person接口。這個接口就是學生(被代理類),和班長(代理類)的公共接口,他們都有上交班費的行為。這樣,學生上交班費就可以讓班長來代理執行。

/** * 創建Person接口
 * @author Gonjan
 */public interface Person {    //上交班費    void giveMoney();}

Student類實現Person接口。Student可以具體實施上交班費的動作。

public class Student implements Person {    private String name;    public Student(String name) {        this.name = name;    }
    @Override    public void giveMoney() {       System.out.println(name + "上交班費50元");    }}

StudentsProxy類,這個類也實現了Person接口,但是還另外持有一個學生類對象,由于實現了Peson接口,同時持有一個學生對象,那么他可以代理學生類對象執行上交班費(執行giveMoney()方法)行為。

/** * 學生代理類,也實現了Person接口,保存一個學生實體,這樣既可以代理學生產生行為
 * @author Gonjan
 *
 */public class StudentsProxy implements Person{    //被代理的學生    Student stu;
    public StudentsProxy(Person stu) {        // 只代理學生對象        if(stu.getClass() == Student.class) {            this.stu = (Student)stu;        }    }
    //代理上交班費,調用被代理學生的上交班費行為    public void giveMoney() {        stu.giveMoney();    }}

下面測試一下,看如何使用代理模式:

public class StaticProxyTest {    public static void main(String[] args) {        //被代理的學生張三,他的班費上交有代理對象monitor(班長)完成        Person zhangsan = new Student("張三");
        //生成代理對象,并將張三傳給代理對象        Person monitor = new StudentsProxy(zhangsan);
        //班長代理上交班費        monitor.giveMoney();    }}

運行結果:

image

這里并沒有直接通過張三(被代理對象)來執行上交班費的行為,而是通過班長(代理對象)來代理執行了。這就是代理模式。

代理模式最主要的就是有一個公共接口(Person),一個具體的類(Student),一個代理類(StudentsProxy),代理類持有具體類的實例,代為執行具體類實例方法。上面說到,代理模式就是在訪問實際對象時引入一定程度的間接性,因為這種間接性,可以附加多種用途。這里的間接性就是指不直接調用實際對象的方法,那么我們在代理過程中就可以加上一些其他用途。就這個例子來說,加入班長在幫張三上交班費之前想要先反映一下張三最近學習有很大進步,通過代理模式很輕松就能辦到:

public class StudentsProxy implements Person{    //被代理的學生    Student stu;
    public StudentsProxy(Person stu) {        // 只代理學生對象        if(stu.getClass() == Student.class) {            this.stu = (Student)stu;        }    }
    //代理上交班費,調用被代理學生的上交班費行為    public void giveMoney() {        System.out.println("張三最近學習有進步!");        stu.giveMoney();    }}

運行結果:

image

可以看到,只需要在代理類中幫張三上交班費之前,執行其他操作就可以了。這種操作,也是使用代理模式的一個很大的優點。最直白的就是在Spring中的面向切面編程(AOP),我們能在一個切點之前執行一些操作,在一個切點之后執行一些操作,這個切點就是一個個方法。這些方法所在類肯定就是被代理了,在代理過程中切入了一些其他操作。

三、動態代理

1.動態代理

代理類在程序運行時創建的代理方式被成為動態代理。 我們上面靜態代理的例子中,代理類(studentProxy)是自己定義好的,在程序運行之前就已經編譯完成。然而動態代理,代理類并不是在Java代碼中定義的,而是在運行時根據我們在Java代碼中的“指示”動態生成的。相比于靜態代理, 動態代理的優勢在于可以很方便的對代理類的函數進行統一的處理,而不用修改每個代理類中的方法。 比如說,想要在每個代理的方法前都加上一個處理方法:

 public void giveMoney() {        //調用被代理方法前加入處理方法        beforeMethod();        stu.giveMoney();    }

這里只有一個giveMoney方法,就寫一次beforeMethod方法,但是如果出了giveMonney還有很多其他的方法,那就需要寫很多次beforeMethod方法,麻煩。那看看下面動態代理如何實現。

2、動態代理簡單實現

在java的java.lang.reflect包下提供了一個Proxy類和一個InvocationHandler接口,通過這個類和這個接口可以生成JDK動態代理類和動態代理對象。

創建一個動態代理對象步驟,具體代碼見后面:

創建一個InvocationHandler對象

//創建一個與代理對象相關聯的InvocationHandler InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);

使用Proxy類的getProxyClass靜態方法生成一個動態代理

  Class<?> stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[] {Person.class});

獲得stuProxyClass 中一個帶InvocationHandler參數的構造器constructor

Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.class);

通過構造器constructor來創建一個動態實例stuProxy

Person stuProxy = (Person) cons.newInstance(stuHandler);

就此,一個動態代理對象就創建完畢,當然,上面四個步驟可以通過Proxy類的newProxyInstances方法來簡化:

 //創建一個與代理對象相關聯的InvocationHandler  InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);//創建一個代理對象stuProxy,代理對象的每個執行方法都會替換執行Invocation中的invoke方法  Person stuProxy= (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);

到這里肯定都會很疑惑,這動態代理到底是如何執行的,是如何通過代理對象來執行被代理對象的方法的,先不急,我們先看看一個簡單的完整的動態代理的例子。還是上面靜態代理的例子,班長需要幫學生代交班費。
首先是定義一個Person接口:

/** * 創建Person接口
 * @author Gonjan
 */public interface Person {    //上交班費    void giveMoney();}

創建需要被代理的實際類:

public class Student implements Person {    private String name;    public Student(String name) {        this.name = name;    }
    @Override    public void giveMoney() {        try {          //假設數錢花了一秒時間            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }       System.out.println(name + "上交班費50元");    }}

再定義一個檢測方法執行時間的工具類,在任何方法執行前先調用start方法,執行后調用finsh方法,就可以計算出該方法的運行時間,這也是一個最簡單的方法執行時間檢測工具。

public class MonitorUtil {
    private static ThreadLocal<Long> tl = new ThreadLocal<>();
    public static void start() {        tl.set(System.currentTimeMillis());    }
    //結束時打印耗時    public static void finish(String methodName) {        long finishTime = System.currentTimeMillis();        System.out.println(methodName + "方法耗時" + (finishTime - tl.get()) + "ms");    }}

創建StuInvocationHandler類,實現InvocationHandler接口,這個類中持有一個被代理對象的實例target。InvocationHandler中有一個invoke方法,所有執行代理對象的方法都會被替換成執行invoke方法。

再再invoke方法中執行被代理對象target的相應方法。當然,在代理過程中,我們在真正執行被代理對象的方法前加入自己其他處理。這也是Spring中的AOP實現的主要原理,這里還涉及到一個很重要的關于java反射方面的基礎知識。

public class StuInvocationHandler<T> implements InvocationHandler {   //invocationHandler持有的被代理對象    T target;
    public StuInvocationHandler(T target) {       this.target = target;    }
    /**     * proxy:代表動態代理對象
     * method:代表正在執行的方法
     * args:代表調用目標方法時傳入的實參
     */    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        System.out.println("代理執行" +method.getName() + "方法");     */           //代理過程中插入監測方法,計算該方法耗時        MonitorUtil.start();        Object result = method.invoke(target, args);        MonitorUtil.finish(method.getName());        return result;    }}

做完上面的工作后,我們就可以具體來創建動態代理對象了,上面簡單介紹了如何創建動態代理對象,我們使用簡化的方式創建動態代理對象:

public class ProxyTest {    public static void main(String[] args) {
        //創建一個實例對象,這個對象是被代理的對象        Person zhangsan = new Student("張三");
        //創建一個與代理對象相關聯的InvocationHandler        InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan);
        //創建一個代理對象stuProxy來代理zhangsan,代理對象的每個執行方法都會替換執行Invocation中的invoke方法        Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
       //代理執行上交班費的方法        stuProxy.giveMoney();    }}

我們執行這個ProxyTest類,先想一下,我們創建了一個需要被代理的學生張三,將zhangsan對象傳給了stuHandler中,我們在創建代理對象stuProxy時,將stuHandler作為參數了的,上面也有說到所有執行代理對象的方法都會被替換成執行invoke方法,也就是說,最后執行的是StuInvocationHandler中的invoke方法。所以在看到下面的運行結果也就理所當然了。

運行結果:

image

上面說到,動態代理的優勢在于可以很方便的對代理類的函數進行統一的處理,而不用修改每個代理類中的方法。是因為所有被代理執行的方法,都是通過在InvocationHandler中的invoke方法調用的,所以我們只要在invoke方法中統一處理,就可以對所有被代理的方法進行相同的操作了。例如,這里的方法計時,所有的被代理對象執行的方法都會被計時,然而我只做了很少的代碼量。

動態代理的過程,代理對象和被代理對象的關系不像靜態代理那樣一目了然,清晰明了。因為動態代理的過程中,我們并沒有實際看到代理類,也沒有很清晰地的看到代理類的具體樣子,而且動態代理中被代理對象和代理對象是通過InvocationHandler來完成的代理過程的,其中具體是怎樣操作的,為什么代理對象執行的方法都會通過InvocationHandler中的invoke方法來執行。帶著這些問題,我們就需要對java動態代理的源碼進行簡要的分析,弄清楚其中緣由。

四、動態代理原理分析

1、Java動態代理創建出來的動態代理類

上面我們利用Proxy類的newProxyInstance方法創建了一個動態代理對象,查看該方法的源碼,發現它只是封裝了創建動態代理類的步驟(紅色標準部分):

    public static Object newProxyInstance(ClassLoader loader,                                          Class<?>[] interfaces,                                          InvocationHandler h)        throws IllegalArgumentException    {        Objects.requireNonNull(h);
        final Class<?>[] intfs = interfaces.clone();        final SecurityManager sm = System.getSecurityManager();        if (sm != null) {            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);        }
        /*         * Look up or generate the designated proxy class.
         */        Class<?> cl = getProxyClass0(loader, intfs);
        /*         * Invoke its constructor with the designated invocation handler.
         */        try {            if (sm != null) {                checkNewProxyPermission(Reflection.getCallerClass(), cl);            }
            final Constructor<?> cons = cl.getConstructor(constructorParams);            final InvocationHandler ih = h;            if (!Modifier.isPublic(cl.getModifiers())) {                AccessController.doPrivileged(new PrivilegedAction<Void>() {                    public Void run() {                        cons.setAccessible(true);                        return null;                    }                });            }            return cons.newInstance(new Object[]{h});        } catch (IllegalAccessException|InstantiationException e) {            throw new InternalError(e.toString(), e);        } catch (InvocationTargetException e) {            Throwable t = e.getCause();            if (t instanceof RuntimeException) {                throw (RuntimeException) t;            } else {                throw new InternalError(t.toString(), t);            }        } catch (NoSuchMethodException e) {            throw new InternalError(e.toString(), e);        }    }

其實,我們最應該關注的是 Class<?> cl = getProxyClass0(loader, intfs);這句,這里產生了代理類,后面代碼中的構造器也是通過這里產生的類來獲得,可以看出,這個類的產生就是整個動態代理的關鍵,由于是動態生成的類文件,我這里不具體進入分析如何產生的這個類文件,只需要知道這個類文件時緩存在java虛擬機中的,我們可以通過下面的方法將其打印到文件里面,一睹真容:

        byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", Student.class.getInterfaces());        String path = "G:/javacode/javase/Test/bin/proxy/StuProxy.class";        try(FileOutputStream fos = new FileOutputStream(path)) {            fos.write(classFile);            fos.flush();            System.out.println("代理類class文件寫入成功");        } catch (Exception e) {           System.out.println("寫文件錯誤");        }

對這個class文件進行反編譯,我們看看jdk為我們生成了什么樣的內容:

import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;import proxy.Person;public final class $Proxy0 extends Proxy implements Person{  private static Method m1;  private static Method m2;  private static Method m3;  private static Method m0;
  /**  *注意這里是生成代理類的構造方法,方法參數為InvocationHandler類型,看到這,是不是就有點明白
  *為何代理對象調用方法都是執行InvocationHandler中的invoke方法,而InvocationHandler又持有一個
  *被代理對象的實例,不禁會想難道是....? 沒錯,就是你想的那樣。
  *
  *super(paramInvocationHandler),是調用父類Proxy的構造方法。
  *父類持有:protected InvocationHandler h;
  *Proxy構造方法:
  *    protected Proxy(InvocationHandler h) {
  *         Objects.requireNonNull(h);
  *         this.h = h;
  *     }
  *
  */  public $Proxy0(InvocationHandler paramInvocationHandler)    throws   {    super(paramInvocationHandler);  }
  //這個靜態塊本來是在最后的,我把它拿到前面來,方便描述   static  {    try    {      //看看這兒靜態塊兒里面有什么,是不是找到了giveMoney方法。請記住giveMoney通過反射得到的名字m3,其他的先不管      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);      m3 = Class.forName("proxy.Person").getMethod("giveMoney", new Class[0]);      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);      return;    }    catch (NoSuchMethodException localNoSuchMethodException)    {      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());    }    catch (ClassNotFoundException localClassNotFoundException)    {      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());    }  }
  /**  * 
  *這里調用代理對象的giveMoney方法,直接就調用了InvocationHandler中的invoke方法,并把m3傳了進去。
  *this.h.invoke(this, m3, null);這里簡單,明了。
  *來,再想想,代理對象持有一個InvocationHandler對象,InvocationHandler對象持有一個被代理的對象,
  *再聯系到InvacationHandler中的invoke方法。嗯,就是這樣。
  */  public final void giveMoney()    throws   {    try    {      this.h.invoke(this, m3, null);      return;    }    catch (Error|RuntimeException localError)    {      throw localError;    }    catch (Throwable localThrowable)    {      throw new UndeclaredThrowableException(localThrowable);    }  }
  //注意,這里為了節省篇幅,省去了toString,hashCode、equals方法的內容。原理和giveMoney方法一毛一樣。}

jdk為我們的生成了一個叫$Proxy0(這個名字后面的0是編號,有多個代理類會一次遞增)的代理類,這個類文件時放在內存中的,我們在創建代理對象時,就是通過反射獲得這個類的構造方法,然后創建的代理實例。通過對這個生成的代理類源碼的查看,我們很容易能看出,動態代理實現的具體過程。

我們可以對InvocationHandler看做一個中介類,中介類持有一個被代理對象,在invoke方法中調用了被代理對象的相應方法。通過聚合方式持有被代理對象的引用,把外部對invoke的調用最終都轉為對被代理對象的調用。

代理類調用自己方法時,通過自身持有的中介類對象來調用中介類對象的invoke方法,從而達到代理執行被代理對象的方法。也就是說,動態代理通過中介類實現了具體的代理功能。

五、總結

生成的代理類:$Proxy0 extends Proxy implements Person,我們看到代理類繼承了Proxy類,所以也就決定了java動態代理只能對接口進行代理,Java的繼承機制注定了這些動態代理類們無法實現對class的動態代理。
上面的動態代理的例子,其實就是AOP的一個簡單實現了,在目標對象的方法執行之前和執行之后進行了處理,對方法耗時統計。Spring的AOP實現其實也是用了Proxy和InvocationHandler這兩個東西的。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容

  • 原文: Dyanmic Proxy Classes 介紹 一個動態代理類是實現了多個接口存在于運行時的類,這樣,一...
    半黑月缺閱讀 952評論 0 0
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,328評論 11 349
  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陳阿飛閱讀 873評論 1 1
  • 一、基本概念 1.什么是代理? 在闡述JDK動態代理之前,我們很有必要先來弄明白代理的概念。代理這個詞本身并不是計...
    小李彈花閱讀 16,467評論 2 40
  • 正月二十七,二十八,二十九這三天里,全縣的父老鄉親以及周邊的親朋好友都會蒞臨這小小的道口縣城,來赴一年一度的道口古...
    藍天白云1134閱讀 879評論 0 6