代理模式(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;
}
}
下面是代理类的实现,代理类采用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;
}
}
使用示例,对同一个商品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.lang.reflect.Proxy和java.lang.reflect.InvocationHandler接口来实现。 - 动态代理适合于需要对多个类进行代理的情况,可以减少重复代码的编写。
- 动态代理的缺点是相对复杂,需要理解Java的反射机制和动态代理的实现原理。
静态代理在编译时就确定了代理类和目标类的关系,而动态代理则是在运行时动态创建代理类,不需要为每个需要代理的类编写专门的代理类。动态代理比静态代理更加灵活,适合于需要对多个类进行代理的情况,但相对复杂一些。
而上面的例子中的实现方式就是静态代理,下面我们介绍动态代理的实现。