跳到主要内容

代理模式(Proxy Pattern)

序言

代理模式(Proxy Pattern)是一种结构型设计模式,它充当了另一个对象的接口,以控制对这个对象的访问。

代理模式的核心思想是通过引入一个代理对象来间接访问另一个对象,从而可以在访问这个对象时添加一些额外的控制逻辑,比如权限验证、缓存、延迟加载等。代理模式可以帮助我们在不改变原始对象的情况下,对其进行控制和扩展。

在日益追求高效与解耦的现代软件工程实践中,代理模式的应用愈发广泛。从Web服务中的远程代理,到大数据处理中的虚拟代理,再到日常编程中的智能指针,代理模式的身影无处不在。特别是在需要对对象的访问进行控制和扩展的情况下,代理模式可以提供一种灵活的解决方案。

然而,正如所有强大的工具一样,代理模式也不是没有代价。它可能会增加系统的复杂性,使得代码的理解和调试变得更加困难。因此,何时使用代理模式,以及如何正确地使用它,成为了每位软件工程师必须审慎考虑的问题。

定义

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

为另一个对象提供代理或占位符以控制对其的访问。

结构

想象一下,有一个商品查询的功能,该功能提供一个可以根据商品ID查询商品基本信息的功能。

现在为了提升查询效率,如何在不改变ProductDetailsQueryServiceImpl代码的情况下,实现这个功能?

答案就是代理模式。通过创建一个代理类,在访问 getProductDetails 方法的时候先根据 productId 查询缓存中是否存在,如果缓存中存在,直接从缓存中获取,如果缓存中不存在,再调用被代理类的 getProductDetails 方法。

类的结构上来说,它和装饰模式(Decorator Pattern)很像,两者都实现了原有的接口。但是两者又有着根本的区别。装饰模式主要的目的是在原有功能的基础上扩展新的功能。而代理模式则不同,代理模式主要是控制对原有功能的访问。

装饰模式最后一定会调用原有功能接口,而代理模式可能不会调用原有功能接口。

代码实现

@Data
public class Product {
private String productId;
private String productName;
private String productDesc;
}

public interface ProductDetailsQueryService {
Product getProductDetails(String productId);
}

public class ProductDetailsQueryServiceImpl implements ProductDetailsQueryService{
@Override
public Product getProductDetails(String productId) {
Product product = new Product();
product.setProductName("商品:" + productId);
product.setProductDesc("商品描述:" + productId);
// 实际上应该从数据库中获取,而且返回值可能为null
return product;
}
}
java

下面是代理类的实现,代理类采用caffeine缓存框架。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class ProductDetailsQueryServiceProxy implements ProductDetailsQueryService {

private final ProductDetailsQueryService productDetailsQueryService;

private final Cache<String, Product> productCache = Caffeine.newBuilder().maximumSize(1000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();

public ProductDetailsQueryServiceProxy(ProductDetailsQueryService productDetailsQueryService) {
this.productDetailsQueryService = productDetailsQueryService;

}

@Override
public Product getProductDetails(String productId) {
Product product = productCache.getIfPresent(productId);
if (product == null) {
System.out.println("从数据库中读取商品信息");
product = productDetailsQueryService.getProductDetails(productId);
productCache.put(productId, product);
} else {
System.out.println("从缓存中读取商品信息");
}
return product;
}
}

java

使用示例,对同一个商品ID连续查询两次,然后线程休眠10s后再读取一次。

public class Main {

public static void main(String[] args) throws InterruptedException {

ProductDetailsQueryService productDetailsQueryService = new ProductDetailsQueryServiceImpl();
ProductDetailsQueryService productDetailsQueryServiceProxy = new ProductDetailsQueryServiceProxy(productDetailsQueryService);
Product product = productDetailsQueryServiceProxy.getProductDetails("123");
Product product2 = productDetailsQueryServiceProxy.getProductDetails("123");

TimeUnit.SECONDS.sleep(10);

Product product3 = productDetailsQueryServiceProxy.getProductDetails("123");
}
}
java

输出结果

从数据库中读取商品信息
从缓存中读取商品信息
从数据库中读取商品信息

代理分类

代理分为静态代理和动态代理。它们之间的区别如下:

静态代理:

  • 在编译时就已经确定代理类和目标类的关系,代理类是在编译期间就创建好的。
  • 静态代理需要为每个需要代理的类编写一个代理类,因此会导致类的数量增加。
  • 静态代理的优点是简单直观,容易理解和实现。

动态代理:

  • 在运行时动态创建代理类,不需要为每个需要代理的类编写专门的代理类。
  • Java中的动态代理主要使用java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler接口来实现。
  • 动态代理适合于需要对多个类进行代理的情况,可以减少重复代码的编写。
  • 动态代理的缺点是相对复杂,需要理解Java的反射机制和动态代理的实现原理。

静态代理在编译时就确定了代理类和目标类的关系,而动态代理则是在运行时动态创建代理类,不需要为每个需要代理的类编写专门的代理类。动态代理比静态代理更加灵活,适合于需要对多个类进行代理的情况,但相对复杂一些。

而上面的例子中的实现方式就是静态代理,下面我们介绍动态代理的实现。

JDK动态代理

JDK动态代理是通过java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler接口来实现的。其中ProductProductDetailsQueryServiceProductDetailsQueryServiceImpl不用修改。

具体实现如下:

public class ProductDetailsQueryServiceInvocationHandler implements InvocationHandler {

private final ProductDetailsQueryService productDetailsQueryService;

private final Cache<String, Object> productCache = Caffeine.newBuilder().maximumSize(1000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();

public ProductDetailsQueryServiceInvocationHandler(ProductDetailsQueryService productDetailsQueryService) {
this.productDetailsQueryService = productDetailsQueryService;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String productId = (String) args[0];
Object product = productCache.getIfPresent(productId);
if (product == null) {
System.out.println("JDK Proxy:从数据库中读取商品信息");
product = method.invoke(productDetailsQueryService, args);
productCache.put(productId, product);
} else {
System.out.println("JDK Proxy:从缓存中读取商品信息");
}
return product;
}
}
java

使用JDK动态代理

public class JdkProxyMain {

public static void main(String[] args) throws InterruptedException {

ProductDetailsQueryService realSubject = new ProductDetailsQueryServiceImpl();
InvocationHandler handler = new ProductDetailsQueryServiceInvocationHandler(realSubject);
ProductDetailsQueryService proxy = (ProductDetailsQueryService) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
handler
);

Product product = proxy.getProductDetails("123");
Product product2 = proxy.getProductDetails("123");

TimeUnit.SECONDS.sleep(10);

Product product3 = proxy.getProductDetails("123");
}
}
java

输出结果

从数据库中读取商品信息
从缓存中读取商品信息
从数据库中读取商品信息

cglib动态代理

cglib动态代理是通过MethodInterceptorEnhancer来实现的。

public class ProductDetailsQueryServiceMethodInterceptor implements MethodInterceptor {
private final Cache<String, Object> productCache = Caffeine.newBuilder().maximumSize(1000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();

public ProductDetailsQueryServiceMethodInterceptor() {
}

@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
String productId = (String) args[0];
Object product = productCache.getIfPresent(productId);
if (product == null) {
System.out.println("cglib Proxy:从数据库中读取商品信息");
product = proxy.invokeSuper(o, args);
productCache.put(productId, product);
} else {
System.out.println("cglib Proxy:从缓存中读取商品信息");
}
return product;
}

}
java

使用cglib动态代理

public class CglibMain {

public static void main(String[] args) throws InterruptedException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ProductDetailsQueryServiceImpl.class);
enhancer.setInterfaces(new Class[]{ProductDetailsQueryService.class});
enhancer.setCallback(new ProductDetailsQueryServiceMethodInterceptor());

ProductDetailsQueryService proxy = (ProductDetailsQueryService) enhancer.create();
Product product = proxy.getProductDetails("123");
Product product2 = proxy.getProductDetails("123");

TimeUnit.SECONDS.sleep(10);

Product product3 = proxy.getProductDetails("123");
}
}
java

输出结果

从数据库中读取商品信息
从缓存中读取商品信息
从数据库中读取商品信息

在开源框架中的应用

代理模式最典型的一个应用就是Spring AOP(Aspect-Oriented Programming)。Spring通过以下两种方式实现动态代理

  • 基于JDK动态代理:当目标对象实现了接口时,Spring会使用JDK动态代理来创建代理对象。
  • 基于CGLIB动态代理:当目标对象没有实现接口时,Spring会使用CGLIB动态代理来创建代理对象。

限于篇幅,后续专门做Spring AOP源码解析,就不在本文中做详细解释了。

MyBatis动态代理

动态代理另一个典型的框架是MyBatis(其实ORM框架都大量使用了动态代理,包括Hibernate和Spring Data JPA),以下简单介绍下MyBatis动态代理的使用。Mybatis动态代理主要用在我们通过调用Mapper中的接口,执行数据库的DML操作上。

通过SqlSession#getMapper方法可是获取到一个Mapper代理。最终实际上是通过MapperProxyFactory#newInstance来创建Mapper代理的。

public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

public Class<T> getMapperInterface() {
return mapperInterface;
}

public Map<Method, MapperMethodInvoker> getMethodCache() {
return methodCache;
}

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

}
java

其核心代理类为:org.apache.ibatis.binding.MapperProxyMapperProxy实现了InvocationHandler接口,当调用Mapper中的接口时,实际上走到了org.apache.ibatis.binding.MapperProxy#invoke方法。

  @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
mapperproxy#invoke

本文中仅介绍动态代理在MyBatis中的使用,对于MyBatis的原理不做详细解释,后续会专门写具体的系列文章进行详细解释。

何时使用

代理模式通常在以下情况下使用:

  • 远程代理: 当需要在不同地址空间中访问对象时,可以使用远程代理来隐藏对象存在于不同地址空间的细节,使得客户端可以像访问本地对象一样访问远程对象。
  • 虚拟代理: 当需要延迟加载大对象或者控制对对象的访问时,可以使用虚拟代理来在需要时才创建或加载对象,从而提高系统的性能和响应速度。
  • 保护代理: 当需要控制对对象的访问权限时,可以使用保护代理来进行权限验证,以确保客户端有访问对象的权限。
  • 缓存代理: 当需要对对象的访问进行缓存时,可以使用缓存代理来缓存对象的访问结果,以提高系统的性能。

代理模式适用于需要在访问对象时添加一些额外控制逻辑的情况,比如延迟加载、权限验证、缓存等。

与其他设计模式的联系

代理模式(Proxy Pattern)是一个与装饰模式(Decorator Pattern)极易混淆的模式。但是两者又有着一些区别。具体见 装饰模式(Decorator Pattern)

总结

代理模式的优点在于它可以有效地控制对对象的访问,同时可以在不改变原对象的基础上增加新的功能。此外,代理模式还可以实现延迟加载和降低系统的耦合度。

然而,代理模式也存在一些缺点。首先,由于引入了中介对象,系统的复杂性会增加。其次,代理模式可能会增加系统的响应时间,因为对原对象的操作都需要经过代理对象。

使用代理模式时也需要注意其可能带来的问题,如系统复杂性的增加和响应时间的延长。因此,在实际的软件设计中,需要根据具体的需求场景来权衡是否使用代理模式。