你有被代理過嗎?講講開源框架都在用的代理模式

三分惡發表於2022-02-18

大家好,我是老三。

這節我們來看一個非常重要的設計模式——代理模式,儘管我們工作中可能很少用到,但它是很多框架重要功能的基石,肘,我們開始吧。

引言

節假日的地鐵站,你是否見過有人掏出電腦,原地輸出,原來是群裡被瘋狂@……

告訴自己不準哭

使用者提問題,客服@我們解決,可能很多開發同學都經歷過這樣的場景。

使用者找到客服,提出問題,客服又找到開發同學,讓開發同學去解決問題,開發同學解決完,最後反饋給客服,客服再反饋到使用者。

站在使用者的視角,感覺就是客服解決了這個問題,這其實就是一種代理。

使用者向客服提問題

我們以這個例子,來看看Java怎麼實現代理模式的吧。

Java的三種代理模式實現

代理模式的定義:

Provide a surrogate or placeholder for another object to control access to it.(為其他物件提供一種代理以控制對這個物件的訪問。)

簡單說,就是設定一箇中間代理來控制訪問原目標物件,達到增強原物件的功能和簡化訪問方式的目的。

代理模式通用類圖

Java實現代理模式分為兩類三種,兩類是靜態代理動態代理,動態代理又可以分為JDK動態代理CGLIB動態代理

Java實現代理模式

靜態代理

靜態代理比較簡單,代理類需要實現和目標介面類一樣的介面。

Solver靜態代理類圖

  • 介面類:ISolver

    public interface ISolver {
        void solve();
    }
    
  • 目標類:Solver

    public class Solver implements ISolver {
        @Override
        public void solve() {
            System.out.println("瘋狂掉頭髮解決問題……");
        }
    }
    
  • 代理類:SolverProxy,代理類也要實現介面,並且還要維護一個目標物件。

    public class SolverProxy implements ISolver {
        //目標物件
        private ISolver target;
    
        public SolverProxy(ISolver target) {
            this.target = target;
        }
    
        @Override
        public void solve() {
            System.out.println("請問有什麼能幫到您?");
            target.solve();
            System.out.println("問題已經解決啦!");
        }
    }
    
  • 客戶端;Client

    public class Client {
        public static void main(String[] args) {
            //目標物件:程式設計師
            ISolver developer = new Solver();
            //代理:客服小姐姐
            SolverProxy csProxy = new SolverProxy(developer);
            //目標方法:解決問題
            csProxy.solve();
        }
    }
    
  • 執行結果

    請問有什麼能幫到您?
    瘋狂掉頭髮解決問題……
    問題已經解決啦!
    

我們看到,通過靜態代理,可以在不修改目標物件的前提下擴充套件目標物件的功能。

但是,它也有一些問題:

  • 冗餘:由於代理物件要實現與目標物件一致的介面,會產生過多的代理類。
  • 維護性不佳:一旦介面增加方法,目標物件與代理物件都要進行修改。

JDK動態代理

JDK動態代理利用了JDK反射機制,動態地在記憶體中構建代理物件,從而實現對目標物件的代理功能。

它主要用到了兩個反射類的API:

  • java.lang.reflect Proxy| static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h):返回一個指定介面的代理類例項,該介面可以將方法呼叫指派到指定的呼叫處理程式
  • java.lang.reflect InvocationHandler|Object invoke(Object proxy, Method method, Object[] args) :在代理例項上呼叫目標方法,並返回結果。

我們來看看使用JDK動態代理之後的客服代理場景。

JDK動態代理類圖

  • 介面類:ISolver

    public interface ISolver {
        void solve();
    }
    
  • 目標類:Solver,目標類需要實現介面類。

    public class Solver implements ISolver {
        @Override
        public void solve() {
            System.out.println("瘋狂掉頭髮解決問題……");
        }
    }
    
  • 動態代理工廠:ProxyFactory,這裡的動態代理工廠,不需要實現介面,直接採用反射的方式生成一個目標物件的代理物件例項。

    ps:這裡用了一個匿名內部類的方法,還有一種方法,動態代理類實現InvocationHandler介面,大體上類似,就不再給出例子了。

    public class ProxyFactory {
    
        // 維護一個目標物件
        private Object target;
    
        public ProxyFactory(Object target) {
            this.target = target;
        }
    
        // 為目標物件生成代理物件
        public Object getProxyInstance() {
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("請問有什麼可以幫到您?");
    
                            // 呼叫目標物件方法
                            Object returnValue = method.invoke(target, args);
    
                            System.out.println("問題已經解決啦!");
                            return null;
                        }
                    });
        }
    }
    
    
  • 客戶端:Client

    客戶端生成一個代理物件例項,通過代理物件呼叫目標物件方法的時候,就會進入invoke()方法,最後是通過反射的方式呼叫目標物件的方法。

    public class Client {
        public static void main(String[] args) {
            //目標物件:程式設計師
            ISolver developer = new Solver();
            //代理:客服小姐姐
            ISolver csProxy = (ISolver) new ProxyFactory(developer).getProxyInstance();
            //目標方法:解決問題
            csProxy.solve();
        }
    }
    
  • 執行結果:

    請問有什麼可以幫到您?
    瘋狂掉頭髮解決問題……
    問題已經解決啦!
    

我們簡單總結一下靜態代理和動態代理的主要區別:

靜態代理動態代理最主要區別

  • 靜態代理在編譯時就已經實現,編譯完成後代理類是一個實際的class檔案
  • 動態代理是在執行時動態生成的,即編譯完成後沒有實際的class檔案,而是在執行時動態生成類位元組碼,並載入到JVM中

我們也觀察到,JDK動態代理,目標物件必須得實現介面,也就是說它是面向介面的,假如我們不想要介面怎麼辦呢?

Cglib動態代理

CGLIB(Code Generation Library)是一個基於ASM的位元組碼生成庫,它允許我們在執行時對位元組碼進行修改和動態生成,它是通過繼承來實現的。

我們來看看使用Cglib之後,我們的客服代理是什麼樣的:

Cglib動態代理類圖

  • 引入依賴:Cglib是第三方類庫,需要引入依賴

            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>3.2.5</version>
            </dependency>
    
  • 目標類:Solver,這裡目標類不用再實現介面。

    public class Solver {
    
        public void solve() {
            System.out.println("瘋狂掉頭髮解決問題……");
        }
    }
    
  • 動態代理工廠:

    public class ProxyFactory implements MethodInterceptor {
    
       //維護一個目標物件
        private Object target;
    
        public ProxyFactory(Object target) {
            this.target = target;
        }
    
        //為目標物件生成代理物件
        public Object getProxyInstance() {
            //工具類
            Enhancer en = new Enhancer();
            //設定父類
            en.setSuperclass(target.getClass());
            //設定回撥函式
            en.setCallback(this);
            //建立子類物件代理
            return en.create();
        }
    
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("請問有什麼可以幫到您?");
            // 執行目標物件的方法
            Object returnValue = method.invoke(target, args);
            System.out.println("問題已經解決啦!");
            return null;
        }
    
    }
    
  • 客戶端:Client

    public class Client {
        public static void main(String[] args) {
            //目標物件:程式設計師
            Solver developer = new Solver();
            //代理:客服小姐姐
            Solver csProxy = (Solver) new ProxyFactory(developer).getProxyInstance();
            //目標方法:解決問題
            csProxy.solve();
        }
    }
    
  • 執行結果

    請問有什麼可以幫到您?
    瘋狂掉頭髮解決問題……
    問題已經解決啦!
    

我們可以看到Cglib動態代理和JDK動態代理最大的區別就是:

  • 使用JDK動態代理的物件必須實現一個或多個介面
  • 使用Cglib動態代理的物件則無需實現介面,達到代理類無侵入。

我們還需要注意:

  • CGLib不能對宣告為final的方法進行代理,因為是通過繼承父類的方式實現,如果父類是final的,那麼就無法繼承父類。

擴充套件:動態代理的應用

標題裡說了,開源框架都在用的代理模式,那麼主流的開源框架哪些地方用到了代理模式呢?——確切說是動態代理呢?

比如:

  • Spring AOP可以將一些非核心業務流程抽象成切面,幫助我們處理非主線的流程,它是怎麼實現的呢?
  • 你平時用慣了的MyBatis,寫了那麼多Mapper介面,為什麼它不用實現類就能執行SQL呢?

後面兩期是大家期待的面渣逆襲系列,將會揭曉這兩個問題的答案。


簡單的事情重複做,重複的事情認真做,認真的事情有創造性地做!

我是三分惡,一個能文能武的普通開發。點贊關注不迷路,我們們下期見!



參考:

[1].《設計模式之禪》

[2].Java三種代理模式:靜態代理、動態代理和cglib代理

相關文章