背景
環(huán)境
相關(guān)環(huán)境配置:
SpringBoot+PostGreSQL
Spring Data JPA
問題
兩個(gè)使用 Transaction 注解的 ServiceA 和 ServiceB恨樟,在 A 中引入了 B 的方法用于更新數(shù)據(jù) 半醉,當(dāng) A 中捕捉到 B 中有異常時(shí),回滾動(dòng)作正常執(zhí)行劝术,但是當(dāng) return 時(shí)則出現(xiàn)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();
}
}
知識(shí)回顧
@Transactional
Spring Boot 默認(rèn)集成事務(wù),所以無須手動(dòng)開啟使用 @EnableTransactionManagement 注解夯尽,就可以用 @Transactional 注解進(jìn)行事務(wù)管理瞧壮。
@Transactional
的作用范圍
- 方法 :推薦將注解使用于方法上,不過需要注意的是:該注解只能應(yīng)用到 public 方法上匙握,否則不生效咆槽。
- 類 :如果這個(gè)注解使用在類上的話,表明該注解對(duì)該類中所有的 public 方法都生效圈纺。
- 接口 :不推薦在接口上使用秦忿。
@Transactional
的常用配置參數(shù)
@Transactional
事務(wù)注解原理
@Transactional
的工作機(jī)制是基于 AOP 實(shí)現(xiàn)的,AOP 又是使用動(dòng)態(tài)代理實(shí)現(xiàn)的蛾娶。如果目標(biāo)對(duì)象實(shí)現(xiàn)了接口灯谣,默認(rèn)情況下會(huì)采用 JDK 的動(dòng)態(tài)代理,如果目標(biāo)對(duì)象沒有實(shí)現(xiàn)了接口,會(huì)使用 CGLIB 動(dòng)態(tài)代理蛔琅。
如果一個(gè)類或者一個(gè)類中的 public 方法上被標(biāo)注@Transactional
注解的話胎许,Spring 容器就會(huì)在啟動(dòng)的時(shí)候?yàn)槠鋭?chuàng)建一個(gè)代理類,在調(diào)用被@Transactional
注解的 public 方法的時(shí)候罗售,實(shí)際調(diào)用的是辜窑,TransactionInterceptor
類中的 invoke()
方法。這個(gè)方法的作用就是在目標(biāo)方法之前開啟事務(wù)寨躁,方法執(zhí)行過程中如果遇到異常的時(shí)候回滾事務(wù)穆碎,方法調(diào)用完成之后提交事務(wù)。
Spring AOP 自調(diào)用問題
若同一類中的其他沒有 @Transactional
注解的方法內(nèi)部調(diào)用有 @Transactional
注解的方法职恳,有@Transactional
注解的方法的事務(wù)會(huì)失效所禀。
這是由于Spring AOP
代理的原因造成的方面,因?yàn)橹挥挟?dāng) @Transactional
注解的方法在類以外被調(diào)用的時(shí)候,Spring 事務(wù)管理才生效色徘。
關(guān)于 AOP 自調(diào)用的問題恭金,文章結(jié)尾會(huì)介紹相關(guān)解決方法。
@Transactional
的使用注意事項(xiàng)總結(jié)
-
@Transactional
注解只有作用到 public 方法上事務(wù)才生效贺氓,不推薦在接口上使用蔚叨; - 避免同一個(gè)類中調(diào)用
@Transactional
注解的方法,這樣會(huì)導(dǎo)致事務(wù)失效辙培; - 正確的設(shè)置
@Transactional
的 rollbackFor 和 propagation 屬性,否則事務(wù)可能會(huì)回滾失敗邢锯。
Spring 的 @Transactional
注解控制事務(wù)有哪些不生效的場景扬蕊?
- 數(shù)據(jù)庫引擎是否支持事務(wù)(MySQL的MyISAM引擎不支持事務(wù));
- 注解所在的類是否被加載成Bean類丹擎;
- 注解所在的方法是否為 public 方法尾抑;
- 是否發(fā)生了同類自調(diào)用問題;
- 所用數(shù)據(jù)源是否加載了事務(wù)管理器蒂培;
- @Transactional 的擴(kuò)展配置 propagation(事務(wù)傳播機(jī)制)是否正確再愈。
- 方法未拋出異常
- 異常類型錯(cuò)誤(最好配置rollback參數(shù),指定接收運(yùn)行時(shí)異常和非運(yùn)行時(shí)異常)
案例分析
構(gòu)建項(xiàng)目
1护戳、創(chuàng)建 Maven 項(xiàng)目翎冲,選擇相應(yīng)的依賴。一般不直接用 MySQL 驅(qū)動(dòng)媳荒,而選擇連接池抗悍。
<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 參數(shù)日志
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
實(shí)體類中的修改會(huì)同步到數(shù)據(jù)庫表結(jié)構(gòu)中钳枕,慎用缴渊。 -
show_sql
可開啟 hibernate 生成的 SQL,方便調(diào)試鱼炒。 -
open-in-view
指延時(shí)加載的一些屬性數(shù)據(jù)衔沼,可以在頁面展現(xiàn)的時(shí)候,保持 session 不關(guān)閉昔瞧,從而保證能在頁面進(jìn)行延時(shí)加載指蚁。默認(rèn)為 true。 -
logging
下的幾個(gè)參數(shù)用于顯示 sql 的參數(shù)硬爆。
3欣舵、MySQL 數(shù)據(jù)庫中創(chuàng)建兩個(gè)表
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、創(chuàng)建實(shí)體類并添加 JPA 注解
目前只創(chuàng)建兩個(gè)簡單的實(shí)體類缀磕,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缘圈、創(chuàng)建對(duì)應(yīng)的 Repository
實(shí)現(xiàn) JpaRepository 接口劣光,生成基本的 CRUD 操作樣板代碼。并且可根據(jù) Spring Data JPA 自帶的 Query Lookup Strategies 創(chuàng)建簡單的查詢操作糟把,在 IDEA 中輸入 findBy
等會(huì)有提示绢涡。
@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、創(chuàng)建 UserService 及其實(shí)現(xiàn)類
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();
}
}
復(fù)制代碼
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);
}
}
最后來看一下整個(gè)項(xiàng)目的結(jié)構(gòu)以及文件分布雄可。
基于上述代碼,我們將進(jìn)行特定知識(shí)的學(xué)習(xí)演示缠犀。
事務(wù)回滾
構(gòu)建必要的代碼如下:
//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 來進(jìn)行測試数苫,發(fā)現(xiàn)報(bào)錯(cuò)結(jié)果和預(yù)期不大一樣:
關(guān)鍵信息變?yōu)榱?Transaction silently rolled back because it has been marked as rollback-only
,這里我們暫不討論錯(cuò)誤提示信息為何發(fā)生了改變辨液,先集中討論報(bào)錯(cuò)原因虐急。
根據(jù)基礎(chǔ)知識(shí)中介紹的@Transactional 的作用范圍和傳播機(jī)制可知,當(dāng)我們?cè)?Service 文件類上添加 @Transactional 時(shí)滔迈,該注解對(duì)該類中所有的 public 方法都生效止吁,且傳播機(jī)制默認(rèn)為 PROPAGATION_REQUIRED,即如果當(dāng)前存在事務(wù)燎悍,則加入該事務(wù)敬惦;如果當(dāng)前沒有事務(wù),則創(chuàng)建一個(gè)新的事務(wù)谈山。
在這種情況下俄删,外層事務(wù)(UserApplication)和內(nèi)層事務(wù)(UserServiceImpl)就是一個(gè)事務(wù),任何一個(gè)出現(xiàn)異常勾哩,都會(huì)在 findAll()執(zhí)行完畢后回滾抗蠢。如果內(nèi)層事務(wù)拋出異常 IllegalArgumentException(沒有catch,繼續(xù)向外層拋出)思劳,在內(nèi)層事務(wù)結(jié)束時(shí)迅矛,Spring 會(huì)把內(nèi)層事務(wù)標(biāo)記為“rollback-only”;這時(shí)外層事務(wù)發(fā)現(xiàn)了異常 IllegalArgumentException潜叛,如果外層事務(wù) catch了異常并處理掉秽褒,那么外層事務(wù)A的方法會(huì)繼續(xù)執(zhí)行代碼,直到外層事務(wù)也結(jié)束時(shí)威兜,這時(shí)外層事務(wù)想 commit销斟,因?yàn)檎=Y(jié)束沒有向外拋異常,但是內(nèi)外層事務(wù)是同一個(gè)事務(wù)椒舵,事務(wù)已經(jīng)被內(nèi)層方法標(biāo)記為“rollback-only”蚂踊,需要回滾,無法 commit笔宿,這時(shí) Spring 就會(huì)拋出org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
犁钟,意思是“事務(wù)靜默回滾棱诱,因?yàn)樗驯粯?biāo)記為僅回滾”。
報(bào)錯(cuò)原因分析到此為止涝动,現(xiàn)在我們來分析一下為何自建簡易代碼復(fù)現(xiàn)時(shí)迈勋,錯(cuò)誤提示發(fā)生了變化,那么就直接深入代碼來分析一下醋粟。
根據(jù)日志打印的結(jié)果來看靡菇,rollback-only 異常發(fā)生于 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屬性,默認(rèn)為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默認(rèn)實(shí)現(xiàn)是false米愿。這里是指如果發(fā)現(xiàn)事務(wù)被標(biāo)記全局回滾并且在全局回滾標(biāo)記情況下不應(yīng)該提 // 交事務(wù)的話厦凤,那么則進(jìn)行回滾。
// defStatus.isGlobalRollbackOnly()進(jìn)行判斷是指讀取DefaultTransactionStatus中EntityTransaction對(duì)象的 // rollbackOnly標(biāo)志位育苟,即判斷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()) {
// 判斷當(dāng)前事務(wù)是否是個(gè)新事務(wù)泳唠,false表示參與現(xiàn)有事務(wù)或不在當(dāng)前事務(wù)中
if (status.isDebug()) {
this.logger.debug("Initiating transaction rollback");
}
this.doRollback(status);
} else {
if (status.hasTransaction()) {
// 參與現(xiàn)有事務(wù)
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);
}
}
結(jié)合上述代碼,通過斷點(diǎn)調(diào)試宙搬,大致可以梳理出如下邏輯:
1、當(dāng)內(nèi)層事務(wù)(UserServiceImpl)中的 query 拋出異常后拓哺,開始進(jìn)行回滾勇垛,即進(jìn)入 rollback()方法,接著進(jìn)入 processRollback()方法士鸥,此時(shí)第二個(gè)入?yún)⒌闹禐?false闲孤;
2、進(jìn)入 processRollback()方法后烤礁,首先判斷事物是否擁有 savepoint(回滾點(diǎn))讼积,如果有,就回滾到設(shè)置的 savepoint脚仔;接著判斷當(dāng)前事務(wù)是否是新事務(wù)勤众,因?yàn)檫@里是內(nèi)外層事務(wù),其實(shí)是同一個(gè)事務(wù)鲤脏,所以判斷結(jié)果為 false们颜;但 hasTransaction()判斷為 true,接著進(jìn)入 if 方法體猎醇,isLocalRollbackOnly()為 false窥突,isGlobalRollbackOnParticipationFailure()為 true(globalRollbackOnParticipationFailure默認(rèn)情況下為true,表示異常全局回滾)硫嘶,那么只能執(zhí)行 doSetRollbackOnly()方法阻问,此處只是補(bǔ)充打印一下日志;緊接著調(diào)用 isFailEarlyOnGlobalRollbackOnly()方法沦疾,這里主要是獲取 failEarlyOnGlobalRollbackOnly 字段的值称近,默認(rèn)情況下 failEarlyOnGlobalRollbackOnly 開關(guān)是關(guān)閉的第队,這個(gè)開關(guān)的作用是如果開啟了程序則會(huì)盡早拋出異常。最終 unexpectedRollback 字段仍為 false煌茬,所以沒有拋出 Transaction rolled back because it has been marked as rollback-only
異常斥铺。
3、內(nèi)層事務(wù)方法調(diào)用結(jié)束后坛善,回到外層方法晾蜘,在事務(wù)提交時(shí),即執(zhí)行 commit()方法,實(shí)際上執(zhí)行的是 processCommit()方法臀突。該方法中的邏輯和 processRollback()方法有些重疊料祠,此時(shí)判斷當(dāng)前事務(wù)是新事務(wù),所以 unexpectedRollback 就被賦值為 true岖常,最終拋出 Transaction silently rolled back because it has been marked as rollback-only
異常。
上面我們簡述了自定義代碼時(shí)葫督,為何只能得到 Transaction silently rolled back because it has been marked as rollback-only
異常竭鞍,但一開始在項(xiàng)目代碼中確實(shí)遇到了 Transaction rolled back because it has been marked as rollback-only
異常(尷尬的是,后來我也沒能再復(fù)現(xiàn)該錯(cuò)誤)橄镜。網(wǎng)上查閱了很多資料偎快,發(fā)現(xiàn)自定義的代碼并沒有問題,但很多博主依據(jù)類似代碼卻能得到Transaction rolled back because it has been marked as rollback-only
異常洽胶。這里我個(gè)人還是覺得挺疑惑的晒夹,一度認(rèn)為是自己哪里出了問題,最后實(shí)在復(fù)現(xiàn)不出來就放棄了姊氓,個(gè)人姑且認(rèn)為是 JPA 或事務(wù)管理的版本問題丐怯。
rollback-only異常產(chǎn)生的原因
對(duì)于上述測試代碼,稍微改變一下翔横,最后結(jié)果也有所不同读跷,這里就不贅述了
從上述分析看,產(chǎn)生 rollback-only 異常需要同時(shí)滿足以下前提:
1.事務(wù)方法嵌套棕孙,位于同一個(gè)事務(wù)中舔亭,方法位于不同的文件;
2.子方法拋出異常蟀俊,被上層方法捕獲和消化钦铺。
解決方法
1、捕獲異常時(shí)肢预,手動(dòng)設(shè)置上層事務(wù)狀態(tài)為 rollback 狀態(tài)
@Transactional
public List<User> findAll() {
try {
userService.query("hresh2");
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return userRepository.findAll();
}
日志輸出如下所示:
2矛洞、修改事務(wù)傳播機(jī)制,比如說將內(nèi)層事務(wù)的傳播方式指定為@Transactional(propagation= Propagation.NESTED)
,外層事務(wù)的提交和回滾能夠控制嵌套的內(nèi)層事務(wù)回滾沼本;而內(nèi)層事務(wù)報(bào)錯(cuò)時(shí)噩峦,只回滾內(nèi)層事務(wù),外層事務(wù)可以繼續(xù)提交抽兆。
但嘗試Propagation.NESTED
與 Hibernate JPA 一起使用將導(dǎo)致 Spring 異常识补,如下所示:
JpaDialect does not support savepoints - check your JPA provider's capabilities
這是因?yàn)?Hibernate JPA 不支持嵌套事務(wù)。
導(dǎo)致異常的 Spring 代碼是:
private SavepointManager getSavepointManager() {
...
SavepointManager savepointManager= getEntityManagerHolder().getSavepointManager();
if (savepointManager == null) {
throw new NestedTransactionNotSupportedException("JpaDialect does not support ...");
}
return savepointManager;
}
可以考慮用 Propagation.REQUIRES_NEW 代替一下辫红。
3凭涂、如果這個(gè)異常發(fā)生時(shí),內(nèi)層需要事務(wù)回滾的代碼還沒有執(zhí)行贴妻,則可以@Transactional(noRollbackFor = {內(nèi)層拋出的異常}.class)
切油,指定內(nèi)層也不為這個(gè)異常回滾名惩。
//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澎胡、內(nèi)層方法取消@Transactional 注解,這樣就不會(huì)發(fā)生回滾操作娩鹉。
事務(wù)失效
接下來我們分析事務(wù)是否生效的問題攻谁。雖然大家對(duì)于同類自調(diào)用會(huì)導(dǎo)致事務(wù)失效這一知識(shí)點(diǎn)朗朗上口,但你真的了解嗎弯予?具體來說就是類A的方法a()調(diào)用方法b()巢株,方法b()配置了事務(wù),那么該事務(wù)在調(diào)用時(shí)不會(huì)生效熙涤。
Case 1
UserServiceImpl 中的兩個(gè)方法
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);
根據(jù)上述代碼可知,findByAddress()方法沒有配置事務(wù)困檩,而 query()方法配置了事務(wù)祠挫,日志輸出如下:
由上可知,query()方法的事務(wù)配置沒有生效悼沿。我們進(jìn)一步猜測等舔,如果 query()方法中拋出異常,數(shù)據(jù)會(huì)回滾嗎糟趾?答案可想而知慌植,沒有事務(wù)就不會(huì)回滾。
Case 2
如果類A的方法a()調(diào)用方法b()义郑,方法a()蝶柿、b()都配置了事務(wù),那么又是什么結(jié)果呢非驮?我們只需在 findByAddress()方法加上 @Transactional 注解交汤,重新執(zhí)行代碼,結(jié)果如下:
根據(jù)結(jié)果可知劫笙,findByAddress()方法的事務(wù)生效了芙扎,但 query()方法的事務(wù)沒有生效星岗,因?yàn)樗鼈儍蓚€(gè)共享同一個(gè)事務(wù)。
Case 3
在測試上述場景的過程中戒洼,我發(fā)現(xiàn)了一個(gè)有意思的情況俏橘,就是關(guān)于 save()方法的調(diào)用。
public UserResponse add(UserDTO userDTO) {
System.out.println("事務(wù)開啟");
User user = User.builder().name(userDTO.getName())
.age(userDTO.getAge()).address(userDTO.getAddress()).build();
userRepository.save(user);
return toUserResponse(user);
}
控制臺(tái)輸出為:
明明我們沒有加@Transactional 注解圈浇,為什么會(huì)輸出事務(wù)相關(guān)內(nèi)容呢寥掐?這里可以深入源碼進(jìn)行分析,看看 JPA 自帶的 save 方法是如何實(shí)現(xiàn)的汉额,具體實(shí)現(xiàn)是在 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 方法中調(diào)用配置了事務(wù)的 query()方法,日志輸出為:
根據(jù)結(jié)果可知蠕搜,query()方法的事務(wù)沒有生效怎茫。且事務(wù)生效的范圍僅在 save()方法上,而非 add()方法妓灌,如果此時(shí) query()方法中拋出異常轨蛤,add()方法是不會(huì)回滾的。感興趣的朋友可以測試一下虫埂。
Case 4
如果此時(shí)在 add()方法上添加 @Transactional 注解祥山,執(zhí)行代碼,控制臺(tái)輸出如下:
因?yàn)?Transactional 的傳播機(jī)制默認(rèn)為 REQUIRED掉伏,即如果上下文中已經(jīng)存在事務(wù)缝呕,那么就加入到事務(wù)中執(zhí)行,如果當(dāng)前上下文中不存在事務(wù)斧散,則新建事務(wù)執(zhí)行供常。所以 save()方法的加入到了 add()方法的事務(wù)中。
如果此時(shí) query()方法中拋出異常鸡捐,不管 query()方法是否添加@Transactional 注解栈暇,add()方法都是會(huì)回滾的。
事務(wù)失效原因分析
事務(wù)不生效的原因在于箍镜,Spring 基于 AOP 機(jī)制實(shí)現(xiàn)事務(wù)的管理源祈,不管是通過 @Authwired 來注入 UserService,還是其他方式色迂,調(diào)用UserService 的方法時(shí)香缺,實(shí)際上是通過 UserService 的代理類調(diào)用 UserService 的方法,代理類在執(zhí)行目標(biāo)方法前后歇僧,加上了事務(wù)管理的代碼赫悄。
因此,只有通過注入的 UserService 調(diào)用事務(wù)方法,才會(huì)走代理類埂淮,才會(huì)執(zhí)行事務(wù)管理姑隅;如果在同類直接調(diào)用,沒走代理類倔撞,事務(wù)就無效讲仰。 注意:除了@Transactional,@Async 同樣需要代理類調(diào)用痪蝇,異步才會(huì)生效鄙陡。
以前只是知道同類自調(diào)用會(huì)導(dǎo)致事務(wù)失效,剛學(xué)習(xí)了事務(wù)失效的背后原因躏啰,除此之外趁矾,在網(wǎng)上查閱資料的時(shí)候,又發(fā)現(xiàn)解決事務(wù)失效的三種方法给僵,這里簡單給大家介紹一下毫捣。
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方法事務(wù)開啟");
User user = userRepository.findByName(name);
return toUserResponse(user);
}
@Override
public UserResponse add(UserDTO userDTO) {
System.out.println("事務(wù)開啟");
User user = User.builder().name(userDTO.getName())
.age(userDTO.getAge()).address(userDTO.getAddress()).build();
userRepository.save(user);
userService.query(user.getName());
return toUserResponse(user);
}
}
因?yàn)?Spring 通過三級(jí)緩存解決了循環(huán)依賴的問題,所以上面的寫法不會(huì)有循環(huán)依賴問題帝际。
但是使用@RequiredArgsConstructor 會(huì)出現(xiàn)循環(huán)依賴的問題蔓同,究其原因,是因?yàn)锧RequiredArgsConstructor 是 Lombok 的注解蹲诀,屬于是構(gòu)造器注入斑粱。
由此引出一個(gè)問題,為何@Autowired 來注入對(duì)象不會(huì)出現(xiàn)循環(huán)依賴脯爪,而@RequiredArgsConstructor 不行则北?
循環(huán)調(diào)用其實(shí)就是一個(gè)死循環(huán),除非有終結(jié)條件痕慢。Spring 中循環(huán)依賴場景有:
- 構(gòu)造器的循環(huán)依賴
- field 屬性的循環(huán)依賴
對(duì)于構(gòu)造器的循環(huán)依賴咒锻,Spring 是無法解決的,只能拋出 BeanCurrentlyInCreationException 異常表示循環(huán)依賴守屉,所以下面我們分析的都是基于 field 屬性的循環(huán)依賴。
Spring 只解決 scope 為 singleton 的循環(huán)依賴蒿辙,對(duì)于scope 為 prototype 的 bean Spring 無法解決拇泛,直接拋出 BeanCurrentlyInCreationException 異常。
我們使用@Autowired思灌,將其添加到字段上俺叭,所以即使出現(xiàn)循環(huán)依賴,Spring 也可以應(yīng)對(duì)泰偿。
Way 2
通過 ApplicationContext 獲取到當(dāng)前代理類熄守,
@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方法事務(wù)開啟");
User user = userRepository.findByName(name);
return toUserResponse(user);
}
@Override
public UserResponse add(UserDTO userDTO) {
System.out.println("事務(wù)開啟");
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);
}
}
不管要什么解決方案,都要盡量避免出現(xiàn)循環(huán)依賴,實(shí)在不行就使用@Autowired裕照。
擴(kuò)展
數(shù)據(jù)持久化自動(dòng)生成新增時(shí)間
在 spring jpa 中攒发,支持在字段或者方法上進(jìn)行注解 @CreatedDate
、@CreatedBy
晋南、@LastModifiedDate
惠猿、@LastModifiedBy
,從字面意思可以很清楚的了解负间,這幾個(gè)注解的用處偶妖。
-
@CreatedDate
表示該字段為創(chuàng)建時(shí)間時(shí)間字段,在這個(gè)實(shí)體被 insert 的時(shí)候政溃,會(huì)設(shè)置值 -
@CreatedBy
表示該字段為創(chuàng)建人趾访,在這個(gè)實(shí)體被 insert 的時(shí)候,會(huì)設(shè)置值 -
@LastModifiedDate
董虱、@LastModifiedBy
同理扼鞋。
如何使用上述注解,并啟用它們空扎?
首先申明實(shí)體類藏鹊,需要在類上加上注解 @EntityListeners(AuditingEntityListener.class)
,其次在 application 啟動(dòng)類中加上注解 EnableJpaAuditing
转锈,或者定義一個(gè) config 類盘寡,同時(shí)在需要的字段上加上 @CreatedDate
、@CreatedBy
撮慨、@LastModifiedDate
竿痰、@LastModifiedBy
等注解。
在 jpa.save 方法被調(diào)用的時(shí)候砌溺,時(shí)間字段會(huì)自動(dòng)設(shè)置并插入數(shù)據(jù)庫影涉,但是 CreatedBy 和 LastModifiedBy 并沒有賦值,因?yàn)樾枰獙?shí)現(xiàn) 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;
}
}
}
問題記錄
Method threw 'java.lang.StackOverflowError' exception. Cannot evaluate com.msdn.hresh.domain.User.toString()
問題出現(xiàn)的原因:debug 模式下蟹倾,因?yàn)?User 類和 Job 類相互引用,以及都加了 lombok 的 @Data 注解猖闪,@Data 注解會(huì)生成 toString()方法鲜棠,而這兩個(gè)類在使用 toString()方法時(shí),會(huì)不斷的互相循環(huán)調(diào)用引用對(duì)象的方法培慌,導(dǎo)致棧溢出豁陆。
解決辦法:
1、刪去@Data 注解吵护,用@Getter 和@Setter 來代替盒音;
2表鳍、重寫 toString()方法,覆蓋@Data 注解實(shí)現(xiàn)的 toString()祥诽,注意不要再互相循環(huán)調(diào)用方法譬圣。
推薦使用第一種方法。
總結(jié)
使用 Spring 框架進(jìn)行開發(fā)給我們提供了便利原押,隱藏了很多事務(wù)控制的細(xì)節(jié)和底層繁瑣的邏輯胁镐,極大的減少了開發(fā)的復(fù)雜度。但是诸衔,如果我們對(duì)底層源碼多一些了解的話盯漂,對(duì)于開發(fā)和問題排查都會(huì)有所幫助。不過學(xué)習(xí)源碼本身就是一件枯燥的事情笨农,需要時(shí)再去研究源碼就缆,動(dòng)力更強(qiáng)一些,效率更高一些谒亦。