1. 从“中间人”到“代码魔术”:为什么我们需要代理模式?
在Java开发中,尤其是当你需要为一个已有的类添加一些额外功能,比如日志记录、性能监控、事务管理或者权限校验,但又不想、也不能直接修改这个类的源代码时,你会怎么做?直接修改源码,违反了“开闭原则”,也让代码变得臃肿且难以维护。这时候,一个经典的设计模式——代理模式(Proxy Pattern)就派上了用场。
你可以把代理想象成一个“中间人”或者“秘书”。比如,你想约见一位大忙人(目标对象),直接联系他可能很困难,或者他需要你提前准备好所有材料。这时,他的秘书(代理对象)就出现了。你联系秘书,秘书会帮你预约时间、检查你的材料是否齐全,甚至帮你处理一些前置或后置的事务(比如记录会面时间、通知相关部门),最后再帮你安排与目标对象的会面。在整个过程中,你感觉是在和目标对象直接交互,但实际上,是代理对象在为你打理一切。
在Java世界里,这个“秘书”就是代理对象,它持有对真实对象(目标对象)的引用,并在调用真实对象的方法前后,插入我们需要的“增强”逻辑。根据这个“秘书”是提前雇佣好的(编译期确定),还是根据情况临时生成的(运行期动态创建),我们将代理分为静态代理和动态代理。理解它们的区别和实现,不仅是应对面试八股文的必备技能,更是写出高扩展性、低耦合度代码的实用工具。接下来,我们就深入这个“中间人”的世界,看看静态代理如何按部就班,动态代理又如何施展“代码魔术”。
2. 静态代理:手写“中间人”的清晰与局限
静态代理,顾名思义,代理关系在编译期就已经确定下来了。我们需要手动创建一个代理类,这个代理类实现了和目标对象相同的接口(或者继承自同一个父类),并在内部持有一个目标对象的实例。所有对目标方法的调用,都会经过这个代理类的方法进行转发,我们就在这个转发过程中加入自己的逻辑。
2.1 静态代理的标准实现步骤
让我们通过一个最经典的场景来理解:为服务层的业务方法添加日志记录。
首先,定义业务接口。这是代理模式和目标对象都需要遵守的“契约”。
// 1. 定义业务接口 public interface UserService { void addUser(String username); void deleteUser(String id); }接着,创建目标对象,即接口的真实实现类。
// 2. 目标对象(真实实现) public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("添加用户: " + username); // 模拟业务逻辑... } @Override public void deleteUser(String id) { System.out.println("删除用户,ID: " + id); // 模拟业务逻辑... } }现在,关键角色登场——静态代理类。它也必须实现UserService接口。
// 3. 静态代理类 public class UserServiceStaticProxy implements UserService { // 持有目标对象的引用 private UserService target; // 通过构造器注入目标对象 public UserServiceStaticProxy(UserService target) { this.target = target; } @Override public void addUser(String username) { // **前置增强**:在调用真实方法前执行 System.out.println("[静态代理] 开始记录日志 -> 准备执行 addUser, 参数: " + username); long startTime = System.currentTimeMillis(); // 调用目标对象的方法 target.addUser(username); // **后置增强**:在调用真实方法后执行 long endTime = System.currentTimeMillis(); System.out.println("[静态代理] 方法执行完毕 -> 执行 addUser 耗时: " + (endTime - startTime) + "ms"); } @Override public void deleteUser(String id) { System.out.println("[静态代理] 开始记录日志 -> 准备执行 deleteUser, 参数: " + id); long startTime = System.currentTimeMillis(); target.deleteUser(id); long endTime = System.currentTimeMillis(); System.out.println("[静态代理] 方法执行完毕 -> 执行 deleteUser 耗时: " + (endTime - startTime) + "ms"); } }最后,在客户端(调用方)中,我们不再直接使用UserServiceImpl,而是使用它的代理类。
// 4. 客户端使用 public class Client { public static void main(String[] args) { // 创建目标对象 UserService realService = new UserServiceImpl(); // 创建代理对象,并将目标对象传入 UserService proxy = new UserServiceStaticProxy(realService); // 通过代理对象调用方法 proxy.addUser("张三"); System.out.println("----------"); proxy.deleteUser("001"); } }运行上面的Client类,你会看到类似下面的输出,清晰地展示了代理在方法执行前后插入的日志和耗时统计:
[静态代理] 开始记录日志 -> 准备执行 addUser, 参数: 张三 添加用户: 张三 [静态代理] 方法执行完毕 -> 执行 addUser 耗时: 0ms ---------- [静态代理] 开始记录日志 -> 准备执行 deleteUser, 参数: 001 删除用户,ID: 001 [静态代理] 方法执行完毕 -> 执行 deleteUser 耗时: 0ms2.2 静态代理的优缺点与适用场景
静态代理的优点非常明显:结构清晰,易于理解和上手。代理类和目标类的关系一目了然,符合面向对象的设计思想。对于小型项目或者需要增强的方法数量很少且固定的情况,静态代理是一个不错的选择。
然而,它的缺点也同样突出,这直接导致了其在复杂项目中的局限性:
- 冗余与僵化:每一个需要被代理的类,我们都需要手动编写一个对应的代理类。如果
UserService接口有10个方法,即使你只想代理其中的2个,代理类也必须实现全部10个方法,并在不想增强的方法里简单调用目标对象的方法。这会产生大量的模板代码。 - 紧耦合:代理类与目标类/接口紧密耦合。一旦接口(
UserService)发生变更,比如增加或修改了一个方法,那么所有实现了该接口的代理类都必须同步修改,维护成本高。 - 不灵活:无法在运行时动态地为一个新出现的类创建代理。代理关系在编码时就已经写死了。
正因为这些局限,当我们需要为大量类或方法提供类似增强(如日志、事务)时,静态代理就显得力不从心。这时,我们就需要更强大的武器——动态代理。
3. JDK动态代理:基于接口的运行时“造物”
动态代理的核心思想是:在程序运行时,动态地在内存中创建代理类及其对象。我们无需为每个目标类手动编写代理类代码。在Java中,实现动态代理主要有两种方式:JDK原生动态代理和第三方库(如CGLIB)。我们先看JDK自带的方案。
JDK动态代理的核心是java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。它的一个关键限制是:它只能为接口创建代理,无法代理没有实现任何接口的普通类。这是因为它生成的代理类默认继承了Proxy类(Java是单继承),所以只能通过实现接口的方式来实现代理。
3.1 JDK动态代理的实现机制
我们来改造上面的例子,用JDK动态代理实现同样的日志增强功能。
首先,业务接口UserService和目标类UserServiceImpl保持不变。
然后,我们需要创建一个InvocationHandler的实现类。这个类是增强逻辑的集中地。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; // 动态代理的调用处理器 public class LogInvocationHandler implements InvocationHandler { // 持有目标对象(真实对象) private final Object target; public LogInvocationHandler(Object target) { this.target = target; } /** * 代理对象任何方法被调用时,都会触发此方法。 * @param proxy 代理对象本身(通常很少直接使用) * @param method 被调用的方法对象(反射对象) * @param args 调用方法时传入的参数 * @return 方法的返回值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强 System.out.println("[JDK动态代理] 开始记录日志 -> 准备执行 " + method.getName() + ", 参数: " + (args != null ? String.join(", ", (CharSequence[]) args) : "无")); long startTime = System.currentTimeMillis(); // 利用反射,调用目标对象的方法 Object result = method.invoke(target, args); // 后置增强 long endTime = System.currentTimeMillis(); System.out.println("[JDK动态代理] 方法执行完毕 -> 执行 " + method.getName() + " 耗时: " + (endTime - startTime) + "ms"); // 返回目标方法执行的结果 return result; } }最后,在客户端,我们使用Proxy.newProxyInstance方法来动态创建代理对象。
import java.lang.reflect.Proxy; public class JdkDynamicProxyClient { public static void main(String[] args) { // 1. 创建目标对象 UserService realService = new UserServiceImpl(); // 2. 创建调用处理器,并传入目标对象 InvocationHandler handler = new LogInvocationHandler(realService); // 3. 动态创建代理对象 // 参数1:类加载器,通常用目标类的类加载器 // 参数2:代理类需要实现的接口数组 // 参数3:调用处理器实例 UserService proxy = (UserService) Proxy.newProxyInstance( realService.getClass().getClassLoader(), // 类加载器 realService.getClass().getInterfaces(), // 目标对象实现的接口 handler // 自定义的InvocationHandler ); // 4. 通过代理对象调用方法 System.out.println("代理对象的类型: " + proxy.getClass().getName()); proxy.addUser("李四"); System.out.println("----------"); proxy.deleteUser("002"); } }运行这段代码,输出与静态代理类似,但你会注意到代理对象的类名非常特殊,类似com.sun.proxy.$Proxy0,这就是JVM在运行时为我们动态生成的代理类。
3.2 深入理解Proxy.newProxyInstance
这个方法是我们接触JDK动态代理的入口,理解它的三个参数至关重要:
- ClassLoader loader:用于加载动态生成的代理类的类加载器。通常传入目标对象的类加载器(
target.getClass().getClassLoader()),确保代理类和目标类在同一个类加载器环境下,能访问到相同的接口。 - Class<?>[] interfaces:代理类需要实现的接口列表。因为JDK动态代理是基于接口的,所以必须指定。通常传入目标对象实现的所有接口(
target.getClass().getInterfaces())。 - InvocationHandler h:最重要的部分,即我们自定义的调用处理器。代理对象所有方法的调用,都会被路由到它的
invoke方法中。在这里,我们通过反射调用原始方法,并环绕它添加增强逻辑。
一个重要的实操心得:在
InvocationHandler的invoke方法内部,如果你调用method.invoke(proxy, args)(即用代理对象自己作为目标),会导致无限递归调用,最终栈溢出。一定要调用method.invoke(target, args),target才是我们传入的真实对象。
3.3 JDK动态代理的优缺点分析
优点:
- 解耦与通用:代理类与目标类完全解耦。一个
InvocationHandler可以用于代理多个不同的接口和类,只要增强逻辑相同。这避免了静态代理的大量重复编码。 - 灵活:可以在运行时动态地为不同的接口创建代理,非常灵活。
- 符合设计原则:很好地遵循了“对扩展开放,对修改关闭”的开闭原则。
缺点:
- 只能代理接口:这是其最根本的限制。如果你的业务类没有实现任何接口,JDK动态代理就无能为力了。
- 反射调用性能开销:通过
Method.invoke进行反射调用,其性能比直接方法调用要差。但在绝大多数应用场景下,这点开销是可以接受的,尤其是相比于它带来的灵活性优势。 - 内部细节黑盒:动态生成的代理类对我们来说是黑盒,调试时可能不太直观。
4. CGLIB动态代理:突破接口限制的“继承”魔法
为了解决JDK动态代理必须基于接口的限制,我们就需要用到第三方库,最著名的就是CGLIB(Code Generation Library)。CGLIB通过继承目标类的方式创建子类,并在子类中重写父类的方法来实现代理。因此,它甚至可以代理没有实现任何接口的普通类。
4.1 CGLIB动态代理的实现步骤
首先,需要引入CGLIB的依赖。如果你使用Maven,可以在pom.xml中添加:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> <!-- 请使用最新稳定版本 --> </dependency>这次,我们创建一个没有实现接口的普通类作为目标。
// 目标类,没有实现任何接口 public class ProductService { public void saveProduct(String name) { System.out.println("保存产品: " + name); } public final void finalMethod() { System.out.println("这是一个final方法,无法被代理增强"); } }注意,我们特意加了一个final方法。CGLIB通过生成子类来代理,而final方法是不能被重写的,所以它无法被代理增强。
接下来,创建一个方法拦截器,它类似于JDK动态代理中的InvocationHandler。
import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class LogMethodInterceptor implements MethodInterceptor { /** * 拦截目标类所有非final方法的调用 * @param obj 代理对象(CGLIB生成的子类对象) * @param method 被拦截的方法(目标类的方法) * @param args 方法参数 * @param proxy 用于调用父类(即目标类)原始方法的代理对象 * @return 方法的返回值 * @throws Throwable */ @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 前置增强 System.out.println("[CGLIB动态代理] 开始记录日志 -> 准备执行 " + method.getName() + ", 参数: " + (args != null ? String.join(", ", (CharSequence[]) args) : "无")); long startTime = System.currentTimeMillis(); // 关键:调用父类(目标类)的原始方法。 // 注意这里用的是 MethodProxy.invokeSuper,而不是 method.invoke。 Object result = proxy.invokeSuper(obj, args); // 后置增强 long endTime = System.currentTimeMillis(); System.out.println("[CGLIB动态代理] 方法执行完毕 -> 执行 " + method.getName() + " 耗时: " + (endTime - startTime) + "ms"); return result; } }最后,使用CGLIB的Enhancer类来创建代理对象。
import net.sf.cglib.proxy.Enhancer; public class CglibProxyClient { public static void main(String[] args) { // 1. 创建Enhancer对象,相当于JDK动态代理的Proxy类 Enhancer enhancer = new Enhancer(); // 2. 设置父类(即目标类) enhancer.setSuperclass(ProductService.class); // 3. 设置回调对象(即我们的方法拦截器) enhancer.setCallback(new LogMethodInterceptor()); // 4. 创建代理对象 ProductService proxy = (ProductService) enhancer.create(); // 5. 通过代理对象调用方法 System.out.println("代理对象的类型: " + proxy.getClass().getName()); System.out.println("代理对象的父类: " + proxy.getClass().getSuperclass().getName()); proxy.saveProduct("笔记本电脑"); System.out.println("----------"); // 尝试调用final方法 proxy.finalMethod(); // 这个方法不会被拦截增强,直接执行目标类的逻辑 } }运行这段代码,你会看到saveProduct方法被成功增强,而finalMethod则直接执行,输出中不会有代理添加的日志。代理对象的类名类似ProductService$$EnhancerByCGLIB$$...,表明它是ProductService的子类。
4.2 CGLIB的核心:MethodProxy与性能优化
在MethodInterceptor.intercept方法中,我们使用MethodProxy.invokeSuper(obj, args)来调用原始方法。这里的MethodProxy是CGLIB的核心优化之一。
与JDK动态代理的Method.invoke(target, args)相比,MethodProxy.invokeSuper避免了反射调用。它通过生成两个FastClass(快速类)来建立方法索引,直接进行方法调用,其性能在大量调用时通常优于JDK动态代理的反射机制。这是CGLIB除了能代理类之外的另一大优势。
4.3 CGLIB动态代理的优缺点与坑点
优点:
- 可以代理普通类:无需目标类实现接口,应用范围更广。
- 性能通常更好:通过FastClass机制,方法调用效率高于反射。
- 功能强大:除了方法拦截,还支持回调过滤、固定值返回等高级特性。
缺点与注意事项:
- 无法代理final类或final方法:因为继承是它的基础,final类无法被继承,final方法无法被重写。
- 需要引入第三方库:增加了项目的依赖。
- 构造方法会被调用两次:由于CGLIB通过继承实现,在创建代理对象时,会先调用父类(目标类)的构造器,再调用子类(代理类)的构造器。如果目标类的构造器中有副作用代码(如计数器),需要注意这个问题。
- 目标类必须有默认构造器:因为CGLIB生成子类时需要调用父类的无参构造器。如果目标类只有带参数的构造器,创建代理时会失败。
5. 实战中的抉择:JDK代理 vs CGLIB代理 vs 静态代理
了解了三种代理的实现后,在实际项目中该如何选择呢?这里有一个清晰的决策路径和对比表格。
决策路径:
- 如果目标对象有实现接口:优先考虑JDK动态代理。它是Java标准库的一部分,无需额外依赖,且符合面向接口编程的思想。
- 如果目标对象没有实现接口:只能选择CGLIB动态代理。
- 如果增强逻辑极其简单,且代理的类和方法非常固定、数量极少:可以考虑使用静态代理,代码最直观。
- 在Spring AOP等框架中:早期版本默认对实现接口的类使用JDK动态代理,对未实现接口的类使用CGLIB。Spring Boot 2.x 之后,默认统一使用CGLIB,因为它能应对更多场景(如代理非public方法)。你可以在配置中通过
spring.aop.proxy-target-class=true/false来切换。
特性对比表格:
| 特性 | 静态代理 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|---|
| 实现方式 | 手动编写代理类 | 运行时动态生成代理类(实现接口) | 运行时动态生成代理类(继承目标类) |
| 目标要求 | 需实现相同接口或继承相同父类 | 目标必须实现至少一个接口 | 目标类不能是final,方法不能是final |
| 性能 | 最好,直接调用 | 较差,基于反射调用 | 较好,基于FastClass直接调用 |
| 灵活性 | 差,每代理一个类需写一个代理类 | 高,一个处理器可代理多个接口 | 高,一个拦截器可代理多个类 |
| 依赖 | 无 | JDK自带 | 需要引入CGLIB库 |
| 适用场景 | 简单、固定的代理需求 | 基于接口的代理,Spring AOP(旧版默认) | 代理普通类,Spring AOP(新版默认) |
一个重要的实操心得:关于“代理对象”的类型当你使用JDK动态代理时,proxy instanceof UserService为true,但proxy instanceof UserServiceImpl为false。因为代理对象实现的是接口,而不是目标类。而CGLIB代理对象proxy instanceof ProductService为true,因为它是目标类的子类。这个细微差别在某些依赖类型检查的框架或代码中可能会产生影响。
6. 动态代理的底层窥探与高级应用
理解了怎么用,我们不妨再深入一步,看看动态代理背后发生了什么,以及它的一些高级玩法。
6.1 动态生成的代理类长什么样?
我们可以通过设置系统属性,将运行时动态生成的代理类字节码保存到磁盘,然后反编译查看。这对于调试和理解原理非常有帮助。
对于JDK动态代理:在JVM启动参数或代码开头添加:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");运行程序后,会在项目根目录的com/sun/proxy下找到$Proxy0.class等文件。用反编译工具(如JD-GUI)打开,你会看到它继承了Proxy类,并实现了我们指定的接口,每个方法内部都调用了InvocationHandler.invoke。
对于CGLIB代理:在创建Enhancer时设置:
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./cglib_proxy_classes"); Enhancer enhancer = new Enhancer(); // ... 其他设置运行后会在指定目录生成代理类的.class文件。反编译后可以看到它继承了目标类,并重写了非final方法,在重写的方法中调用了MethodInterceptor.intercept。
6.2 高级应用:选择性代理与方法过滤
我们并不总是想代理目标类的所有方法。例如,对于Object类的toString(),hashCode()等方法,我们可能不想增强。这时就需要在调用处理器或拦截器中进行方法过滤。
以JDK动态代理为例,修改LogInvocationHandler:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 过滤掉Object类的基础方法,不进行增强 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(target, args); } // 也可以根据方法名过滤,例如只增强以"add"或"delete"开头的方法 if (!methodName.startsWith("add") && !methodName.startsWith("delete")) { return method.invoke(target, args); } // 以下是增强逻辑... System.out.println("[选择性代理] 开始增强方法: " + methodName); long startTime = System.currentTimeMillis(); Object result = method.invoke(target, args); long endTime = System.currentTimeMillis(); System.out.println("[选择性代理] 方法执行耗时: " + (endTime - startTime) + "ms"); return result; }在CGLIB中,可以使用CallbackFilter来实现更精细的方法回调控制,为不同的方法指定不同的MethodInterceptor,甚至直接调用父类方法(不增强)。
6.3 在主流框架中的应用
动态代理是许多Java框架的基石:
- Spring AOP (Aspect-Oriented Programming):其核心实现就是基于动态代理。
@Transactional,@Cacheable,@Async等注解的生效,背后都是Spring为你创建了代理对象,在方法调用前后插入了事务管理、缓存处理、异步执行等逻辑。 - MyBatis:Mapper接口并没有实现类,但我们可以直接调用其方法执行SQL。这正是因为MyBatis在启动时,为每个Mapper接口动态生成了代理对象,将方法调用转化为对
SqlSession的调用。 - RPC框架:如Dubbo,客户端持有的服务接口实例实际上是一个代理对象。当你调用接口方法时,代理对象会将方法名、参数等信息序列化,通过网络发送到服务端,拿到结果后再返回给你。
理解动态代理,是理解这些框架如何“无侵入”地增强我们代码的关键。下次当你使用@Transactional注解时,可以想想,Spring是如何通过一个“看不见”的代理对象,帮你管理数据库连接和事务提交回滚的。这种设计极大地提升了代码的模块化和可维护性。