골든스톤 프로젝트 1차 챌린지 신청했습니다 - 100,000 상금 풀 공유, 이것은 2번째 글 입니다. 이벤트 세부정보를 보려면 클릭하세요.
이 기사는 내 개인 웹사이트 에 처음 게시되었습니다.
배경
주위
관련 환경 구성:
-
SpringBoot+PostGreSQL
-
스프링 데이터 JPA
의문
Transaction으로 annotated가 붙은 두 ServiceA와 ServiceB는 A에 B의 메소드를 도입하여 데이터를 갱신하는데, A가 B에서 예외를 잡으면 정상적으로 롤백이 실행되지만 리턴 시 예외가 발생한다 org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
.
코드 예:
서비스A
@Transactional
public class ServiceA {
@Autowired
private ServiceB serviceB;
public Object methodA() {
try{
serviceB.methodB();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
复制代码
서비스B
@Transactional
public class ServiceB {
public void methodB() {
throw new RuntimeException();
}
}
复制代码
지식 복습
@트랜잭션
Spring Boot는 기본적으로 트랜잭션을 통합하므로 @EnableTransactionManagement 주석을 수동으로 활성화하지 않고도 트랜잭션 관리를 위해 @Transactional 주석을 사용할 수 있습니다.
@Transactional
행동 범위
- 메소드 : 메소드에 어노테이션을 사용하는 것을 권장하지만, 이 어노테이션은 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);
}
}
复制代码
最后来看一下整个项目的结构以及文件分布。
基于上述代码,我们将进行特定知识的学习演示。
事务回滚
构建必要的代码如下:
//UserController.java
@GetMapping("/users")
public List<User> queryAll() {
return userApplication.findAll();
}
//UserApplication.java
@Service
@Transactional
public class UserApplication {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
public List<User> findAll() {
try {
userService.query("hresh2");
} catch (Exception e) {
}
return userRepository.findAll();
}
}
//UserServiceImpl.java
@Override
@Transactional
public UserResponse query(String name) {
if (!name.equals("hresh")) {
throw new IllegalArgumentException("name is forbidden");
}
return null;
}
public void validateName(String name) {
if (!name.equals("hresh")) {
throw new IllegalArgumentException("name is forbidden");
}
}
复制代码
我们利用 postman 来进行测试,发现报错结果和预期不大一样:
关键信息变为了 Transaction silently rolled back because it has been marked as rollback-only
,这里我们暂不讨论错误提示信息为何发生了改变,先集中讨论报错原因。
根据基础知识中介绍的@Transactional 的作用范围和传播机制可知,当我们在 Service 文件类上添加 @Transactional 时,该注解对该类中所有的 public 方法都生效,且传播机制默认为 PROPAGATION_REQUIRED,即如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
在这种情况下,外层事务(UserApplication)和内层事务(UserServiceImpl)就是一个事务,任何一个出现异常,都会在 findAll()执行完毕后回滚。如果内层事务抛出异常 IllegalArgumentException(没有catch,继续向外层抛出),在内层事务结束时,Spring 会把内层事务标记为“rollback-only”;这时外层事务发现了异常 IllegalArgumentException,如果外层事务 catch了异常并处理掉,那么外层事务A的方法会继续执行代码,直到外层事务也结束时,这时外层事务想 commit,因为正常结束没有向外抛异常,但是内外层事务是同一个事务,事务已经被内层方法标记为“rollback-only”,需要回滚,无法 commit,这时 Spring 就会抛出org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
,意思是“事务静默回滚,因为它已被标记为仅回滚”。
报错原因分析到此为止,现在我们来分析一下为何自建简易代码复现时,错误提示发生了变化,那么就直接深入代码来分析一下。
根据日志打印的结果来看,rollback-only 异常发生于 org.springframework.transaction.support.AbstractPlatformTransactionManager 文件中:
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
} else {
DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
//isLocalRollbackOnly()获取的是AbstractTransactionStatus类中的rollbackOnly属性,默认为false
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
this.logger.debug("Transactional code has requested rollback");
}
this.processRollback(defStatus, false);
} else if (!this.shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
//shouldCommitOnGlobalRollbackOnly默认实现是false。这里是指如果发现事务被标记全局回滚并且在全局回滚标记情况下不应该提 // 交事务的话,那么则进行回滚。
// defStatus.isGlobalRollbackOnly()进行判断是指读取DefaultTransactionStatus中EntityTransaction对象的 // rollbackOnly标志位,即判断TransactionStatus是否等于MARKED_ROLLBACK
if (defStatus.isDebug()) {
this.logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
this.processRollback(defStatus, true);
} else {
this.processCommit(defStatus);
}
}
}
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;
try {
boolean unexpectedRollback = false;
this.prepareForCommit(status);
this.triggerBeforeCommit(status);
this.triggerBeforeCompletion(status);
beforeCompletionInvoked = true;
if (status.hasSavepoint()) {
if (status.isDebug()) {
this.logger.debug("Releasing transaction savepoint");
}
unexpectedRollback = status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
} else if (status.isNewTransaction()) {
if (status.isDebug()) {
this.logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
this.doCommit(status);
} else if (this.isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}
if (unexpectedRollback) {
throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only");
}
}
//.........
}
public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
} else {
DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
this.processRollback(defStatus, false);
}
}
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
try {
this.triggerBeforeCompletion(status);
if (status.hasSavepoint()) {
if (status.isDebug()) {
this.logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
} else if (status.isNewTransaction()) {
// 判断当前事务是否是个新事务,false表示参与现有事务或不在当前事务中
if (status.isDebug()) {
this.logger.debug("Initiating transaction rollback");
}
this.doRollback(status);
} else {
if (status.hasTransaction()) {
// 参与现有事务
if (!status.isLocalRollbackOnly() && !this.isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
this.logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
} else {
if (status.isDebug()) {
this.logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
this.doSetRollbackOnly(status);
}
} else {
this.logger.debug("Should roll back transaction but cannot - no transaction available");
}
if (!this.isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
} catch (Error | RuntimeException var8) {
this.triggerAfterCompletion(status, 2);
throw var8;
}
this.triggerAfterCompletion(status, 1);
if (unexpectedRollback) {
throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only");
}
} finally {
this.cleanupAfterCompletion(status);
}
}
复制代码
结合上述代码,通过断点调试,大致可以梳理出如下逻辑:
1、当内层事务(UserServiceImpl)中的 query 抛出异常后,开始进行回滚,即进入 rollback()方法,接着进入 processRollback()方法,此时第二个入参的值为 false;
2、进入 processRollback()方法后,首先判断事物是否拥有 savepoint(回滚点),如果有,就回滚到设置的 savepoint;接着判断当前事务是否是新事务,因为这里是内外层事务,其实是同一个事务,所以判断结果为 false;但 hasTransaction()判断为 true,接着进入 if 方法体,isLocalRollbackOnly()为 false,isGlobalRollbackOnParticipationFailure()为 true(globalRollbackOnParticipationFailure默认情况下为true,表示异常全局回滚),那么只能执行 doSetRollbackOnly()方法,此处只是补充打印一下日志;紧接着调用 isFailEarlyOnGlobalRollbackOnly()方法,这里主要是获取 failEarlyOnGlobalRollbackOnly 字段的值,默认情况下 failEarlyOnGlobalRollbackOnly 开关是关闭的,这个开关的作用是如果开启了程序则会尽早抛出异常。最终 unexpectedRollback 字段仍为 false,所以没有抛出 Transaction rolled back because it has been marked as rollback-only
异常。
3、内层事务方法调用结束后,回到外层方法,在事务提交时,即执行 commit()方法,实际上执行的是 processCommit()方法。该方法中的逻辑和 processRollback()方法有些重叠,此时判断当前事务是新事务,所以 unexpectedRollback 就被赋值为 true,最终抛出 Transaction silently rolled back because it has been marked as rollback-only
异常。
上面我们简述了自定义代码时,为何只能得到 Transaction silently rolled back because it has been marked as rollback-only
异常,但一开始在项目代码中确实遇到了 Transaction rolled back because it has been marked as rollback-only
异常(尴尬的是,后来我也没能再复现该错误)。网上查阅了很多资料,发现自定义的代码并没有问题,但很多博主依据类似代码却能得到Transaction rolled back because it has been marked as rollback-only
异常。这里我个人还是觉得挺疑惑的,一度认为是自己哪里出了问题,最后实在复现不出来就放弃了,个人姑且认为是 JPA 或事务管理的版本问题。
rollback-only异常产生的原因
对于上述测试代码,稍微改变一下,最后结果也有所不同,这里就不赘述了,可以参考这篇文章。
从上述分析看,产生 rollback-only 异常需要同时满足以下前提:
1.事务方法嵌套,位于同一个事务中,方法位于不同的文件;
2.子方法抛出异常,被上层方法捕获和消化。
解决方法
1、捕获异常时,手动设置上层事务状态为 rollback 状态
@Transactional
public List<User> findAll() {
try {
userService.query("hresh2");
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return userRepository.findAll();
}
复制代码
日志输出如下所示:
2、修改事务传播机制,比如说将内层事务的传播方式指定为@Transactional(propagation= Propagation.NESTED)
,外层事务的提交和回滚能够控制嵌套的内层事务回滚;而内层事务报错时,只回滚内层事务,外层事务可以继续提交。
但尝试Propagation.NESTED
与 Hibernate JPA 一起使用将导致 Spring 异常,如下所示:
JpaDialect does not support savepoints - check your JPA provider's capabilities
复制代码
这是因为 Hibernate JPA 不支持嵌套事务。
导致异常的 Spring 代码是:
private SavepointManager getSavepointManager() {
...
SavepointManager savepointManager= getEntityManagerHolder().getSavepointManager();
if (savepointManager == null) {
throw new NestedTransactionNotSupportedException("JpaDialect does not support ...");
}
return savepointManager;
}
复制代码
可以考虑用 Propagation.REQUIRES_NEW 代替一下。
3、如果这个异常发生时,内层需要事务回滚的代码还没有执行,则可以@Transactional(noRollbackFor = {内层抛出的异常}.class)
,指定内层也不为这个异常回滚。
//UserServiceImpl.java
@Override
@Transactional(noRollbackFor = IllegalArgumentException.class)
public UserResponse query(String name) {
if (!name.equals("hresh")) {
throw new IllegalArgumentException("name is forbidden");
}
return null;
}
复制代码
4、内层方法取消@Transactional 注解,这样就不会发生回滚操作。
事务失效
接下来我们分析事务是否生效的问题。虽然大家对于同类自调用会导致事务失效这一知识点朗朗上口,但你真的了解吗?具体来说就是类A的方法a()调用方法b(),方法b()配置了事务,那么该事务在调用时不会生效。
Case 1
UserServiceImpl 中的两个方法
public List<UserResponse> findByAddress(String address) {
List<User> users = userRepository.findByAddress(address);
UserResponse userResponse = query("hresh");
return users.stream().map(this::toUserResponse).collect(Collectors.toList());
}
@Transactional
public UserResponse query(String name) {
User user = userRepository.findByName(name);
return toUserResponse(user);
}
复制代码
UserRepository 定义的查询方法
@EntityGraph(
attributePaths = {"jobs"}
)
List<User> findByAddress(String address);
复制代码
根据上述代码可知,findByAddress()方法没有配置事务,而 query()方法配置了事务,日志输出如下:
由上可知,query()方法的事务配置没有生效。我们进一步猜测,如果 query()方法中抛出异常,数据会回滚吗?答案可想而知,没有事务就不会回滚。
Case 2
如果类A的方法a()调用方法b(),方法a()、b()都配置了事务,那么又是什么结果呢?我们只需在 findByAddress()方法加上 @Transactional 注解,重新执行代码,结果如下:
根据结果可知,findByAddress()方法的事务生效了,但 query()方法的事务没有生效,因为它们两个共享同一个事务。
Case 3
在测试上述场景的过程中,我发现了一个有意思的情况,就是关于 save()方法的调用。
public UserResponse add(UserDTO userDTO) {
System.out.println("事务开启");
User user = User.builder().name(userDTO.getName())
.age(userDTO.getAge()).address(userDTO.getAddress()).build();
userRepository.save(user);
return toUserResponse(user);
}
复制代码
控制台输出为:
明明我们没有加@Transactional 注解,为什么会输出事务相关内容呢?这里可以深入源码进行分析,看看 JPA 自带的 save 方法是如何实现的,具体实现是在 SimpleJpaRepository 文件中。
@Transactional
public <S extends T> S save(S entity) {
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
复制代码
如果在 add 方法中调用配置了事务的 query()方法,日志输出为:
根据结果可知,query()方法的事务没有生效。且事务生效的范围仅在 save()方法上,而非 add()方法,如果此时 query()方法中抛出异常,add()方法是不会回滚的。感兴趣的朋友可以测试一下。
Case 4
如果此时在 add()方法上添加 @Transactional 注解,执行代码,控制台输出如下:
因为 Transactional 的传播机制默认为 REQUIRED,即如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以 save()方法的加入到了 add()方法的事务中。
如果此时 query()方法中抛出异常,不管 query()方法是否添加@Transactional 注解,add()方法都是会回滚的。
事务失效原因分析
事务不生效的原因在于,Spring 基于 AOP 机制实现事务的管理,不管是通过 @Authwired 来注入 UserService,还是其他方式,调用UserService 的方法时,实际上是通过 UserService 的代理类调用 UserService 的方法,代理类在执行目标方法前后,加上了事务管理的代码。
因此,只有通过注入的 UserService 调用事务方法,才会走代理类,才会执行事务管理;如果在同类直接调用,没走代理类,事务就无效。 注意:除了@Transactional,@Async 同样需要代理类调用,异步才会生效。
以前只是知道同类自调用会导致事务失效,刚学习了事务失效的背后原因,除此之外,在网上查阅资料的时候,又发现解决事务失效的三种方法,这里简单给大家介绍一下。
Way 1
@Service
//@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
@Override
@Transactional
public UserResponse query(String name) {
System.out.println("query方法事务开启");
User user = userRepository.findByName(name);
return toUserResponse(user);
}
@Override
public UserResponse add(UserDTO userDTO) {
System.out.println("事务开启");
User user = User.builder().name(userDTO.getName())
.age(userDTO.getAge()).address(userDTO.getAddress()).build();
userRepository.save(user);
userService.query(user.getName());
return toUserResponse(user);
}
}
复制代码
因为 Spring 通过三级缓存解决了循环依赖的问题,所以上面的写法不会有循环依赖问题。
但是使用@RequiredArgsConstructor 会出现循环依赖的问题,究其原因,是因为@RequiredArgsConstructor 是 Lombok 的注解,属于是构造器注入。
由此引出一个问题,为何@Autowired 来注入对象不会出现循环依赖,而@RequiredArgsConstructor 不行?
循环调用其实就是一个死循环,除非有终结条件。Spring 中循环依赖场景有:
- 构造器的循环依赖
- field 属性的循环依赖
对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。
Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。
我们使用@Autowired,将其添加到字段上,所以即使出现循环依赖,Spring 也可以应对。
Way 2
通过 ApplicationContext 获取到当前代理类,
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final ApplicationContext applicationContext;
@Override
@Transactional
public UserResponse query(String name) {
System.out.println("query方法事务开启");
User user = userRepository.findByName(name);
return toUserResponse(user);
}
@Override
public UserResponse add(UserDTO userDTO) {
System.out.println("事务开启");
User user = User.builder().name(userDTO.getName())
.age(userDTO.getAge()).address(userDTO.getAddress()).build();
userRepository.save(user);
UserService bean = applicationContext.getBean(UserService.class);
bean.query(user.getName());
return toUserResponse(user);
}
}
复制代码
不管要什么解决方案,都要尽量避免出现循环依赖,实在不行就使用@Autowired。
扩展
数据持久化自动生成新增时间
在 spring jpa 中,支持在字段或者方法上进行注解 @CreatedDate
、@CreatedBy
、@LastModifiedDate
、@LastModifiedBy
,从字面意思可以很清楚的了解,这几个注解的用处。
@CreatedDate
表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值@CreatedBy
表示该字段为创建人,在这个实体被 insert 的时候,会设置值@LastModifiedDate
、@LastModifiedBy
同理。
如何使用上述注解,并启用它们?
首先申明实体类,需要在类上加上注解 @EntityListeners(AuditingEntityListener.class)
,其次在 application 启动类中加上注解 EnableJpaAuditing
,或者定义一个 config 类,同时在需要的字段上加上 @CreatedDate
、@CreatedBy
、@LastModifiedDate
、@LastModifiedBy
等注解。
jpa.save 메서드가 호출되면 시간 필드가 자동으로 설정되어 데이터베이스에 삽입되지만 CreatedBy 및 LastModifiedBy에는 값이 할당되지 않습니다. 삽입해야 하는 값을 반환하기 위해 AuditorAware
인터페이스 입니다.
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@Configuration
public class UserIDAuditorBean implements AuditorAware<Long> {
@Override
public Long getCurrentAuditor() {
SecurityContext ctx = SecurityContextHolder.getContext();
if (ctx == null) {
return null;
}
if (ctx.getAuthentication() == null) {
return null;
}
if (ctx.getAuthentication().getPrincipal() == null) {
return null;
}
Object principal = ctx.getAuthentication().getPrincipal();
if (principal.getClass().isAssignableFrom(Long.class)) {
return (Long) principal;
} else {
return null;
}
}
}
复制代码
문제 로그
메소드에서 'java.lang.StackOverflowError' 예외가 발생했습니다. com.msdn.hresh.domain.User.toString()을 평가할 수 없습니다.
문제 원인: 디버그 모드에서는 User 클래스와 Job 클래스가 서로를 참조하고 lombok의 @Data 주석이 추가되기 때문에 @Data 주석이 toString() 메서드를 생성하고 이 두 가지가 클래스는 순환 방식으로 참조된 객체의 메서드를 지속적으로 호출하여 스택 오버플로가 발생하는 toString() 메서드를 사용합니다.
해결책:
1. @Data 주석을 삭제하고 @Getter 및 @Setter로 대체합니다.
2. toString() 메서드를 다시 작성하고 @Data 주석으로 구현된 toString()을 덮어쓰고 루프에서 메서드를 호출하지 않도록 주의합니다.
첫 번째 방법이 권장됩니다.
요약하다
개발을 위해 Spring 프레임워크를 사용하면 많은 트랜잭션 제어 세부 사항과 기본 복잡한 논리를 숨기고 개발의 복잡성을 크게 줄이는 편리함을 제공합니다. 그러나 기본 소스 코드에 대해 더 많이 알고 있다면 개발 및 문제 해결에 도움이 될 것입니다. 하지만 소스코드를 배우는 것 자체가 지루한 일이고, 소스코드를 공부해야 할 때 동기가 더 강해지고 효율성이 높아집니다.