什么是代理模式
GoF 设计结构型模式,其中包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。今天,我们聊聊代理模式。代理模式,是实际开发中常用的一种设计模式。
代理模式(Proxy Design Pattern),在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
如何使用代理模式
在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能,如何做到呢?
只是从字面上理解,可能比较难,我们通过例子来描述吧。我们有一个后端服务,对外提供用户登录、登出等服务,我们需要统计登录、登出等操作的响应时间,具体代码如下:
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());
代理模式的使用场景
非功能性的需求,如监控、统计、鉴权、限流、事务、幂等、日志
;RPC 的调用。
RPC 可以认为是一种代理模式,也被称为远程代理。因为 RPC 将网络通信、数据编码解码等等细节隐藏起来,所以客户端在使用 RPC 服务时,就像本地函数调用一样,不需要关注网络交互细节,只专注于业务逻辑即可。缓存的应用。
在生产环境下,某些查询接口计算逻辑复杂或者 IO 延时较长,我们应该如何解决问题呢?缓存是一个不错的方案。实现接口的缓存功能,入参相同,在一定时间周期内,返回缓存结果,可以避免接口请求超时。基于动态代理,结合配置、缓存策略(timeout),请求到达时,代理拦截请求,满足时直接返回缓存,否则实时计算查询。