Java設計模式之(五)——代理模式


1、什么是代理模式

Provide a surrogate or placeholder for another object to control access to it.

Proxy Pattern:為其他對象提供一種代理以控制對這個對象的訪問。

說人話:在不改變原始類(或叫被代理類)代碼的情況下,通過引入代理類來給原始類附加功能,比如Spring AOP。

2、代理模式定義

image-20210907235345478

①、Subject

抽象主題角色,可以是抽象類,可以是接口,是一個最普通的業務類定義,無特殊要求。

②、RealSubject

真實主題角色,也叫被代理角色,是業務邏輯的具體執行者。

③、Proxy

代理主題角色,也叫代理類,它負責對真實角色的應用,把所有抽象主題類定義的方法限制委托給真實主題角色實現,並在真實主題角色處理前后做一些預處理或善后工作。

通用代碼如下:

/**
 * 抽象主題類
 */
public interface Subject {
    void doSomething();
}
/**
 * 真實主題角色
 */
public class RealSubject implements Subject{

    @Override
    public void doSomething() {
        //TODO 具體執行的事
    }
}
/**
 * 代理主題角色
 */
public class Proxy implements Subject{
    //要代理的具體實現類
    private Subject realSubject;

    public Proxy(Subject realSubject){
        this.realSubject = realSubject;
    }
    @Override
    public void doSomething() {
        this.before();
        realSubject.doSomething();
        this.after();
    }

    // 預處理
    private void before(){
        // TODO
    }

    // 善后處理
    private void after(){
        // TODO
    }
}

3、代理模式的兩種實現

比如用代理模式實現統計某個接口的耗時。

3.1 靜態代理

①、基於接口編程

抽象主題類:

public interface IUserController {
    // 登錄
    String login(String username,String password);
    // 注冊
    String register(String username,String password);
}

具體主題類:

public class UserController implements IUserController{
    @Override
    public String login(String username, String password) {
        // TODO 登錄邏輯
        return null;
    }

    @Override
    public String register(String username, String password) {
        // TODO 注冊邏輯
        return null;
    }
}

代理主題類:

public class UserControllerProxy implements IUserController{
    private IUserController userController;

    public UserControllerProxy(IUserController userController){
        this.userController = userController;
    }
    @Override
    public String login(String username, String password) {
        long startTime = System.currentTimeMillis();
        // 登錄邏輯
        userController.login("username","password");
        long endTime = System.currentTimeMillis();
        long responseTime = endTime - startTime;
        System.out.println("接口響應時間:"+responseTime);
        return null;
    }

    @Override
    public String register(String username, String password) {
        long startTime = System.currentTimeMillis();
        // 注冊邏輯
        userController.register("username","password");
        long endTime = System.currentTimeMillis();
        long responseTime = endTime - startTime;
        System.out.println("接口響應時間:"+responseTime);
        return null;
    }
}

測試:

因為原始類 UserController 和代理類 UserControllerProxy 實現相同的接口,是基於接口而非實現編程,將UserController類對象替換為UserControllerProxy類對象,不需要改動太多代碼

public class StaticProxyTest {
    public static void main(String[] args) {
        IUserController userController = new UserControllerProxy(new UserController());
        userController.login("username","password");
        userController.register("username","password");
    }
}

在上面的代碼中,代理類和具體主題類需要實現相同的接口,假如具體主題類沒有實現接口,並且不是我們開發維護的(比如來自第三方接口),我們要統計這個第三方接口的耗時,那應該如何實現代理模式呢?

②、基於繼承

繼承具體主題類,然后擴展其方法即可,直接看代碼。

public class UserControllerProxy extends UserController {
    @Override
    public String login(String username, String password) {
        long startTime = System.currentTimeMillis();
        // 登錄邏輯
        super.login("username","password");
        long endTime = System.currentTimeMillis();
        long responseTime = endTime - startTime;
        System.out.println("接口響應時間:"+responseTime);
        return null;
    }

    @Override
    public String register(String username, String password) {
        long startTime = System.currentTimeMillis();
        // 注冊邏輯
        super.register("username","password");
        long endTime = System.currentTimeMillis();
        long responseTime = endTime - startTime;
        System.out.println("接口響應時間:"+responseTime);
        return null;
    }
}

3.2 動態代理

在上面的例子中,有兩個問題:

①、我們需要在代理類中,將具體主題類中的所有的方法,都重新實現一遍,並且為每個方法都附加相似的代碼邏輯,如果方法很多,重復代碼也會很多。

②、如果要添加的附加功能的類有不止一個,我們需要針對每個類都創建一個代理類。

那該如何解決上面的問題呢?答案就是動態代理(Dynamic Proxy)。

動態代理:不事先為每個原始類編寫代理類,而是在運行的時候,動態地創建原始類對應的代理類,然后在系統中用代理類替換掉原始類。

JDK動態代理:

public class DynamicProxyHandler implements InvocationHandler {

    private Object target;
    public DynamicProxyHandler(Object target){
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = method.invoke(this.target, args);
        long endTime = System.currentTimeMillis();
        long responseTime = endTime - startTime;
        System.out.println("接口響應時間:"+responseTime);
        return result;
    }
}

測試:

public class DynamicProxyTest {
    public static void main(String[] args) {
        // 1、創建具體主題類
        IUserController userController = new UserController();
        // 2、創建 Handler
        DynamicProxyHandler proxyHandler = new DynamicProxyHandler(userController);
        // 3、動態產生代理類
        IUserController o = (IUserController)Proxy.newProxyInstance(userController.getClass().getClassLoader(),
                userController.getClass().getInterfaces(),
                proxyHandler);
        o.login("username","password");
        o.register("username","password");
    }
}

這是 JDK 動態代理,利用反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理,代理對象是在程序運行時產生的,而不是編譯期,要求是具體主題類必須實現接口。

另外一種方式是 Cglib 動態代理。CGLIB(Code Generation Library)是一個基於ASM的字節碼生成庫,它允許我們在運行時對字節碼進行修改和動態生成,也就是通過修改字節碼生成子類來處理。

Cglib 動態代理:

public class UserController{
    public String login(String username, String password) {
        // TODO 登錄邏輯
        System.out.println("登錄");
        return null;
    }

    public String register(String username, String password) {
        // TODO 注冊邏輯
        System.out.println("注冊");
        return null;
    }
}

注意:真實主題類是沒有實現接口的。

public class CglibDynamicProxy implements MethodInterceptor {
    private Object target;

    public CglibDynamicProxy(Object target){
        this.target = target;
    }

    // 給目標創建代理對象
    public Object newProxyInstance(){
        // 1.工具類
        Enhancer enhancer = new Enhancer();
        // 2.設置父類
        enhancer.setSuperclass(target.getClass());
        // 3.設置回調函數
        enhancer.setCallback(this);
        // 4.創建子類(代理對象)
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = method.invoke(this.target, args);
        long endTime = System.currentTimeMillis();
        long responseTime = endTime - startTime;
        System.out.println("接口響應時間:"+responseTime);
        return result;
    }
}

測試:

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
        UserController userController = new UserController();
        CglibDynamicProxy cglibDynamicProxy = new CglibDynamicProxy(userController);
        UserController o = (UserController)cglibDynamicProxy.newProxyInstance();
        o.login("username","password");
        o.register("username","password");
    }
}

4、代理模式優點

①、職責清晰

真實的角色就是實現實際的業務邏輯, 不用關心其他非本職責的事務, 通過后期的代理完成一件事務, 附帶的結果就是編程簡潔清晰。

②、高擴展性

具體主題角色是隨時都會發生變化的, 只要它實現了接口, 甭管它如何變化,代理類完全都可以在不做任何修改的情況下使用。

5、代理模式應用場景

①、業務系統的非功能性需求開發

這是最常用的一個場景。比如:監控、統計、鑒權、限流、事務、冪等、日志。我們將這些附加功能與業務功能解耦,放到代理類中統一處理,讓程序員只需要關注業務方面的開發。

典型例子就是 SpringAOP。

②、RPC

RPC(遠程代理) 框架也可以看作一種代理模式,通過遠程代理,將網絡通信、數據編解碼等細節隱藏起來。客戶端在使用 RPC 服務的時候,就像使用本地函數一樣,無需了解跟服務器交互的細節。除此之外,RPC 服務的開發者也只需要開發業務邏輯,就像開發本地使用的函數一樣,不需要關注跟客戶端的交互細節。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM