在业务方法上使用@Transactional
开启声明式事务时,很有可能由于使用方式有误,导致事务没有生效。
环境准备
表结构
sql
CREATE TABLE `admin` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`phone` int(11) DEFAULT NULL,
`power` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
实体类
java
@Entity
@Data
public class Admin {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String name;
private Integer phone;
private Integer power;
}
DAO 层
java
@Repository
public interface AdminRepository extends JpaRepository<Admin,Long> {
List<Admin> findByName(String name);
}
上面这些类都是不变的,主要是 service
类。
事务失效
非 public
当被@Transactional
注解修饰的方法为非public
时,事务将失效。
java
@Service
@Slf4j
public class AdminService {
@Autowired
private AdminRepository adminRepository;
@Transactional
protected void saveAdmin(Admin admin) {
adminRepository.save(admin);
if (admin.getName().contains("@")) {
throw new RuntimeException("不合法");
}
}
}
在同包下新建一个测试类。
java
@Autowired
private AdminService adminService;
@GetMapping("/addAdminWrong")
public void add(@RequestParam("name") String name) {
Admin admin = new Admin();
admin.setName(name);
adminService.saveAdmin(admin);
}
测试接口发现,即使用户名不合法,用户也能创建成功。
@Transactional 生效原则(一):只有定义在 public 方法上的 @Transactional 才能生效。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。
所以,将 saveAdmin
方法,修改为 public,就可以了。
自调用
当saveAdmin
方法是 public时,事务一定能生效吗?
答案是不一定,比如下面这个例子。
java
@Service
@Slf4j
public class AdminService {
@Autowired
private AdminRepository adminRepository;
public int addAdminWrong(String name) {
Admin admin = new Admin();
admin.setName(name);
try {
/**
* 一些其他业务处理
*/
this.saveAdmin(admin);
} catch (Exception e) {
log.error("添加失败:{}",e);
}
return adminRepository.findByName(name).size();
}
@Transactional
public void saveAdmin(Admin admin) {
adminRepository.save(admin);
if (admin.getName().contains("@")) {
throw new RuntimeException("不合法");
}
}
}
在上面代码中,我们新定义了一个addAdminWrong
方法,并在它内部调用了本类的saveAdmin
方法。
测试代码如下:
java
@GetMapping("/addAdminWrong")
public void add(@RequestParam("name") String name) {
adminService.addAdminWrong(name);
}
测试后发现,不合法的用户,还是被创建成功了。
java
Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
观察日志发现,自调用因为没有走代理,事务没有在 saveAdmin
方法上生效,只在 SimpleJpaRepository
上的 save 方法层面生效。
最简单的修改方案是,在AdminService
类中,自己注入自己,比如:
java
@Autowired
private AdminService self;
然后通过 self 实例去调用 self.saveAdmin(admin)
还有一种优雅的方案,是通过AopContext
在代理对象中获取自身。
比如:
java
AdminService adminService = (AdminService) AopContext.currentProxy();
adminService.saveAdmin(admin);
然后就会发现一个异常:
cpp
Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
它的意思是:没有开启一个 exposeProxy
的属性,导致无法暴露出代理对象,从而无法获取。
所以我们在启动类上加上这个注解 @EnableAspectJAutoProxy(exposeProxy=true)
即可。
java
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy=true) //暴露代理对象
public class StarterDemoApplication {
public static void main(String[] args) {
SpringApplication.run(StarterDemoApplication.class, args);
}
}
然后,再观察日志,发现事务在AdminService.saveAdmin
方法上生效了
cpp
Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdmin]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
@Transactional 生效原则(二):需要确保方法调用是通过Spring的代理对象进行的,而不是直接在类内部调用。
异常处理不当
上面的两个例子,是由于事务失效导致回滚失败。
接下来,我们来看下,即使事务生效也会回滚失败的场景。
(一):被@Transactional
注解标记的方法抛出了异常,事务才会回滚。
意思就是说,得把异常抛出来才行。
在 Spring 的 TransactionAspectSupport.invokeWithinTransaction
方法中,可以找到处理事务的逻辑,可以看到只有捕获到异常才能进行后续事务处理。
比如这段代码,虽然在方法中抛出了异常,但又被它自己给捕获了。
java
@Transactional
public void saveAdminWrong1(String name) {
Admin admin = new Admin();
admin.setName(name);
try {
adminRepository.save(admin);
throw new RuntimeException("模拟错误");
} catch (Exception e) {
log.error("save admin error:",e);
}
}
同时再次观察日志可以发现,虽然事务在AdminService.saveAdminWrong1
上是生效的,但由于异常没有被传播出去,所以无法回滚。
cpp
Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdminWrong1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
我们可以手动回滚当前事务,在 catch 代码块中加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
通过日志可以看到回滚的信息。
cpp
2024-11-22 17:13:08.537 DEBUG 7219 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Transactional code has requested rollback
2024-11-22 17:13:08.537 DEBUG 7219 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback
2024-11-22 17:13:08.537 DEBUG 7219 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Rolling back JPA transaction on EntityManager [SessionImpl(1495642341<open>)]
(二):默认情况下,出现 RuntimeException 或 Error 的时候,Spring 才会回滚事务。
追踪completeTransactionAfterThrowing
方法,可以看到,它是根据异常的类型来决定是否回滚的。
点进 rollbackOn
方法,可以看到,它只会在RuntimeException
或 Error
的时候返回 true。
比如这段代码,我们希望保存用户的时候,同时去加载一个文件,如果加载文件失败,则事务需要回滚。
java
@Transactional
public void saveAdminWrong2(String name) throws IOException {
Admin admin = new Admin();
admin.setName(name);
adminRepository.save(admin);
otherTask(); //额外的操作
}
private void otherTask() throws IOException{
Files.readAllLines(Paths.get("admin.txt"));
}
同时观察日志可以发现,虽然事务在AdminService.saveAdminWrong2
上是生效的,也没有去捕获异常,但是由于传播出去的是 checked exception
,所以事务也不会回滚。
cpp
Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdminWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
可以在注解中声明,希望遇到所有的 Exception
都回滚事务。
cpp
@Transactional(rollbackFor = Exception.class)
public void saveAdminWrong2(String name) throws IOException {
Admin admin = new Admin();
admin.setName(name);
adminRepository.save(admin);
otherTask(); //额外的操作
}
private void otherTask() throws IOException{
Files.readAllLines(Paths.get("test.sql"));
}
同时观察日志可以发现,事务在AdminService.saveAdminWrong2
上是生效的,还看到了回滚的日志信息。
java
2024-11-22 17:29:24.846 DEBUG 7560 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdminWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
2024-11-22 17:29:24.948 DEBUG 7560 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback
2024-11-22 17:29:24.948 DEBUG 7560 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Rolling back JPA transaction on EntityManager [SessionImpl(1844074856<open>)]
事务传播行为不当
比如,在插入用户信息的时候,也插入一份扩展信息,但由于扩展信息不是很重要,即使它失败了,也不要影响到我们的主逻辑(把插入扩展信息的操作当成一个独立的事务)。
java
@Service
@Slf4j
public class AdminService {
@Autowired
private AdminRepository adminRepository;
@Autowired
private AddressService addressService;
@Transactional
public void saveAdminWrong3(String name) {
//1.保存用户信息
saveAdmin(name);
//2.保存扩展信息
addressService.saveAddress(name);
}
private void saveAdmin(String name) {
Admin admin = new Admin();
admin.setName(name);
adminRepository.save(admin);
log.info("save admin success");
}
}
java
@Service
@Slf4j
public class AddressService {
@Autowired
private AddressRepository addressRepository;
@Transactional
public void saveAddress(String name) {
Address address = new Address();
address.setName(name);
log.info("saveAddress start");
addressRepository.save(address);
throw new RuntimeException("模拟 save address 失败");
}
}
可以看到,saveAddress
的操作是失败的,按照我们的期望,saveAdmin
方法不能受到影响,能够正常插入成功。
测试执行后发现,saveAdmin
方法出现了回滚,不符合我们的预期。
cpp
2024-11-22 18:02:18.171 DEBUG 8251 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Rolling back JPA transaction on EntityManager [SessionImpl(809976208<open>)]
2024-11-22 18:02:18.188 DEBUG 8251 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
2024-11-22 18:02:18.188 ERROR 8251 --- [nio-8080-exec-1] c.s.demo.controller.TestController : 模拟 save address 失败
2024-11-22 18:02:18.205 DEBUG 8251 --- [nio-8080-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
我们来猜想一下,是不是因为saveAddress
抛出的异常,没有在saveAdminWrong3
中捕获,而saveAdminWrong3
也会接着往上层抛,导致被回滚了呢?
所以呢,我们先在saveAdminWrong3
方法中捕获一下saveAddress
抛出的异常试试。
cpp
@Transactional
public void saveAdminWrong3(String name) {
//1.保存用户信息
saveAdmin(name);
//2.保存扩展信息
try {
addressService.saveAddress(name);
} catch (Exception e) {
log.error("save address error:{}",e.getMessage());
}
}
运行程序,再次观察日志:
cpp
2024-11-22 18:39:41.741 DEBUG 8496 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.starter.demo.service.AdminService.saveAdminWrong3]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-11-22 18:39:41.850 DEBUG 8496 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Participating transaction failed - marking existing transaction as rollback-only
2024-11-22 18:39:41.851 ERROR 8496 --- [nio-8080-exec-1] com.starter.demo.service.AdminService : save address error:模拟 save address 失败
2024-11-22 18:39:41.851 DEBUG 8496 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(98155953<open>)]
2024-11-22 18:39:41.884 ERROR 8496 --- [nio-8080-exec-1] c.s.demo.controller.TestController : org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
通过日志,可以发现:
- 在
AdminService.saveAdminWrong3
上开启了事务处理; - 当前事务被标记为了回滚;
- 在
saveAdminWrong3
中打印出了saveAddress
的异常信息; - 主方法已经提交了事务;
- 在
TestController
中打印了一个UnexpectedRollbackException
,提示这个事务要静默回滚了。
UnexpectedRollbackException
是 Spring 框架抛出的一个异常,表明事务由于某些原因被静默地标记为只能回滚(rollback-only),意味着事务不会正常提交,而是会在结束时被回滚。
在saveAdminWrong3
方法中并没有出现异常,所以在事务提交时,发现当前事务已经被子方法设置成了回滚,导致无法正常提交,进而证实了saveAdminWrong3
和saveAddress
使用了同一个事务。
在@Transactional
注解中,propagation
属性决定了事务的传播行为,默认是REQUIRED
。
REQUIRED
:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
所以,这也说明了saveAddress
方法不会开启一个新事务,而是会加入到saveAdminWrong3
的事务中。
所以,需要将propagation
设置为 REQUIRES_NEW
。
REQUIRES_NEW
:它会创建一个新事务,如果当前存在事务,把当前事务挂起,直到新事务完成。这种传播行为适用于需要独立于当前事务的场景。
修改AddressService
的代码,其他不变。
java
@Service
@Slf4j
public class AddressService {
@Autowired
private AddressRepository addressRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(String name) {
Address address = new Address();
address.setName(name);
log.info("saveAddress start");
addressRepository.save(address);
throw new RuntimeException("模拟 save address 失败");
}
}
再次执行,查看日志:
cpp
2024-11-22 19:13:56.643 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.starter.demo.service.AdminService.saveAdminWrong3]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-11-22 19:13:56.734 INFO 9201 --- [nio-8080-exec-1] com.starter.demo.service.AdminService : save admin success
2024-11-22 19:13:56.734 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Suspending current transaction, creating new transaction with name [com.starter.demo.service.AddressService.saveAddress]
2024-11-22 19:13:56.781 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback
2024-11-22 19:13:56.833 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Resuming suspended transaction after completion of inner transaction
2024-11-22 19:13:56.833 ERROR 9201 --- [nio-8080-exec-1] com.starter.demo.service.AdminService : save address error:模拟 save address 失败
2024-11-22 19:13:56.834 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1073120187<open>)]
通过日志可以看到
- 在
AdminService.saveAdminWrong3
上开启了事务处理; - admin 创建完成;
- 主事务挂起了,在
AddressService.saveAddress
上开启了一个新的子事务; - 子事务回滚了;
- 子事务完成,继续被挂起的主事务;
- 捕获到了
saveAddress
的异常; - 主事务提交了,没有看到静默回滚的异常。