前言
在软件开发中,我们经常遇到这样的难题:想给现有功能添加日志、权限校验等通用逻辑,但又不想修改原有代码。这就像装修时,想加装隔断,却又不愿直接砸墙,成本太高。这个问题可以通过“活动屏风”来优雅解决。Java 动态代理就像这面“代码屏风”,通过中间层实现功能的灵活扩展。
一、从租房中介看代理模式
假设你要租房,但不想直接和房东打交道。这时,房产中介会帮助你:筛选房源、谈判价格、处理合同——这就是现实中的代理模式。在 Java 中,动态代理就像一个“智能中介生成器”,它能自动创建各种服务的代理对象。接下来,看看实际案例。
二、为什么要使用动态代理?
场景重现:修改代码的噩梦
假设我们需要给 10 个业务类添加日志功能,传统方式可能需要这样修改:
// 原始代码
public class OrderService {
public void createOrder() {
// 业务逻辑
}
}
// 修改后的代码
public class OrderService {
public void createOrder() {
System.out.println("方法开始"); // 新增
// 业务逻辑
System.out.println("方法结束"); // 新增
}
}
当需要修改 100 个方法时,弊端显而易见:
- 代码重复严重
- 容易遗漏某些方法
- 后期维护困难
动态代理的三大优势
-
非侵入式增强
不需要修改原始代码即可实现功能扩展,符合开闭原则。 -
批量处理能力
一个代理处理器可以服务多个类或接口。 -
灵活组合特性
不同的Handler
可以组合实现日志、事务等多种功能。
三、动态代理初体验:为支付接口添加日志
假设我们有一个支付接口:
public interface Payment {
void pay(double amount);
}
public class Alipay implements Payment {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount);
}
}
传统的静态代理需要创建代理类:
public class StaticProxy implements Payment {
private Payment target;
public StaticProxy(Payment target) {
this.target = target;
}
@Override
public void pay(double amount) {
System.out.println("[静态代理]记录日志");
target.pay(amount);
}
}
而动态代理可以这样实现:
public class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[动态代理]方法执行前:" + method.getName());
Object result = method.invoke(target, args);
System.out.println("[动态代理]方法执行后");
return result;
}
}
// 使用动态代理
Payment alipay = new Alipay();
Payment proxy = (Payment) Proxy.newProxyInstance(
alipay.getClass().getClassLoader(),
new Class[]{Payment.class},
new LogHandler(alipay)
);
proxy.pay(100.0);
输出结果:
[动态代理]方法执行前:pay
支付宝支付:100.0
[动态代理]方法执行后
四、动态代理实现原理揭秘
通过 JDK 提供的 Proxy
类创建代理对象时,代理类结构大致如下:
- 类加载器:继承自目标对象的类加载器。
- 接口列表:代理类会实现所有指定接口。
- InvocationHandler:所有方法调用都会路由到
handler
。
生成的代理类结构类似于:
public final class $Proxy0 extends Proxy implements Payment {
public final void pay(double amount) {
try {
super.h.invoke(this, m3, new Object[]{amount});
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}
五、动态代理的典型应用场景
场景 | 实现方式 | 优势 |
---|---|---|
日志记录 | 在 invoke 方法中添加日志逻辑 | 无侵入式日志 |
事务管理 | 方法执行前开启事务,结束后提交 | 统一事务控制 |
权限校验 | 在方法调用前进行权限判断 | 集中权限管理 |
缓存加速 | 先查缓存,未命中再执行真实方法 | 提升系统性能 |
远程调用 | 将方法调用转换为网络请求 | 透明化远程通信 |
六、动态代理与静态代理的对比
对比维度 | 静态代理 | 动态代理 |
---|---|---|
代码量 | 代理类数量 = 接口数量 | 代理类数量 = 0(运行时生成) |
维护成本 | 修改 N 个代理类 | 修改 1 个处理器 |
执行效率 | 直接调用(较快) | 反射调用(稍慢但可优化) |
适用场景 | 接口数量少且稳定 | 接口数量多或频繁变化 |
扩展性 | 每新增接口需新建代理类 | 自动适配新接口 |
选择建议
- 当需要给少量固定接口添加简单功能时,静态代理更合适(例如只为支付接口添加验签)。
- 当面对多个接口或需要灵活组合功能时,动态代理是更优选择(例如同时给支付和订单添加日志和事务)。
七、动态代理的局限性及解决方案
-
只能代理接口
解决方案:使用 CGLIB 库实现类代理。 -
性能损耗
优化方案:缓存代理对象,避免重复创建。 -
自调用问题
错误示例:public class ServiceImpl implements Service { public void a() { b(); // 自调用不会走代理 } public void b() {} }
正确做法:通过代理对象调用。
八、Spring 框架中的实战应用
Spring AOP 的声明式事务正是基于动态代理实现的:
@Transactional
public void transfer(Account from, Account to, double amount) {
withdraw(from, amount);
deposit(to, amount);
}
Spring 通过动态代理自动为该方法添加:
- 开启事务。
- 提交/回滚事务。
- 异常处理。
九、手写简易动态代理框架
我们可以尝试实现一个简化版的动态代理:
public class MiniProxy {
public static Object newProxy(Class<?> intf, InvocationHandler h) {
return Proxy.newProxyInstance(
intf.getClassLoader(),
new Class[]{intf},
h
);
}
}
// 使用示例
Payment proxy = (Payment) MiniProxy.newProxy(
Payment.class,
(method, args) -> {
System.out.println("迷你代理");
return method.invoke(alipay, args);
}
);
十、总结与最佳实践
适用场景:
- 为多个类添加统一逻辑。
- 不确定未来要代理哪些类。
- 希望减少重复代理代码。
使用建议:
- 优先使用接口代理。
- 避免在
handler
中处理耗时操作。 - 合理使用缓存提升性能。
- 结合注解实现灵活控制。
结语
动态代理允许我们在不修改原有代码的情况下,灵活地为系统添加新功能。它帮助保持代码整洁,同时实现功能扩展,为开发带来更高的效率。