代理模式,你还不懂?

339 阅读5分钟

什么是代理模式

GoF 设计结构型模式,其中包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。今天,我们聊聊代理模式。代理模式,是实际开发中常用的一种设计模式。

代理模式(Proxy Design Pattern),在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

1920px-Proxy_pattern_diagram.svg.png

如何使用代理模式

在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能,如何做到呢?

只是从字面上理解,可能比较难,我们通过例子来描述吧。我们有一个后端服务,对外提供用户登录、登出等服务,我们需要统计登录、登出等操作的响应时间,具体代码如下:

public class UserController {
    // 省略其他方法和字段

    private MetricsService metricsService;// 性能统计服务,通过依赖注入
    
    public LoginVO login(String id, String password) {
        long startTime = System.currentTimeMillis();
        
        // 省略 login 业务逻辑
        
        long endTime = System.currentTimeMillis();
        long resolveTime = endTime - startTime;
        ResolveInfo resolveInfo = new ResolveInfo("login", resolveTime, startTime);
        metricsService.record(resolveInfo);// 记录响应时间
        
        // 返回 LoginVO
    }
    
    public LogoutVO logout(String id) {
        long startTime = System.currentTimeMillis();
        
        // 省略 logout 业务逻辑
        
        long endTime = System.currentTimeMillis();
        long resolveTime = endTime - startTime;
        ResolveInfo resolveInfo = new ResolveInfo("logout", resolveTime, startTime);
        metricsService.record(resolveInfo);// 记录响应时间
        
        // 返回 LogoutVO
    }
}

明显,上面的代码存在俩个问题:

  • 性能统计服务的逻辑侵入到业务代码中,与业务逻辑耦合,如果对性能统计服务调整,成本较高;
  • 单一原则方向出发,性能统计服务与业务逻辑无关,不应该放在同一类中,业务类应该聚焦业务;

基于接口的代理模式

为了解决上面代码耦合的问题,代理模式就出现了。引入新的代理类 UserControllerProxy ,代理类与 UserController 实现共同的接口 IUserController , UserController 只专注于具体业务逻辑,而代理类 UserControllerProxy 负责业务以外的附加逻辑,业务逻辑部分以委托 UserController 的形式执行业务逻辑。具体代码如下:

public interface IUserController {
    LoginVO login(String id, String password);
    LogoutVO logout(String id);
}

public class UserController implements IUserController {
    // 省略其他方法和字段
    
    public LoginVO login(String id, String password) {        
        // 省略 login 业务逻辑
        // 返回 LoginVO
    }
    
    public LogoutVO logout(String id) {
        // 省略 logout 业务逻辑
        // 返回 LogoutVO
    }
}

public class UserControllerProxy implements IUserController {
    // 省略其他方法和字段
    
    private UserController userController;
    private MetricsService metricsService;// 性能统计服务,通过依赖注入
    
    public UserControllerProxy(UserController userController) {
        this.userController = userController;
    }
    
    public LoginVO login(String id, String password) {
        long startTime = System.currentTimeMillis();
        
        LoginVO vo = userController.login(id, password);
        
        long endTime = System.currentTimeMillis();
        long resolveTime = endTime - startTime;
        ResolveInfo resolveInfo = new ResolveInfo("login", resolveTime, startTime);
        metricsService.record(resolveInfo);// 记录响应时间
        
        return vo;
    }
    
    public LogoutVO logout(String id) {
        long startTime = System.currentTimeMillis();
        
        LogoutVO vo = userController.logout(id);
        
        long endTime = System.currentTimeMillis();
        long resolveTime = endTime - startTime;
        ResolveInfo resolveInfo = new ResolveInfo("logout", resolveTime, startTime);
        metricsService.record(resolveInfo);// 记录响应时间
        
        return vo;
    }
}

...
IUserController userController = new UserControllerProxy(new UserController());

上面的例子,将原始类替换成代理类,附加与业务无关的逻辑在代理类中实现,避免了与业务逻辑的耦合问题,减少了代码改动。

基于继承的代理模式

上述例子中,原始类与代理类实现同一接口,但是,如果原始类为第三方类库,不由我们维护,我们无法直接修改原始类,我们应该怎么使用代理模式呢?这种情况下,我们一般是采用继承的方式,代理类继承原始类,然后再附加逻辑,具体代码如下:

public class UserController {
    // 省略其他方法和字段
    
    public LoginVO login(String id, String password) {        
        // 省略 login 业务逻辑
        // 返回 LoginVO
    }
    
    public LogoutVO logout(String id) {
        // 省略 logout 业务逻辑
        // 返回 LogoutVO
    }
}

public class UserControllerProxy extends UserController {
    // 省略其他方法和字段
    
    private MetricsService metricsService;// 性能统计服务,通过依赖注入
    
    public LoginVO login(String id, String password) {
        long startTime = System.currentTimeMillis();
        
        LoginVO vo = super.login(id, password);
        
        long endTime = System.currentTimeMillis();
        long resolveTime = endTime - startTime;
        ResolveInfo resolveInfo = new ResolveInfo("login", resolveTime, startTime);
        metricsService.record(resolveInfo);// 记录响应时间
        
        return vo;
    }
    
    public LogoutVO logout(String id) {
        long startTime = System.currentTimeMillis();
        
        LogoutVO vo = super.logout(id);
        
        long endTime = System.currentTimeMillis();
        long resolveTime = endTime - startTime;
        ResolveInfo resolveInfo = new ResolveInfo("logout", resolveTime, startTime);
        metricsService.record(resolveInfo);// 记录响应时间
        
        return vo;
    }
}

...
IUserController userController = new UserControllerProxy();

动态代理

无论是基于接口,还是基于继承的实现方式,都是存在问题的。

问题:

  • 代理类中每个方法中的逻辑都是相似的,重复性逻辑加大了开发维护成本;
  • 如果每次代理时都创建代理类,随着版本需求的迭代,代理类增多,代码维护成本也会随着增大;

此时,为了节省没必要的开发成本,动态代理(Dynamic Proxy)可以有效解决这个问题。动态代理,在运行时,动态创建原始类对应的代理类,运行时用代理类替换原始类。如果你对 Java 熟悉,Java 对于动态代理提供了支持,是基于反射实现的。同时 Spring AOP 也是基于动态代理实现的。

我们一起看下动态代理实现具体逻辑如下:

public class MetricsCollectorProxy {
  private MetricsService metricsService;// 性能统计服务,通过依赖注入

  public Object createProxy(Object proxiedObject) {
    Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
    DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
    return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
  }

  private class DynamicProxyHandler implements InvocationHandler {
    private Object proxiedObject;

    public DynamicProxyHandler(Object proxiedObject) {
      this.proxiedObject = proxiedObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      long startTime = System.currentTimeMillis();
      
      Object result = method.invoke(proxiedObject, args);
      
      long endTime = System.currentTimeMillis();
      long resolveTime = endTime - startTime;
      String opName = proxiedObject.getClass().getName() + ":" + method.getName();
      
      ResolveInfo resolveInfo = new ResolveInfo(opName, resolveTime, startTime);
      metricsCollector.recordRequest(requestInfo);
      
      return result;
    }
  }
}

...
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new UserController());

代理模式的使用场景

  1. 非功能性的需求,如监控、统计、鉴权、限流、事务、幂等、日志
  2. RPC 的调用。RPC 可以认为是一种代理模式,也被称为远程代理。因为 RPC 将网络通信、数据编码解码等等细节隐藏起来,所以客户端在使用 RPC 服务时,就像本地函数调用一样,不需要关注网络交互细节,只专注于业务逻辑即可。
  3. 缓存的应用。在生产环境下,某些查询接口计算逻辑复杂或者 IO 延时较长,我们应该如何解决问题呢?缓存是一个不错的方案。实现接口的缓存功能,入参相同,在一定时间周期内,返回缓存结果,可以避免接口请求超时。基于动态代理,结合配置、缓存策略(timeout),请求到达时,代理拦截请求,满足时直接返回缓存,否则实时计算查询。