看故事學知識,這篇Java代理的文章妙啊!

這是我的第 208 期分享

作者 | java金融

來源 | java金融(ID:java4299)

分享 | Java中文社羣(ID:javacn666)

什麼是代理

代理模式是常用的java設計模式,他的特徵是代理類與委託類有同樣的接口,代理類主要負責爲委託類預處理消息、過濾消息、把消息轉發給委託類,以及事後處理消息等。代理類與委託類之間通常會存在關聯關係,一個代理類的對象與一個委託類的對象關聯,代理類的對象本身並不真正實現服務,而是通過調用委託類的對象的相關方法,來提供特定的服務。

代理其實不僅僅是在軟件開發領域,在我們的日常生活中也是時常可見。比如某p2p老闆突然攜款帶着小姨子跑路了,可憐了下面一堆的程序員揹負一身房貸,上有老下有小,程序員只能被迫去申請勞動仲裁,勞動局就會爲其指派一位代理律師全權負責程序員的仲裁事宜(PS:p2p跑路仲裁拿回工資的可能性非常低,沒讓你把工資退回就算好的了)。那這裏面就是使用了代理模式,因爲在勞動仲裁這個活動中,代理律師會全權代理程序員。比如:房東要將房子出售,於是到房地產中介公司找一箇中介(代理),由他來幫房東完成銷售房屋,簽訂合同、網籤、貸款過戶等等事宜。

代理模式

這是常見代理模式常見的 UML 示意圖。 需要注意的有下面幾點:

  1. 用戶只關心接口功能,而不在乎誰提供了功能。上圖中接口是 Subject

  2. 接口真正實現者是上圖的 RealSubject,但是它不與用戶直接接觸,而是通過代理。

  3. 代理就是上圖中的 Proxy,由於它實現了 Subject 接口,所以它能夠直接與用戶接觸。

  4. 用戶調用 Proxy 的時候,Proxy 內部調用了 RealSubject。所以,Proxy 是中介者,它可以增強 RealSubject 操作。

  • 代理又可以分爲靜態代理和動態代理兩種。我們先來看下靜態代理。

靜態代理

電影是電影公司委託給影院進行播放的,但是影院可以在播放電影的時候,產生一些自己的經濟收益,比如提供按摩椅,娃娃機(這個每次去電影院都會嘗試下,基本上是夾不起來,有木有大神可以傳授下訣竅),賣爆米花、飲料(貴的要死,反正喫不起)等。我們平常去電影院看電影的時候,在電影開始的階段是不是經常會放廣告呢?然後在影片開始結束時播放一些廣告。 下面我們通過代碼來模擬下電影院這一系列的賺錢操作。 首先得有一個接口,通用的接口是代理模式實現的基礎。這個接口我們命名爲 Movie,代表電影播放的能力。

package com.workit.demo.proxy;

public interface Movie {
    void play();
}

  • 接下來我們要創建一個真正的實現這個 Movie 接口的類,和一個實現該接口的代理類。 真正的類《美國隊長》電影:

package com.workit.demo.proxy;

public class CaptainAmericaMovie implements Movie {
    @Override
    public void play() {
        System.out.println("普通影廳正在播放的電影是《美國隊長》");
    }
}

代理類:

package com.workit.demo.proxy;

public class MovieStaticProxy implements Movie {
    Movie movie;

    public MovieStaticProxy(Movie movie) {
        this.movie = movie;
    }

    @Override
    public void play() {
        playStart();
        movie.play();
        playEnd();
    }

    public void playStart() {
        System.out.println("電影開始前正在播放廣告");
    }
    public void playEnd() {
        System.out.println("電影結束了,接續播放廣告");
    }
}


測試類:

package com.workit.demo.proxy;

package com.workit.demo.proxy;

public class StaticProxyTest {
    public static void main(String[] args) {
        Movie captainAmericaMovie = new CaptainAmericaMovie();
        Movie movieStaticProxy = new MovieStaticProxy(captainAmericaMovie);
        movieStaticProxy.play();

    }
}

運行結果:

電影開始前正在播放廣告
正在播放的電影是《美國隊長》
電影結束了,接續播放廣告

現在可以看到,代理模式可以在不修改被代理對象的基礎上,通過擴展代理類,進行一些功能的附加與增強。值得注意的是,代理類和被代理類應該共同實現一個接口,或者是共同繼承某個類。這個就是是靜態代理的內容,爲什麼叫做靜態呢?因爲它的類型是事先預定好的,比如上面代碼中的 MovieStaticProxy 這個類。

優點
  • 代理模式在客戶端與目標對象之間起到一箇中介作用和保護目標對象的作用

  • 代理對象可以擴展目標對象的功能

  • 代理模式能將客戶端與目標對象分離,在一定程度上降低了系統的耦合度。

缺點
  • 代理對象需要與目標對象實現一樣的接口,所以會有很多代理類,類太多.同時,一旦接口增加方法,目標對象與代理對象都要維護。

jdk動態代理

與靜態代理類對照的是動態代理類,動態代理類的字節碼在程序運行時由Java反射機制動態生成,無需程序員手工編寫它的源代碼。動態代理類不僅簡化了編程工作,而且提高了軟件系統的可擴展性,因爲Java 反射機制可以生成任意類型的動態代理類。java.lang.reflect 包中的Proxy類和InvocationHandler 接口提供了生成動態代理類的能力。

  • 接着上面的例子,剛看完《美國隊長》不過癮,還想繼續去看一場《鋼鐵俠》。一直在普通影廳看電影覺得沒啥意思,那就趕緊去VIP影廳(至今不知道長啥樣子)體驗一把。既然 實體店沒體驗過那就用代碼來體驗一次吧。創建一個VIPMovie電影接口

package com.workit.demo.proxy;
public interface VIPMovie {
    void vipPlay();
}

緊接着創建一個VIP影廳的播放實現類

package com.workit.demo.proxy;

public class IronManVIPMovie implements VIPMovie {
    @Override
    public void vipPlay() {
        System.out.println("VI影廳正在播放的電影是《鋼鐵俠》");
    }
}

如果按照靜態代理我們是不是又要創建一個VIP影廳播放的代理實現類,這種方式我們就不演示了。下面我們來看看通過動態代理怎麼來實現吧。

package com.workit.demo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {

    private Object object;

    public MyInvocationHandler(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        playStart();
        Object invoke = method.invoke(object, args);
        playEnd();
        return invoke;
    }

    public void playStart() {
        System.out.println("電影開始前正在播放廣告");
    }
    public void playEnd() {
        System.out.println("電影結束了,接續播放廣告");
    }
}

MyInvocationHandler實現了 InvocationHandler 這個類,這個類是什麼意思呢?大家不要慌張,下面我會解釋。然後,我們就可以在VIP影廳看電影了。

package com.workit.demo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class DynamicProxyTest {
    public static void main(String[] args) {
        IronManVIPMovie ironManVIPMovie = new IronManVIPMovie();
        InvocationHandler invocationHandler = new MyInvocationHandler(ironManVIPMovie);
        VIPMovie dynamicProxy = (VIPMovie) Proxy.newProxyInstance(IronManVIPMovie.class.getClassLoader(),
                IronManVIPMovie.class.getInterfaces(), invocationHandler);
        dynamicProxy.vipPlay();
    }
}

輸出結果:

電影開始前正在播放廣告
VI影廳正在播放的電影是《鋼鐵俠》
電影結束了,接續播放廣告

看到沒有,我們並沒有像靜態代理那樣爲 VIPMovie接口實現一個代理類,但最終它仍然實現了相同的功能,這其中的差別,就是之前討論的動態代理所謂“動態”的原因。 我們順帶把《美國隊長》也用動態代理實現下吧。

package com.workit.demo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class DynamicProxyTest {
    public static void main(String[] args) {
        // VIP 影廳《鋼鐵俠》
        IronManVIPMovie ironManVIPMovie = new IronManVIPMovie();
        InvocationHandler invocationHandler = new MyInvocationHandler(ironManVIPMovie);
        VIPMovie dynamicProxy = (VIPMovie) Proxy.newProxyInstance(IronManVIPMovie.class.getClassLoader(),
                IronManVIPMovie.class.getInterfaces(), invocationHandler);
        dynamicProxy.vipPlay();
        
        // 普通影廳《美國隊長》
        CaptainAmericaMovie captainAmericaMovie = new CaptainAmericaMovie();
        InvocationHandler invocationHandler1 = new MyInvocationHandler(captainAmericaMovie);
        Movie dynamicProxy1 = (Movie) Proxy.newProxyInstance(CaptainAmericaMovie.class.getClassLoader(),
                CaptainAmericaMovie.class.getInterfaces(), invocationHandler1);
        dynamicProxy1.play();
    }
}

輸出結果:

電影開始前正在播放廣告
VI影廳正在播放的電影是《鋼鐵俠》
電影結束了,接續播放廣告
電影開始前正在播放廣告
正在播放的電影是《美國隊長》
電影結束了,接續播放廣告

我們通過 Proxy.newProxyInstance() 方法,卻產生了 Movie和 VIPMovie兩種接口的實現類代理,這就是動態代理的魔力。

JDK動態代理到底是怎麼實現的呢

動態代碼涉及了一個非常重要的類 Proxy。正是通過 Proxy 的靜態方法 newProxyInstance 纔會動態創建代理。具體怎麼去創建代理類就不分析了,感興趣的可以去看下源碼。我們直接看下生成的代理類。 如何查看生成的代理類? 在生成代理類之前加上以下代碼(我用的jdk1.8):

 //新版本 jdk產生代理類   
 System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

如果上述代碼加上不生效可以考慮加下下面的代碼:

// 老版本jdk
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//  該設置用於輸出cglib動態代理產生的類
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\class");  

代碼如下:

  public static void main(String[] args) {
        //新版本 jdk產生代理類
       System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        // VIP 影廳《鋼鐵俠》
        IronManVIPMovie ironManVIPMovie = new IronManVIPMovie();
        InvocationHandler invocationHandler = new MyInvocationHandler(ironManVIPMovie);
        VIPMovie dynamicProxy = (VIPMovie) Proxy.newProxyInstance(IronManVIPMovie.class.getClassLoader(),
                IronManVIPMovie.class.getInterfaces(), invocationHandler);
        dynamicProxy.vipPlay();

        // 普通影廳《美國隊長》
        CaptainAmericaMovie captainAmericaMovie = new CaptainAmericaMovie();
        InvocationHandler invocationHandler1 = new MyInvocationHandler(captainAmericaMovie);
        Movie dynamicProxy1 = (Movie) Proxy.newProxyInstance(CaptainAmericaMovie.class.getClassLoader(),
                CaptainAmericaMovie.class.getInterfaces(), invocationHandler1);
        dynamicProxy1.play();
        System.out.println("VIP 影廳《鋼鐵俠》代理類:"+dynamicProxy.getClass());
        System.out.println("普通影廳《美國隊長》:"+dynamicProxy1.getClass());
    }

我們可以看到結果

電影開始前正在播放廣告
VI影廳正在播放的電影是《鋼鐵俠》
電影結束了,接續播放廣告
電影開始前正在播放廣告
正在播放的電影是《美國隊長》
電影結束了,接續播放廣告
VIP 影廳《鋼鐵俠》代理類:class com.sun.proxy.$Proxy0
普通影廳《美國隊長》:class com.sun.proxy.$Proxy1

產生了兩個代理類分別是$Proxy0$Proxy1。 下面們來看下"鋼鐵俠"的代理類$Proxy0

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import com.workit.demo.proxy.VIPMovie;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements VIPMovie {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void vipPlay() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.workit.demo.proxy.VIPMovie").getMethod("vipPlay");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
},

通過上述代碼我們可以看到 $Proxy0 extends Proxy implements VIPMovie繼承了Proxy 且實現了VIPMovie接口,這也就是爲什麼jdk動態代理必須基於接口,java 是單繼承的。 然後再看下代理類實現的方法:

 public final void vipPlay() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

這個supper.h.invoke Proxy中的h的invoke方法,即InvocationHandler.invoke也就是上面 MyInvocationHandler.invoke方法,至此整個流程就清晰了。這就是jdk的動態代理。

cglib動態代理

上面說jdk動態代理只能基於接口,那麼如果是類要動態代理怎麼辦呢?cglib動態代理就可解決關於類的動態代理。 下面我們來創建一個“《美國隊長2》”

package com.workit.demo.proxy;

public class CaptainAmerica2MovieImpl {

    public void play(){
        System.out.println("正在播放的電影是《美國隊長2》");
    }
}

引入cglib pom依賴

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

創建一個自定義MethodInterceptor。

package com.workit.demo.proxy;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxyInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        playStart();
        Object object = methodProxy.invokeSuper(o, objects);
        playEnd();
        return object;
    }

    public void playStart() {
        System.out.println("電影開始前正在播放廣告");
    }

    public void playEnd() {
        System.out.println("電影結束了,接續播放廣告");
    }
}

測試類

package com.workit.demo.proxy;

import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;

public class CglibProxyTest {
    public static void main(String[] args) {
        // //在指定目錄下生成動態代理類
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\class");
        //創建Enhancer對象,類似於JDK動態代理的Proxy類,下一步就是設置幾個參數
        Enhancer enhancer = new Enhancer();
        //設置目標類的字節碼文件
        enhancer.setSuperclass(CaptainAmerica2MovieImpl.class);
        //設置回調函數
        enhancer.setCallback(new CglibProxyInterceptor());
        //這裏的creat方法就是正式創建代理類
        CaptainAmerica2MovieImpl captainAmerica2Movie = (CaptainAmerica2MovieImpl)enhancer.create();
        //調用代理類的play方法
        captainAmerica2Movie.play();
        System.out.println("cglib動態代理《美國隊長2》:"+captainAmerica2Movie.getClass());
    }
}

輸出結果:

電影開始前正在播放廣告
正在播放的電影是《美國隊長2》
電影結束了,接續播放廣告
cglib動態代理《美國隊長2》:class com.workit.demo.proxy.CaptainAmerica2MovieImpl$$EnhancerByCGLIB$$5c3ddcfe

我們看下最終創建的代理類生成的play方法

public class CaptainAmerica2MovieImpl$$EnhancerByCGLIB$$5c3ddcfe extends CaptainAmerica2MovieImpl implements Factory {
  public final void play() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$play$0$Method, CGLIB$emptyArgs, CGLIB$play$0$Proxy);
        } else {
            super.play();
        }
    }

從代理對象反編譯源碼可以知道,代理對象繼承於CaptainAmerica2MovieImpl ,攔截器調用intercept()方法, intercept()方法由自定義CglibProxyInterceptor實現,所以,最後調用CglibProxyInterceptor中的intercept()方法,從而完成了由代理對象訪問到目標對象的動態代理實現。

  • CGlib是一個強大的,高性能,高質量的Code生成類庫。它可以在運行期擴展Java類與實現Java接口。

  • 用CGlib生成代理類是目標類的子類。

  • 用CGlib生成 代理類不需要接口。

  • 用CGLib生成的代理類重寫了父類的各個方法。

  • 攔截器中的intercept方法內容正好就是代理類中的方法體。

總結

  • 代理分爲靜態代理和動態代理兩種。

  • 靜態代理,代理類需要自己編寫代碼寫成。

  • 動態代理有jdk和cglib,代理類通過 Proxy.newInstance()或者ASM 生成。

  • 靜態代理和動態代理的區別是在於要不要開發者自己定義 Proxy 類。 動態代理通過 Proxy 動態生成 proxy class,但是它也指定了一個 InvocationHandler 或者 MethodInterceptor的實現類。

  • 代理模式本質上的目的是爲了增強現有代碼的功能。

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。

  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

  • 感謝您的閱讀,十分歡迎並感謝您的關注。

參考

https://blog.csdn.net/m0_37314675/article/details/77850967 https://www.cnblogs.com/cC-Zhou/p/9525638.html https://www.jianshu.com/p/4539e6d9f337



往期推薦

啪啪打臉!領導說:try-catch要放在循環體外!

阿里巴巴爲什麼讓初始化集合時必須指定大小?

關注公衆號發送”進羣“,磊哥拉你進讀者羣。

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