1. 为什么需要理解原始对象与代理对象
在Spring框架中,AOP(面向切面编程)是一个极其重要的特性。很多开发者在使用@Transactional、@Cacheable等注解时,经常会遇到一些"诡异"的现象:明明调用了方法,事务却没有生效;明明设置了缓存,却总是查询数据库。这些问题的根源往往在于没有理解原始对象和代理对象的区别。
我曾在项目中遇到过这样一个案例:在一个用户服务类中,我们使用@Transactional标注了saveUser方法,然后在同一个类的另一个方法中直接调用了saveUser。测试时发现,当saveUser抛出异常时,数据竟然没有被回滚!经过排查才发现,这是因为我们直接在类内部调用了方法,绕过了Spring的代理机制。
重要提示:Spring AOP默认使用动态代理机制,当你在同一个类中调用被AOP增强的方法时,实际上调用的是原始对象的方法,而不是代理对象的方法,这会导致AOP增强失效。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Spring AOP的两种代理方式
2.1 JDK动态代理
JDK动态代理是Spring AOP默认采用的代理方式,它基于接口实现。当你的目标类实现了至少一个接口时,Spring会使用java.lang.reflect.Proxy来创建代理对象。
工作原理:
- 在运行时动态生成一个实现了目标接口的代理类
- 代理类会持有InvocationHandler的实例
- 所有方法调用都会被转发到InvocationHandler的invoke方法
java复制public class JdkDynamicProxyDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxy1, method, args1) -> {
System.out.println("Before method: " + method.getName());
Object result = method.invoke(target, args1);
System.out.println("After method: " + method.getName());
return result;
});
proxy.saveUser(new User());
}
}
JDK动态代理的特点:
- 只能代理接口
- 性能较好,因为JDK自身支持
- 生成的代理类体积小
2.2 CGLIB代理
当目标类没有实现任何接口时,Spring会使用CGLIB来创建代理。CGLIB通过继承目标类并重写方法来实现代理。
工作原理:
- 在运行时动态生成目标类的子类
- 子类重写了父类的方法
- 在重写的方法中加入切面逻辑
java复制public class CglibProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> {
System.out.println("Before method: " + method.getName());
Object result = proxy.invokeSuper(obj, args1);
System.out.println("After method: " + method.getName());
return result;
});
UserService proxy = (UserService) enhancer.create();
proxy.saveUser(new User());
}
}
CGLIB代理的特点:
- 可以代理普通类
- 需要引入额外的CGLIB库
- 生成的代理类体积较大
- 不能代理final类和方法
3. 原始对象与代理对象的本质区别
3.1 对象创建时机
原始对象:
- 由开发者显式new出来
- 或者由Spring IoC容器通过反射创建
- 就是普通的Java对象实例
代理对象:
- 在应用上下文初始化阶段由Spring创建
- 包装了原始对象
- 包含了额外的切面逻辑
3.2 方法调用流程
原始对象的调用:
code复制调用者 -> 原始对象.method()
代理对象的调用:
code复制调用者 -> 代理对象.method()
-> 执行前置通知
-> 原始对象.method()
-> 执行后置通知
-> 返回结果
3.3 实际应用中的识别方法
如何判断一个对象是原始对象还是代理对象?
- 打印对象的class名称:
java复制System.out.println(userService.getClass().getName());
如果是代理对象,名称会包含"$Proxy"或"$$EnhancerBySpringCGLIB$$"
- 使用AopUtils工具类:
java复制AopUtils.isAopProxy(userService) // 判断是否是代理
AopUtils.isCglibProxy(userService) // 判断是否是CGLIB代理
AopUtils.isJdkDynamicProxy(userService) // 判断是否是JDK代理
4. 常见问题与解决方案
4.1 自调用问题
问题描述:
在同一个类中,方法A调用被AOP增强的方法B,AOP增强失效。
原因:
这实际上是原始对象在调用自己的方法,绕过了代理对象。
解决方案:
- 将方法B提取到另一个类中
- 通过ApplicationContext获取代理对象来调用
java复制((UserService)AopContext.currentProxy()).methodB();
- 使用AspectJ的编译时织入
4.2 代理方式选择问题
问题描述:
如何控制Spring使用哪种代理方式?
解决方案:
- 通过配置强制使用CGLIB:
java复制@EnableAspectJAutoProxy(proxyTargetClass = true)
- 如果目标类实现了接口,默认使用JDK动态代理
- 如果没有实现接口,自动使用CGLIB
4.3 性能考虑
JDK动态代理 vs CGLIB性能对比:
- 创建速度:JDK动态代理更快
- 调用速度:JDK动态代理略快
- 内存占用:CGLIB生成的类更大
实际建议:
- 大多数情况下性能差异可以忽略
- 优先使用JDK动态代理(基于接口编程)
- 需要代理具体类时才使用CGLIB
5. 高级应用场景
5.1 多级代理
Spring支持对同一个bean进行多次代理,形成代理链。例如:
- 事务代理
- 缓存代理
- 监控代理
调用顺序:
code复制调用者 -> 监控代理 -> 缓存代理 -> 事务代理 -> 原始对象
5.2 代理对象的转换
有时候我们需要从代理对象获取原始对象,可以通过以下方式:
java复制if(AopUtils.isAopProxy(bean)) {
Object target = ((Advised)bean).getTargetSource().getTarget();
}
5.3 自定义代理逻辑
除了使用Spring提供的AOP注解,我们还可以实现自己的MethodInterceptor:
java复制public class CustomInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 前置处理
Object result = invocation.proceed();
// 后置处理
return result;
}
}
然后在配置中注册:
java复制@Bean
public DefaultAdvisorAutoProxyCreator autoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setInterceptorNames("customInterceptor");
return creator;
}
6. 实际项目中的经验总结
- 在大型项目中,明确标注哪些类使用了AOP增强,可以在类上添加注释:
java复制/**
* 注意:本类方法使用了AOP增强,避免直接调用原始方法
*/
@Service
public class UserService {
// ...
}
- 对于关键业务方法,建议进行代理验证测试:
java复制@Test
public void testSaveUserIsProxied() {
assertTrue(AopUtils.isAopProxy(userService));
}
-
性能敏感场景下,可以考虑使用AspectJ的编译时织入代替动态代理,减少运行时开销。
-
调试代理对象时,可以使用Spring的Advised接口来检查代理的配置:
java复制if(userService instanceof Advised) {
Advised advised = (Advised)userService;
// 查看所有的advisor
for(Advisor advisor : advised.getAdvisors()) {
System.out.println(advisor.getAdvice().getClass().getName());
}
}
- 在微服务架构中,AOP经常被用于实现:
- 分布式事务管理
- 服务调用监控
- API权限控制
- 请求日志记录
理解原始对象和代理对象的区别,是掌握Spring AOP的关键。这不仅帮助我们避免常见的陷阱,还能让我们更好地设计和实现面向切面的功能。在实际开发中,我建议在团队内部进行相关知识分享,特别是对新加入的成员,这可以显著减少因不理解代理机制而导致的问题。
