一、问题的重现:事务失效的诡异现象
在前段时间的项目学习中,我遇见了一个问题,就是当我在一个方法中调用一个有事务注解的方法时,idea提示事务会失效。如下图,我对一个单个创建的方法加了回滚的事务,而后续批量插入调用了这个方法,此时出现了这个提示。
@Transactional 自调用(实际上是目标对象内)的方法调用目标对象的另一个方法/在运行时不会导致实际的事务

起初我并没有深究,直到近期的面试中,面试官问了我一个问题,当两个serviceA和serviceB方法位于同一个Service类中,serviceB定义了切面,然后serviceA调用serviceB,切面会生效吗?我当时回答不会,因为无法创建代理对象。
是的,@Transaction 注解就是通过AOP实现的,事后我恍然大悟,原来是AOP搞的鬼!
二、AOP:Spring框架的隐形功臣
那什么是AOP呢?AOP作为Spring的核心思想,与IoC和DI共同实现了Spring的高内聚、低耦合、轻量的特性,是Spring整个生态不可或缺的基石。AOP(Aspect-Oriented Programming)是一种编程范式,旨在解决横切关注点(cross-cutting concerns)的模块化问题。它允许开发者将影响多个类的公共行为(如日志、事务、安全等)从业务逻辑中分离出来。具有以下的核心思想:
- 关注点分离:将系统分为核心关注点(业务逻辑)和横切关注点(通用功能)
- 增强而非修改:通过动态织入方式为现有代码添加功能,而不是直接修改源代码
对于一个切面,我们会通过@Before、@After、@Around等注解定义切面织入的位置。
三、深入理解:AOP的实现原理------代理模式
要真正理解为什么事务注解会失效,我们需要深入AOP的实现机制。Spring AOP的核心就是代理模式。Spring不会直接调用我们的业务对象,而是创建一个代理对象,通过这个代理对象来间接调用。
代理对象的工作机制
typescript
// 原始业务对象
public class UserServiceImpl implements UserService {
@Transactional
public void createUser() {
// 业务逻辑
}
public void batchCreateUsers() {
// 这里直接调用createUser() - 问题所在!
for (int i = 0; i < 10; i++) {
createUser(); // ❌ 事务不会生效,因为不是代理对象调用,没有实现切面的织入!
}
}
}
Spring AOP的两种动态代理方式
Spring AOP主要使用两种动态代理技术:JDK动态代理和CGLIB动态代理。
1. JDK动态代理
原理:基于接口实现,使用Java反射机制
typescript
// 示例:JDK动态代理的实现原理
public class JdkDynamicProxyDemo {
public static void main(String[] args) {
// 目标对象
UserService target = new UserServiceImpl();
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@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;
}
}
);
// 通过代理对象调用方法
proxy.createUser(); // ✅ 事务生效,代理对象织入了切面的逻辑
}
}
JDK动态代理的特点:
- 只能为接口创建代理
- 要求目标类至少实现一个接口
- 使用
java.lang.reflect.Proxy类 - 性能相对较好(Java 8+优化后)
2. CGLIB动态代理
原理:基于类继承,使用字节码生成技术
typescript
// 示例:CGLIB动态代理的实现原理
public class CglibProxyDemo {
public static void main(String[] args) {
// 创建Enhancer对象
Enhancer enhancer = new Enhancer();
// 设置父类(目标类)
enhancer.setSuperclass(UserServiceImpl.class);
// 设置回调
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy)
throws Throwable {
// 前置增强:开启事务
System.out.println("CGLIB代理:开启事务...");
// 调用父类方法
Object result = proxy.invokeSuper(obj, args);
// 后置增强:提交事务
System.out.println("CGLIB代理:提交事务...");
return result;
}
});
// 创建代理对象
UserService proxy = (UserService) enhancer.create();
// 通过代理对象调用方法
proxy.createUser(); // ✅ 事务生效,代理对象织入了切面的逻辑
}
}
CGLIB动态代理的特点:
- 可以为类创建代理(不需要接口)
- 通过继承目标类实现代理
- 使用ASM字节码操作框架
- 不能代理final类和方法(无法继承或重写)
- 性能略低于JDK动态代理(但差距不大)
四、揭开谜底:为什么事务注解会失效?
现在我们可以回答开头的问题了。事务失效的根本原因在于代理对象的调用机制。
问题重现分析
typescript
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 数据库插入操作
userDao.insert(user);
}
public void batchCreateUsers(List<User> users) {
for (User user : users) {
// 问题:这里调用的是this.createUser(),而不是代理对象的createUser()
this.createUser(user);
// 正确做法:应该调用代理对象的createUser()方法
}
}
}
图解调用流程
scss
错误调用流程:
batchCreateUsers() → this.createUser() → 原始对象的createUser()
↓
❌ 切面没有织入,没有事务控制!
正确调用流程:
batchCreateUsers() → 代理对象.createUser() → 原始对象的createUser()
↓
✅ 切面织入,有事务控制(前置:开启事务,后置:提交事务)
Spring的选择:默认使用哪种代理?
Spring AOP根据以下规则选择代理方式:
- 如果目标对象实现了接口 → 默认使用JDK动态代理
- 如果目标对象没有实现接口 → 使用CGLIB动态代理
- 可以通过配置强制使用CGLIB:
spring.aop.proxy-target-class=true
五、解决方案:如何避免事务失效
方案1:自我注入(推荐)
typescript
@Service
public class UserService {
//SpringBoot 2.x + 默认开启循环依赖支持,自我注入不会有问题;
//若关闭循环依赖(spring.main.allow-circular-references=false),则此方案失效
@Autowired
private UserService self; // 注入自己
@Transactional
public void createUser(User user) {
userDao.insert(user);
}
public void batchCreateUsers(List<User> users) {
for (User user : users) {
self.createUser(user); // 通过代理对象调用
}
}
}
方案2:从ApplicationContext获取代理对象
typescript
@Service
public class UserService implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Transactional
public void createUser(User user) {
userDao.insert(user);
}
public void batchCreateUsers(List<User> users) {
UserService proxy = applicationContext.getBean(UserService.class);
for (User user : users) {
proxy.createUser(user);
}
}
}
方案3:使用AopContext获取当前代理对象
typescript
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userDao.insert(user);
}
public void batchCreateUsers(List<User> users) {
// 启用暴露代理(以下三种方法)
// 在配置中设置:expose-proxy=true
// 注解方式:@EnableAspectJAutoProxy(exposeProxy = true)
// 配置文件方式(SpringBoot):spring.aop.expose-proxy=true
UserService proxy = (UserService) AopContext.currentProxy();
for (User user : users) {
proxy.createUser(user);
}
}
}
方案4:重构代码结构
typescript
// 将事务方法拆分到独立的Service中
@Service
public class UserTransactionService {
@Transactional
public void createUser(User user) {
userDao.insert(user);
}
}
@Service
public class UserService {
@Autowired
private UserTransactionService userTransactionService;
public void batchCreateUsers(List<User> users) {
for (User user : users) {
userTransactionService.createUser(user);
}
}
}
六、面试回答指南
针对面试官的问题:"当两个serviceA和serviceB方法位于同一个Service类中,serviceB定义了切面,然后serviceA调用serviceB,切面会生效吗?"
标准回答:
"不会生效。这是因为Spring AOP是基于代理实现的。当serviceA调用serviceB时,实际上是通过this.serviceB()这种方式直接调用,绕过了代理对象。代理对象只拦截外部调用,对于同一个类内部的相互调用,Spring无法通过代理进行增强。要解决这个问题,可以通过自我注入、从ApplicationContext获取代理对象、或者重构代码结构等方式来确保通过代理对象进行调用。"
七、总结
通过本文的分析,我们可以得出以下结论:
- 事务失效的本质:AOP代理机制导致的内部调用绕过代理对象
- Spring AOP的实现:基于JDK动态代理(接口)和CGLIB动态代理(类继承)
- 代理对象的作用:在目标方法执行前后插入增强逻辑(如事务控制)
- 解决方案:确保通过代理对象调用有切面的方法
理解AOP的代理机制不仅有助于解决事务失效的问题,还能帮助我们更好地设计系统架构,合理使用AOP的各种特性。记住:在Spring的世界里,有些时候"绕过自己"(通过代理调用)才能更好地完成工作!