java | 什麼是動態代理?

微信公衆號:一個優秀的廢人。若有問題,請後臺留言,反正我也不會聽。

最近在複習 Java 相關,回顧了下代理模式。代理模式在 Java 領域不少地方都有應用,它分爲靜態代理和動態代理,其中 Spring AOP 就是動態代理的典型例子。動態代理又分爲接口代理和 cglib (子類代理),結合個人理解寫了幾個 demo 分享給大家,這是昨晚修仙到 3 點寫出來的文章,不點在看,我以爲說不過去了。前端

代理模式在咱們平常中很常見,生活到處有代理:java

  • 看張學友的演唱會很難搶票,能夠找黃牛排隊買
  • 嫌出去吃飯麻煩,能夠叫外賣

不管是黃牛、外賣騎手都得幫咱們幹活。可是他們不能一手包辦(好比黃牛不能幫我吃飯),他們只能作咱們不能或者不想作的事。git

  • 找黃牛能夠幫我排隊買上張學友的演唱會門票
  • 外賣騎手能夠幫我把飯送到樓下

因此,你看。代理模式其實就是當前對象不肯意作的事情,委託給別的對象作。github

靜態代理

我仍是以找黃牛幫我排隊買張學友的演唱會門票的例子,寫個 demo 說明。如今有一個 Human 接口,不管是我仍是黃牛都實現了這個接口。算法

public interface Human {

    void eat();

    void sleep();

    void lookConcert();

}

例如,我這個類,我會吃飯和睡覺,如如下類:微信

public class Me implements Human{

    @Override
    public void eat() {
        System.out.println("eat emat ....");
    }

    @Override
    public void sleep() {
        System.out.println("Go to bed at one o'clock in the morning");
    }

    @Override
    public void lookConcert() {
        System.out.println("Listen to Jacky Cheung's Concert");
    }

}

有黃牛類,例如:ide

public class Me implements Human{

    @Override
    public void eat() {
    }

    @Override
    public void sleep() {
    }

    @Override
    public void lookConcert() {
    }

}

如今我和黃牛都已經準備好了,怎麼把這兩者關聯起來呢?咱們要明確的是黃牛是要幫我買票的,買票必然就須要幫我排隊,因而有如下黃牛類:注意這裏咱們不關心,黃牛的其餘行爲,咱們只關心他能不能排隊買票。this

public class HuangNiu implements Human{

    private Me me;

    public HuangNiu() {
        me = new Me();
    }

    @Override
    public void eat() {
    }

    @Override
    public void sleep() {
    }

    @Override
    public void lookConcert() {
        // 添加排隊買票方法
        this.lineUp();
        me.lookConcert();
    }

    public void lineUp() {

        System.out.println("line up");

    }

}

最終的 main 方法調用以下:spa

public class Client {

    public static void main(String[] args) {

        Human human = new HuangNiu();
        human.lookConcert();

    }

}

結果以下:3d

靜態代理結果

因而可知,黃牛就只是作了咱們不肯意作的事(排隊買票),實際看演唱會的人仍是我。客戶端也並不關心代理類代理了哪一個類,由於代碼控制了客戶端對委託類的訪問。客戶端代碼表現爲 Human human = new HuangNiu();

因爲代理類實現了抽象角色的接口,致使代理類沒法通用。好比,個人狗病了,想去看醫生,可是排隊掛號很麻煩,我也想有個黃牛幫個人排隊掛號看病,可是黃牛它不懂這隻狗的特性(黃牛跟狗不是同一類型,黃牛屬於 Human 但狗屬於 Animal 類)但排隊掛號和排隊買票相對於黃牛來講它兩就是一件事,這個方法是不變的,現場排隊。那咱們能不能找一個代理說既能夠幫人排隊買票也能夠幫狗排隊掛號呢?

答案確定是能夠的,能夠用動態代理。

基於接口的動態代理

如靜態代理的內容所描述的,靜態代理受限於接口的實現。動態代理就是經過使用反射,動態地獲取抽象接口的類型,從而獲取相關特性進行代理。因動態代理可以爲全部的委託方進行代理,所以給代理類起個通用點的名字 HuangNiuHandle。先看黃牛類能夠變成什麼樣?

public class HuangNiuHandle implements InvocationHandler {

    private Object proxyTarget;

    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object methodObject = null;

        System.out.println("line up");
        methodObject = method.invoke(proxyTarget, args);
        System.out.println("go home and sleep");

        return methodObject;
    }

}

這個時候的客戶端代碼就變成這樣了

public class Client {

    public static void main(String[] args) {

        HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
        Human human = (Human) huangNiuHandle.getProxyInstance(new Me());

        human.eat();
        human.run();
        human.lookConcert();

        System.out.println("------------------");

        Animal animal = (Animal) huangNiuHandle.getProxyInstance(new Dog());
        animal.eat();
        animal.run();
        animal.seeADoctor();
    }

}

使用動態代理有三個要點,

  1. 必須實現 InvocationHandler 接口,代表該類是一個動態代理執行類。
  2. InvocationHandler 接口內有一實現方法以下: public Object invoke(Object proxy, Method method, Object[] args) 。使用時須要重寫這個方法

    1. 獲取代理類,須要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 這個方法去獲取Proxy對象(Proxy 類類型的實例)。

注意到 Proxy.newProxyInstance 這個方法,它須要傳入 3 個參數。解析以下:

// 第一個參數,是類的加載器
// 第二個參數是委託類的接口類型,證代理類返回的是同一個實現接口下的類型,保持代理類與抽象角色行爲的一致
// 第三個參數就是代理類自己,即告訴代理類,代理類遇到某個委託類的方法時該調用哪一個類下的invoke方法
Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)

再來看看 invoke 方法,用戶調用代理對象的什麼方法,實質上都是在調用處理器的
invoke 方法,經過該方法調用目標方法,它也有三個參數:

// 第一個參數爲 Proxy 類類型實例,如匿名的 $proxy 實例
// 第二個參數爲委託類的方法對象
// 第三個參數爲委託類的方法參數
// 返回類型爲委託類某個方法的執行結果
public Object invoke(Object proxy, Method method, Object[] args)

調用該代理類以後的輸出結果:

動態代理

由結果可知,黃牛不只幫了(代理)我排隊買票,還幫了(代理)個人狗排隊掛號。因此,你看靜態代理須要本身寫代理類(代理類須要實現與目標對象相同的接口),還須要一一實現接口方法,但動態代理不須要。

注意,咱們並非全部的方法都須要黃牛這個代理去排隊。咱們知道只有我看演唱會和個人狗去看醫生時,才須要黃牛,若是要實現咱們想要的方法上面添加特定的代理,能夠經過 invoke 方法裏面的方法反射獲取 method 對象方法名稱便可實現,因此動態代理類能夠變成這樣:

public class HuangNiuHandle implements InvocationHandler {

    private Object proxyTarget;

    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object methodObject = null;

        if ("lookConcert".equals(method.getName()) ||
        "seeADoctor".equals(method.getName())) {

            System.out.println("line up");
            // 調用目標方法
            methodObject = method.invoke(proxyTarget, args);
        } else {
            // 不使用第一個proxy參數做爲參數,不然會形成死循環
            methodObject = method.invoke(proxyTarget, args);
        }

        return methodObject;
    }

}

結果以下:能夠看到咱們只在特定方法求助了黃牛

動態代理

因而可知,動態代理通常應用在記錄日誌等橫向業務。

值得注意的是:

  1. 基於接口類的動態代理模式,必須具有抽象角色、委託類、代理三個基本角色。委託類和代理類必須由抽象角色衍生出來,不然沒法使用該模式。
  2. 動態代理模式最後返回的是具備抽象角色(頂層接口)的對象。在委託類內被 private 或者 protected 關鍵修飾的方法將不會予以調用,即便容許調用。也沒法在客戶端使用代理類轉換成子類接口,對方法進行調用。也就是說上述的動態代理返回的是委託類(Me)或 (Dog)的就接口對象 (Human)或 (Animal)。
  3. 在 invoke 方法內爲何不使用第一個參數進行執行回調。在客戶端使用getProxyInstance(new Child( ))時,JDK 會返回一個 proxy 的實例,實例內有InvokecationHandler 對象及動態繼承下來的目標 。客戶端調用了目標方法,有以下操做:首先 JDK 先查找 proxy 實例內的 handler 對象 而後執行 handler 內的 invoke 方法。

根據 public Object invoke 這個方法第一個參數 proxy 就是對應着 proxy 實例。若是在 invoke 內使用 method.invoke(proxy,args) ,會出現這樣一條方法鏈,目標方法→invoke→目標方法→invoke...,最終致使堆棧溢出。

基於子類的動態代理

爲了省事,我這裏並無繼承父類,但在實際開發中是須要繼承父類才比較方便擴展的。與基於接口實現類不一樣的是:

  1. CGLib (基於子類的動態代理)使用的是方法攔截器 MethodInterceptor ,須要導入 cglib.jar 和 asm.jar 包
  2. 基於子類的動態代理,返回的是子類對象
  3. 方法攔截器對 protected 修飾的方法能夠進行調用

代碼以下:

public class Me {

    public void eat() {
        System.out.println("eat meat ....");
    }

    public void run() {
        System.out.println("I run with two legs");
    }

    public void lookConcert() {
        System.out.println("Listen to Jacky Cheung's Concert");
    }

    protected void sleep() {
        System.out.println("Go to bed at one o'clock in the morning");
    }

}

Dog 類

public class Dog {

    public void eat() {
        System.out.println("eat Dog food ....");
    }

    public void run() {
        System.out.println("Dog running with four legs");
    }

    public void seeADoctor() {
        System.out.println("The dog go to the hospital");
    }

}

黃牛代理類,注意 invoke() 這裏多了一個參數 methodProxy ,它的做用是用於執行目標(委託類)的方法,至於爲何用 methodProxy ,官方的解釋是速度快且在intercep t內調用委託類方法時不用保存委託對象引用。

public class HuangNiuHandle implements MethodInterceptor {

    private Object proxyTarget;

    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this);
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        Object methodObject = null;

        if ("lookConcert".equals(method.getName()) ||
                "seeADoctor".equals(method.getName())) {
            System.out.println("line up");
            // 調用目標方法
            methodObject = methodProxy.invokeSuper(proxy, args);
        } else {
            methodObject = method.invoke(proxyTarget, args);
        }

        return methodObject;
    }
}

client 類

public class Client {

    public static void main(String[] args) {
        HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
        Me me = (Me) huangNiuHandle.getProxyInstance(new Me());

        me.eat();
        me.run();
        me.sleep();
        me.lookConcert();

        System.out.println("------------------");

        Dog dog = (Dog) huangNiuHandle.getProxyInstance(new Dog());
        dog.eat();
        dog.run();
        dog.seeADoctor();
    }
}

結果:

基於子類的動態代理

注意到 Me 類中被 protected 修飾的方法 sleep 仍然能夠被客戶端調用。這在基於接口的動態代理中是不被容許的。

靜態代理與動態代理的區別

靜態代理須要本身寫代理類並一一實現目標方法,且代理類必須實現與目標對象相同的接口。

動態代理不須要本身實現代理類,它是利用 JDKAPI,動態地在內存中構建代理對象(須要咱們傳入被代理類),而且默認實現全部目標方法。

源碼下載:https://github.com/turoDog/re...

後語

若是本文對你哪怕有一丁點幫助,請幫忙點好看,你的好看是我堅持寫做的動力。關注公衆號一個優秀的廢人回覆 1024 獲取資料:Python、C++、Java、Linux、Go、前端、算法資料分享

一個優秀的廢人,給你講幾斤技術。

相關文章
相關標籤/搜索