背景
环境
相关环境配置:
- SpringBoot+PostGreSQL
- Spring Data JPA
问题
两个使用 Transaction 注解的 ServiceA 和 ServiceB,在 A 中引入了 B 的方法用于更新数据 ,当 A 中捕捉到 B 中有异常时,回滚动作正常执行,但是当 return 时则出现org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
异常。
代码示例:
ServiceA
@Transactional public class ServiceA { @Autowired private ServiceB serviceB; public Object methodA() { try{ serviceB.methodB(); } catch (Exception e) { e.printStackTrace(); } return null; } } 复制代码
ServiceB
@Transactional public class ServiceB { public void methodB() { throw new RuntimeException(); } } 复制代码
知识回顾
@Transactional
Spring Boot 默认集成事务,所以无须手动开启使用 @EnableTransactionManagement 注解,就可以用 @Transactional 注解进行事务管理。
@Transactional
的作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口 :不推荐在接口上使用。
@Transactional
的常用配置参数
关于事务传播机制的详细介绍,可以参考这篇文章。
@Transactional
事务注解原理
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
Spring AOP 自调用问题
若同一类中的其他没有 @Transactional
注解的方法内部调用有 @Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。
这是由于Spring AOP
代理的原因造成的,因为只有当 @Transactional
注解的方法在类以外被调用的时候,Spring 事务管理才生效。
关于 AOP 自调用的问题,文章结尾会介绍相关解决方法。
@Transactional
的使用注意事项总结
@Transactional
注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;- 避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效; - 正确的设置
@Transactional
的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败。
Spring 的 @Transactional
注解控制事务有哪些不生效的场景?
- 数据库引擎是否支持事务(MySQL的MyISAM引擎不支持事务);
- 注解所在的类是否被加载成Bean类;
- 注解所在的方法是否为 public 方法;
- 是否发生了同类自调用问题;
- 所用数据源是否加载了事务管理器;
- @Transactional 的扩展配置 propagation(事务传播机制)是否正确。
- 方法未抛出异常
- 异常类型错误(最好配置rollback参数,指定接收运行时异常和非运行时异常)
案例分析
构建项目
1、创建 Maven 项目,选择相应的依赖。一般不直接用 MySQL 驱动,而选择连接池。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <mysql.version>8.0.19</mysql.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.18</version> </dependency> </dependencies> 复制代码
2、配置 application.yml
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mysql_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false username: root password: root jpa: hibernate: ddl-auto: none open-in-view: false properties: hibernate: order_by: default_null_ordering: last order_inserts: true order_updates: true generate_statistics: false jdbc: batch_size: 5000 show-sql: true logging: level: root: info # 是否需要开启 sql 参数日志 org.springframework.orm.jpa: DEBUG org.springframework.transaction: DEBUG org.hibernate.engine.QueryParameters: debug org.hibernate.engine.query.HQLQueryPlan: debug org.hibernate.type.descriptor.sql.BasicBinder: trace 复制代码
hibernate.ddl-auto: update
实体类中的修改会同步到数据库表结构中,慎用。show_sql
可开启 hibernate 生成的 SQL,方便调试。open-in-view
指延时加载的一些属性数据,可以在页面展现的时候,保持 session 不关闭,从而保证能在页面进行延时加载。默认为 true。logging
下的几个参数用于显示 sql 的参数。
3、MySQL 数据库中创建两个表
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `age` int DEFAULT NULL, `address` varchar(100) DEFAULT NULL, `created_date` timestamp NULL, `last_modified_date` timestamp NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; CREATE TABLE `job` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `user_id` bigint(20) NOT NULL, `address` varchar(100) DEFAULT NULL, `created_date` timestamp NULL, `last_modified_date` timestamp NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 复制代码
4、创建实体类并添加 JPA 注解
目前只创建两个简单的实体类,User 和 Job
@MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Getter @EqualsAndHashCode(of = "id") @SuperBuilder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public abstract class BaseDomain implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @CreatedDate private LocalDateTime createdDate; @LastModifiedDate private LocalDateTime lastModifiedDate; } @Entity @EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true) @Setter @Getter @AllArgsConstructor @NoArgsConstructor @SuperBuilder public class User extends BaseDomain { private String name; private Integer age; private String address; @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "user_id") private List<Job> jobs = new ArrayList<>(); } @Entity @EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true) @Setter @Getter @AllArgsConstructor @NoArgsConstructor @SuperBuilder public class Job extends BaseDomain { private String name; @ManyToOne @JoinColumn private User user; private String address; } 复制代码
5、创建对应的 Repository
实现 JpaRepository 接口,生成基本的 CRUD 操作样板代码。并且可根据 Spring Data JPA 自带的 Query Lookup Strategies 创建简单的查询操作,在 IDEA 中输入 findBy
等会有提示。
@Repository public interface UserRepository extends JpaRepository<User, Long> { List<User> findByAddress(String address); User findByName(String name); void deleteByName(String name); } @Repository public interface JobRepository extends JpaRepository<Job, Long> { List<Job> findByUserId(Long userId); } 复制代码
6、创建 UserService 及其实现类
public interface UserService { List<UserResponse> getAll(); List<UserResponse> findByAddress(String address); UserResponse query(String name); UserResponse add(UserDTO userDTO); UserResponse update(UserDTO userDTO); void delete(String name); } @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Override public List<UserResponse> getAll() { List<User> users = userRepository.findAll(); return users.stream().map(this::toUserResponse).collect(Collectors.toList()); } @Override public List<UserResponse> findByAddress(String address) { List<User> users = userRepository.findByAddress(address); return users.stream().map(this::toUserResponse).collect(Collectors.toList()); } @Override public UserResponse query(String name) { if (!Objects.equals("hresh", name)) { throw new RuntimeException(); } User user = userRepository.findByName(name); return toUserResponse(user); } @Override public UserResponse add(UserDTO userDTO) { User user = User.builder().name(userDTO.getName()) .age(userDTO.getAge()).address(userDTO.getAddress()).build(); userRepository.save(user); return toUserResponse(user); } @Override public UserResponse update(UserDTO userDTO) { User user = userRepository.findByName(userDTO.getName()); if (Objects.isNull(user)) { throw new RuntimeException(); } user.setAge(userDTO.getAge()); user.setAddress(userDTO.getAddress()); userRepository.save(user); return toUserResponse(user); } @Override public void delete(String name) { userRepository.deleteByName(name); } private UserResponse toUserResponse(User user) { if (user == null) { return null; } List<Job> jobs = user.getJobs(); List<JobItem> jobItems; if (CollectionUtils.isEmpty(jobs)) { jobItems = new ArrayList<>(); } else { jobItems = jobs.stream().map(job -> { JobItem jobItem = new JobItem(); jobItem.setName(job.getName()); jobItem.setAddress(job.getAddress()); return jobItem; }).collect(Collectors.toList()); } return UserResponse.builder().name(user.getName()).age(user.getAge()).address(user.getAddress()) .jobItems(jobItems) .build(); } } 复制代码
7、UserController
@RestController @RequestMapping("/users") @RequiredArgsConstructor public class UserController { private final UserService userService; private final JobService jobService; @GetMapping public List<UserResponse> queryAll() { return userService.getAll(); } @GetMapping("/address") public List<UserResponse> findByAddress(@RequestParam("address") String address) { return userService.findByAddress(address); } @GetMapping("/{name}") public UserResponse getByName(@PathVariable("name") String name) { return userService.query(name); } @PostMapping public UserResponse add(@RequestBody @Validated(Add.class) UserDTO userDTO) { return userService.add(userDTO); } @PutMapping public UserResponse update(@RequestBody @Validated(Update.class) UserDTO userDTO) { return userService.update(userDTO); } @DeleteMapping public void delete(@RequestParam(value = "name") @NotBlank String name) { userService.delete(name); } @PostMapping("/jobs") public void addJob(@RequestBody @Validated(Update.class) JobDTO jobDTO) { jobService.add(jobDTO); } } 复制代码
最后来看一下整个项目的结构以及文件分布。
基于上述代码,我们将进行特定知识的学习演示。