动态代理的核心价值在于 “动态” ,它在运行时按需生成代理逻辑,完美解决了静态代理在 未知性、重复性、动态性 场景下的局限性。
1. 代理的接口/方法在运行时才能确定
场景:代理的目标接口在编写代码时未知,需根据运行时配置、反射或外部输入动态生成。
-
典型场景:RPC 框架(如 Dubbo、gRPC)的客户端调用远程服务。
-
示例:
// 用户调用远程接口的某个方法,但该接口在框架中是动态加载的 public class RpcClient { public static <T> T getService(Class<T> interfaceClass) { // 动态生成代理对象,处理网络通信、序列化等 return (T) Proxy.newProxyInstance( interfaceClass.getClassLoader(), new Class<?>[] { interfaceClass }, new RpcInvocationHandler() // 处理远程调用逻辑 ); } } // 用户调用时,无需提前知道具体实现类 UserService userService = RpcClient.getService(UserService.class); User user = userService.getUserById(1); // 动态代理处理远程请求 -
静态代理的缺陷:
- 静态代理需要为每个接口(如
UserService、OrderService等)提前编写代理类。 - RPC 框架支持成百上千个接口,静态代理无法实现代码复用。
- 静态代理需要为每个接口(如
2. 需要为大量类统一添加相同功能
场景:为系统中的所有服务类自动添加日志、事务或权限校验,且类的数量庞大。
-
典型场景:Spring AOP 的切面编程。
-
示例:
// 动态代理实现日志切面 public class LogAspect implements InvocationHandler { private Object target; public LogAspect(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Method " + method.getName() + " is called."); return method.invoke(target, args); } } // Spring 容器中所有 Bean 的方法调用自动添加日志 @Bean public UserService userService() { UserService target = new UserServiceImpl(); return (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogAspect(target) ); } -
静态代理的缺陷:
- 若系统有 100 个服务类,需编写 100 个静态代理类,每个代理类中重复编写日志代码。
- 新增或修改日志逻辑时,需逐个修改所有代理类,维护成本极高。
3. 延迟加载(Lazy Loading)
场景:只有在实际调用方法时才触发耗时操作(如数据库查询、网络请求)。
-
典型场景:Hibernate 的懒加载(Lazy Loading)。
-
示例:
// Hibernate 返回的实体对象实际是动态代理 public class User { private List<Order> orders; // 关联订单(延迟加载) } // 获取 User 对象时,orders 字段是一个动态代理 User user = session.load(User.class, 1); List<Order> orders = user.getOrders(); // 此时才真正查询数据库 -
动态代理的实现逻辑:
- 代理对象在
getOrders()首次被调用时,才触发 SQL 查询加载数据。
- 代理对象在
-
静态代理的缺陷:
- 静态代理需在编译时确定所有可能触发加载的方法,而延迟加载的触发时机是动态的。
- 无法为未知的延迟行为提前编写代理逻辑。
4. 动态实现接口的适配
场景:运行时动态实现某个接口,并根据条件生成不同的逻辑。
-
典型场景:Mock 测试框架(如 Mockito)动态生成模拟对象。
-
示例:
// 使用 Mockito 动态生成一个 UserService 的模拟代理对象 UserService mockUserService = Mockito.mock(UserService.class); // 动态配置 mock 行为:当调用 getUserById(1) 时返回特定 User 对象 Mockito.when(mockUserService.getUserById(1)).thenReturn(new User("Alice")); -
动态代理的作用:
- Mockito 在运行时动态生成
UserService的代理对象,并根据when(...)的配置动态处理方法调用。
- Mockito 在运行时动态生成
-
静态代理的缺陷:
- 静态代理无法在运行时根据测试用例动态改变方法行为。
5. 统一拦截未知接口的所有方法
场景:需要对一个接口的所有方法进行统一拦截(如参数校验、性能监控),但无法提前预知接口的所有方法。
-
示例:接口方法参数校验:
// 动态代理统一检查方法参数是否为 null public class NullCheckHandler implements InvocationHandler { private Object target; public NullCheckHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { for (Object arg : args) { if (arg == null) { throw new IllegalArgumentException("参数不能为 null!"); } } return method.invoke(target, args); } } // 对任意接口生成代理 UserService userService = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[] { UserService.class }, new NullCheckHandler(new UserServiceImpl()) ); -
静态代理的缺陷:
- 静态代理需为每个方法显式编写参数校验代码,若接口有 20 个方法,需重复 20 次。
- 新增方法时,静态代理类需同步修改,而动态代理无需调整。
总结:动态代理的不可替代性
| 场景 | 动态代理的优势 | 静态代理的缺陷 |
|---|---|---|
| 运行时动态生成代理对象 | 无需提前编写代理类,适应未知接口/方法 | 需为每个接口/类手动编写代理类,无法应对动态变化 |
| 统一处理大量类的相同逻辑 | 通过一个 InvocationHandler 复用代码 | 每个类需单独编写代理类,代码冗余 |
| 延迟加载或动态触发行为 | 代理对象的行为在运行时按需触发(如懒加载) | 无法在编译时预知所有触发条件 |
| 动态适配接口方法逻辑 | 根据运行时条件生成不同逻辑(如 Mock 对象) | 需提前硬编码所有可能的行为,灵活性差 |