1. 代理模式的核心概念与应用场景
代理模式(Proxy Pattern)是23种经典设计模式中结构型模式的代表之一。它的核心思想是通过创建一个代理对象来控制对原始对象的访问,这种控制可以体现在访问时机、访问方式或访问权限等多个维度。就像现实生活中的房产中介代理房东出租房屋一样,代理对象作为原始对象的"中间人",为客户端提供间接访问的途径。
1.1 为什么需要代理模式
在实际开发中,我们经常会遇到以下几种典型场景:
-
远程对象访问:当目标对象位于不同的地址空间(如远程服务器)时,直接访问会面临网络通信、序列化等复杂问题。就像我们无法亲自去火车站购票时,代售点就成了必要的代理。
-
创建开销大的对象:某些对象的初始化需要消耗大量资源(如数据库连接、大型图像加载),使用代理可以实现延迟加载(Lazy Loading)。
-
安全控制需求:当需要对原始对象的访问进行权限校验时,代理可以在调用前后插入安全检查逻辑。
-
功能增强需求:在不修改原始对象代码的前提下,代理可以添加日志记录、性能统计等辅助功能。
提示:代理模式与装饰器模式在结构上相似,但目的不同。装饰器模式重在功能增强,而代理模式重在访问控制。
1.2 代理模式的三种主要类型
根据使用场景的不同,代理模式可以分为以下几种常见类型:
-
远程代理(Remote Proxy):为位于不同地址空间的对象提供本地代表。Java RMI就是典型的远程代理实现。
-
虚拟代理(Virtual Proxy):根据需要创建开销大的对象,如图片加载代理。
-
保护代理(Protection Proxy):控制对原始对象的访问权限,常用于安全控制场景。
-
智能引用代理(Smart Reference Proxy):在访问对象时执行附加操作,如引用计数、线程安全检查等。
2. 代理模式的结构与实现
2.1 类图解析
代理模式的通用类图结构包含三个核心角色:
-
Subject(抽象主题):定义真实主题和代理主题的共同接口,这样在任何使用真实主题的地方都可以使用代理主题。
-
RealSubject(真实主题):实现真正的业务逻辑,是代理所代表的实际对象。
-
Proxy(代理):持有对真实主题的引用,客户端通过代理间接访问真实主题。代理可以在调用真实主题方法前后添加预处理和后处理逻辑。

2.2 代码实现详解
让我们通过一个完整的Java示例来理解代理模式的具体实现。这个例子模拟了火车票销售场景,其中TrainSeller是实际的票务系统,TicketProxy是面向客户的代售点。
java复制// 1. 定义抽象主题接口
public interface TicketSeller {
void sellTicket();
void refundTicket();
}
// 2. 实现真实主题
public class TrainTicketSystem implements TicketSeller {
@Override
public void sellTicket() {
System.out.println("[真实系统] 火车票出票中...");
// 实际的票务系统逻辑
}
@Override
public void refundTicket() {
System.out.println("[真实系统] 正在处理退票...");
// 实际的退票逻辑
}
}
// 3. 实现代理类
public class TicketProxy implements TicketSeller {
private TrainTicketSystem realSystem;
private boolean accessAllowed;
public TicketProxy() {
this.realSystem = new TrainTicketSystem();
this.accessAllowed = checkAccess();
}
private boolean checkAccess() {
// 模拟权限检查
System.out.println("[代理] 正在验证客户端权限...");
return Math.random() > 0.3; // 70%通过率
}
@Override
public void sellTicket() {
if (!accessAllowed) {
System.out.println("[代理] 错误:客户端无权限访问票务系统");
return;
}
System.out.println("[代理] 预处理:记录售票请求日志");
long start = System.currentTimeMillis();
realSystem.sellTicket(); // 委托给真实系统
long duration = System.currentTimeMillis() - start;
System.out.printf("[代理] 后处理:售票操作耗时%dms\n", duration);
}
@Override
public void refundTicket() {
// 类似的代理逻辑
}
}
// 4. 客户端使用
public class Client {
public static void main(String[] args) {
TicketSeller seller = new TicketProxy();
seller.sellTicket();
}
}
在这个实现中,代理类TicketProxy做了以下几件事:
- 权限验证:检查客户端是否有权访问票务系统
- 日志记录:记录每个售票请求
- 性能监控:统计方法执行时间
- 访问控制:根据验证结果决定是否转发请求
2.3 动态代理技术
除了上述静态代理方式,Java还提供了动态代理机制,可以在运行时动态创建代理类。这主要通过java.lang.reflect.Proxy类实现:
java复制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 {
System.out.println("动态代理预处理");
Object result = method.invoke(target, args);
System.out.println("动态代理后处理");
return result;
}
}
// 使用方式
TicketSeller realSystem = new TrainTicketSystem();
TicketSeller proxy = (TicketSeller) Proxy.newProxyInstance(
TicketSeller.class.getClassLoader(),
new Class[]{TicketSeller.class},
new DynamicProxyHandler(realSystem)
);
proxy.sellTicket();
动态代理的优势在于可以为多个接口创建统一的代理逻辑,而不需要为每个接口编写单独的代理类。Spring AOP就是基于动态代理实现的。
3. 代理模式的典型应用场景
3.1 远程服务调用
在分布式系统中,客户端需要调用远程服务时,代理模式是必不可少的。例如:
-
RPC框架:Dubbo、gRPC等框架都会为远程服务生成本地代理,使开发者可以像调用本地方法一样调用远程服务。
-
WebService客户端:通过代理封装复杂的SOAP协议和网络通信细节。
java复制// 伪代码:电商支付接口代理示例
public class PaymentProxy implements PaymentService {
private RemotePaymentService remoteService;
public PaymentResult pay(Order order) {
// 序列化请求
byte[] request = serialize(order);
// 发送网络请求
byte[] response = sendOverNetwork(request);
// 反序列化响应
return deserialize(response);
}
}
3.2 延迟加载与缓存
对于创建成本高的对象,可以使用虚拟代理实现延迟加载:
java复制public class ImageProxy implements Image {
private String filePath;
private RealImage realImage;
public ImageProxy(String path) {
this.filePath = path;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filePath); // 实际使用时才加载
}
realImage.display();
}
}
3.3 访问控制与安全
代理可以用于实现细粒度的访问控制:
java复制public class SensitiveOperationProxy implements SensitiveOperations {
private User user;
private SensitiveOperations realService;
public SensitiveOperationProxy(User user) {
this.user = user;
this.realService = new SensitiveOperationsImpl();
}
@Override
public void deleteDatabase() {
if (!user.hasPermission(Permission.ADMIN)) {
throw new SecurityException("权限不足");
}
realService.deleteDatabase();
}
}
4. 代理模式的实践技巧与陷阱
4.1 性能优化建议
- 缓存代理结果:对于计算密集型或IO密集型的操作,代理可以缓存结果避免重复计算。
java复制public class ExpensiveCalculationProxy implements Calculator {
private Calculator realCalculator;
private Map<String, BigDecimal> cache = new HashMap<>();
@Override
public BigDecimal calculate(String expression) {
if (cache.containsKey(expression)) {
return cache.get(expression);
}
BigDecimal result = realCalculator.calculate(expression);
cache.put(expression, result);
return result;
}
}
- 批量操作优化:将多个细粒度操作合并为批量操作,减少网络开销。
4.2 常见问题与解决方案
问题1:代理导致调用链过长
多层代理嵌套可能导致调用栈过深,影响性能和可调试性。
解决方案:
- 控制代理层级,避免过度设计
- 使用组合代替嵌套,将多个代理逻辑合并到一个代理中
问题2:动态代理的局限性
Java动态代理只能代理接口,不能代理类。
解决方案:
- 对于需要代理类的情况,可以使用CGLIB等字节码操作库
- 或者考虑重构设计,面向接口编程
问题3:循环依赖
当代理和被代理对象相互引用时,可能导致循环依赖。
解决方案:
- 使用懒加载打破循环
- 引入第三方对象管理依赖关系
4.3 测试策略
代理类的测试应关注:
- 代理逻辑是否正确执行(如权限检查、日志记录)
- 代理是否正确转发请求给真实对象
- 异常处理是否合理
java复制@Test
public void testProxyAccessControl() {
// 创建无权限用户
User guest = new User("guest", Role.GUEST);
SensitiveOperations proxy = new SensitiveOperationProxy(guest);
// 验证无权限访问时抛出异常
assertThrows(SecurityException.class, () -> proxy.deleteDatabase());
}
@Test
public void testProxyMethodForwarding() {
// 创建mock真实对象
RealService mockService = mock(RealService.class);
ServiceProxy proxy = new ServiceProxy(mockService);
// 调用代理方法
proxy.doSomething();
// 验证真实对象方法被调用
verify(mockService).doSomething();
}
5. 代理模式在现代框架中的应用
5.1 Spring AOP中的代理
Spring框架广泛使用代理模式实现AOP(面向切面编程)。根据目标类是否实现接口,Spring会选择使用JDK动态代理或CGLIB代理:
- JDK动态代理:基于接口,运行时生成实现相同接口的代理类。
- CGLIB代理:通过继承目标类生成子类代理,适用于没有接口的类。
java复制@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("调用方法: " + joinPoint.getSignature());
}
}
5.2 MyBatis的Mapper代理
MyBatis通过动态代理将Java接口转换为SQL执行:
java复制public class MapperProxy<T> implements InvocationHandler {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 将方法调用转换为SQL执行
String statement = mapperInterface.getName() + "." + method.getName();
return sqlSession.selectOne(statement, args);
}
}
5.3 RPC框架中的服务代理
Dubbo等RPC框架通过代理隐藏远程调用细节:
java复制public class DubboProxy {
public static <T> T getProxy(Class<T> interfaceClass) {
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[]{interfaceClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 构造RPC请求
RpcRequest request = createRequest(interfaceClass, method, args);
// 发送网络请求
return sendRequest(request);
}
});
}
}
在实际项目中使用代理模式时,我通常会先评估是否真的需要引入代理。对于简单的功能增强,有时候装饰器模式可能更合适;而对于跨进程调用等复杂场景,代理模式几乎是必然选择。一个实用的建议是:当发现自己在重复编写相似的样板代码(如日志、权限检查)时,考虑使用代理模式将这些横切关注点统一处理。