Spring中通過@Transactional注解動(dòng)態(tài)代理對(duì)目標(biāo)方法的增強(qiáng),可以很方便的回滾事務(wù)垒迂。但是械姻,如果不熟悉使用@Transactional注解的話,卻會(huì)有很多隱藏的坑不容易被發(fā)現(xiàn)机断,往往是在線上環(huán)境才出現(xiàn)問題楷拳,通過一番排查才找到問題所在绣夺,以下是本人實(shí)際工作中或是瀏覽其他相關(guān)博客模擬實(shí)現(xiàn)的場(chǎng)景,以此加深記憶和記錄欢揖。
1.@Transactional注解標(biāo)記的方法是private
2.@Transactional注解標(biāo)記的方法不是Spring注入的bean調(diào)用
3.@Transactional注解沒有顯示聲明rollbackFor屬性
4.@Transactional注解標(biāo)記的方法內(nèi)陶耍,使用try...catch捕獲異常
5.@Transactional注解使用默認(rèn)的傳播機(jī)制
打開@Transactional注解的內(nèi)容
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
廢話不多說,直接上案例她混!
以下的案例都是模擬新增用戶的流程该酗,為了簡便畴栖,使用Spring Data JPA操作數(shù)據(jù)庫。
User實(shí)體類
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
// 省略getter、setter
}
UserDao類
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
}
Controller類
@RestController
public class TransactionController {
@Autowired
private UserService userService;
@GetMapping("test")
public void test(){
userService.createUser();
}
}
1.@Transactional注解標(biāo)記的方法是private
接下來看下Service實(shí)現(xiàn)類
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 創(chuàng)建用戶
*/
public void createUser() {
insertUser();
}
@Transactional
private void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new RuntimeException("錯(cuò)誤");
}
}
訪問:http://localhost:8080/test后趟庄,可以發(fā)現(xiàn)控制臺(tái)報(bào)錯(cuò)证鸥,證明有拋出異常镜盯,那么事務(wù)是否有回滾呢吗浩?查看一下數(shù)據(jù)庫的User表,卻發(fā)現(xiàn)有新增用戶信息来累,就證明事務(wù)并沒有回滾算吩,事務(wù)回滾失效了!
這是為什么呢佃扼?這就需要知道@Transactional的原理偎巢,實(shí)際上就是Spring中的AOP,使用@Transactional注解兼耀,Spring就會(huì)通過動(dòng)態(tài)代理的方式增強(qiáng)目標(biāo)方法压昼。所以private的方法是無法被代理,所以動(dòng)態(tài)代理失效瘤运,無法回滾事務(wù)窍霞!
既然知道原因,那是不是將private方法改為public就行啦拯坟?
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 創(chuàng)建用戶
*/
public void createUser() {
insertUser();
}
@Transactional
public void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new RuntimeException("錯(cuò)誤");
}
}
再次訪問http://localhost:8080/test但金,雖然控制臺(tái)有輸出報(bào)錯(cuò)信息,但還是沒有回滾數(shù)據(jù)庫的操作郁季,這就納悶了冷溃,不是使用@Transactional注解就可以了嗎?
這就引申到下一個(gè)"坑"了
2.@Transactional注解標(biāo)記的方法不是Spring注入的bean調(diào)用
有點(diǎn)拗口梦裂,其實(shí)簡單理解為@Transactional注解標(biāo)記的方法應(yīng)該是Bean的調(diào)用似枕,而不是方法內(nèi)調(diào)用。例子中@Transactional注解標(biāo)記的方法是由Bean內(nèi)部方法的調(diào)用年柠,所以將@Transactional注解放到例子中的createUser方法就可以了凿歼。
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 創(chuàng)建用戶
*/
@Transactional
public void createUser() {
insertUser();
}
public void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new RuntimeException("錯(cuò)誤");
}
}
訪問http://localhost:8080/test,這次數(shù)據(jù)表就沒有新增用戶信息了,就證明事務(wù)回滾答憔。
小結(jié):使用@Transactional注解的方法味赃,訪問級(jí)別應(yīng)該是public,而且應(yīng)該是被Bean調(diào)用的方法
3.@Transactional注解沒有顯示聲明rollbackFor屬性
那我再對(duì)Service改一下虐拓,拋出的異常由原來的RuntimeException
改為Exception
@Service
public class UserService{
@Autowired
private UserDao userDao;
/**
* 創(chuàng)建用戶
*/
@Transactional
public void createUser() throws Exception {
insertUser();
}
public void insertUser() throws Exception {
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
throw new Exception("錯(cuò)誤");
}
}
訪問http://localhost:8080/test心俗,再次發(fā)現(xiàn)由新增用戶信息。My God侯嘀!這又是什么坑呀?
其實(shí)谱轨,這是由于不熟悉@Transactional注解的原因戒幔。
這是因?yàn)镾pring框架的事務(wù)管理默認(rèn)地只在發(fā)生不受控異常(RuntimeException和Error)時(shí)才進(jìn)行事務(wù)回滾。也就是說土童,當(dāng)事務(wù)方法拋出受控異常(Exception中除了RuntimeException及其子類以外的)時(shí)不會(huì)進(jìn)行事務(wù)回滾诗茎。
而rollbackFor
屬性的默認(rèn)值是 RuntimeException ,但是如果拋出的異常是 Exception 類型献汗,@Transactional注解無法捕獲異常敢订,所以也就無法回滾事務(wù)。阿里巴巴規(guī)范建議使用@Transactional注解的時(shí)候顯式地聲明rollbackFor屬性的值
// @Transactional注解 rollbackFor 屬性默認(rèn)值
@Transactional(rollbackFor = RuntimeException.class)
錯(cuò)誤使用:
@Transactional
public void test(){}
正確使用:
@Transactional(rollbackFor = Exception.class)
public void test(){}
ps.強(qiáng)烈建議大家在Idea上安裝阿里巴巴規(guī)范插件罢吃,插件掃描代碼楚午,發(fā)現(xiàn)有不規(guī)范的地方就回有提示,使咱們的代碼更加規(guī)范尿招、更加優(yōu)雅矾柜!
將原本使用 @Transactional 改為 @Transactional(rollbackFor = Exception.class)后,重新啟動(dòng)訪問http://localhost:8080/test后可以發(fā)現(xiàn)就谜,用戶信息沒有新增怪蔑,就證明事務(wù)回滾了!
小結(jié):使用 @Transactional 注解的時(shí)候丧荐,為了避免隱藏的bug缆瓣,一定要顯式聲明rollbackFor屬性的值!
4.@Transactional注解標(biāo)記的方法內(nèi)虹统,使用try...catch捕獲異常
接下來弓坞,模擬另外一個(gè)坑,這也是一個(gè)十分常見的事務(wù)失效問題
改動(dòng)使用 @Transactional 注解的方法车荔,將原本throw異常改為try...catch捕獲異常
/**
* 創(chuàng)建用戶
*/
@Transactional(rollbackFor = Exception.class)
public void createUser(){
try {
insertUser();
} catch (Exception e) {
e.printStackTrace();
}
}
訪問http://localhost:8080/test后可以發(fā)現(xiàn)昼丑,用戶信息新增了,就證明事務(wù)并沒有回滾夸赫!
這是因?yàn)楫惓P畔⒃诒籃Transactional捕獲之前被try...catch...捕獲了菩帝,相對(duì)于try...catch..."吃"掉了異常,@Transactional就無法捕獲異常,所以就無法回滾事務(wù)呼奢!
那我想通過使用try...catch...捕獲異常并做出一些補(bǔ)償機(jī)制宜雀,怎么辦?其實(shí)也是可以的握础,加上一行:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
/**
* 創(chuàng)建用戶
*/
@Transactional(rollbackFor = Exception.class)
public void createUser(){
try {
insertUser();
} catch (Exception e) {
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 可以自定義出異常后的操作
}
}
小結(jié):使用@Transactional注解的時(shí)候辐董,要注意異常信息會(huì)不會(huì)被try...catch...捕獲。
5.@Transactional注解使用默認(rèn)的傳播機(jī)制
@Transactional注解中禀综,有個(gè)屬性propagation简烘,默認(rèn)的傳播級(jí)別為Propagation.REQUIRED
propagation屬性的值有以下幾種選擇
- Propagation.REQUIRED(默認(rèn)):如果當(dāng)前存在事務(wù),則加入該事務(wù)定枷,如果當(dāng)前不存在事務(wù)孤澎,則創(chuàng)建一個(gè)新的事務(wù)
- Propagation.SUPPORTS:如果當(dāng)前存在事務(wù),則加入事務(wù)欠窒,沒有則以非事務(wù)方式運(yùn)行
- Propagation.MANDATORY:當(dāng)前存在事務(wù)覆旭,則加入事務(wù),不存在事務(wù)則拋出異常
- Propagation.REQUIRES_NEW:創(chuàng)建一個(gè)新的事務(wù)岖妄,如果當(dāng)前存在事務(wù)型将,則把當(dāng)前事務(wù)掛起
- Propagation.NOT_SUPPORTED:以非事務(wù)方式運(yùn)行,如果當(dāng)前存在事務(wù)荐虐,則把當(dāng)前事務(wù)掛起
- Propagation.NEVER:以非事務(wù)方式運(yùn)行七兜,如果當(dāng)前存在事務(wù),則拋出異常
- Propagation.NESTED:如果當(dāng)前存在事務(wù)福扬,則創(chuàng)建一個(gè)事務(wù)作為當(dāng)前事務(wù)的嵌套事務(wù)來運(yùn)行惊搏;如果當(dāng)前沒有事務(wù),則該取值等價(jià)于TransactionDefinition.PROPAGATION_REQUIRED
但要根據(jù)實(shí)際的業(yè)務(wù)場(chǎng)景選擇事務(wù)傳播級(jí)別忧换,不一定默認(rèn)的傳播級(jí)別適用恬惯!
假設(shè)現(xiàn)在的業(yè)務(wù)場(chǎng)景是,先創(chuàng)建用戶信息亚茬,然后根據(jù)用戶信息創(chuàng)建學(xué)生信息(Student表)酪耳,但如果由于某些原因,創(chuàng)建學(xué)生信息失敗刹缝,但不能影響用戶信息的創(chuàng)建碗暗。所以創(chuàng)建用戶信息和學(xué)生信息應(yīng)該在不同的事務(wù)內(nèi),這樣才不會(huì)相互影響梢夯,這樣的話言疗,使用@Transactional默認(rèn)的傳播級(jí)別就實(shí)現(xiàn)不了,但我們可以改變propagation
屬性值颂砸,改為Propagation.REQUIRES_NEW
Student實(shí)體類
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String classroom;
// 省略getter噪奄、setter
}
StudentDao類
@Repository
public interface StudentDao extends JpaRepository<Student,Integer> {
}
StudentService實(shí)現(xiàn)類
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
/**
* 創(chuàng)建學(xué)生基本信息
*/
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void createStudentInfo() throws Exception {
Student student = new Student();
student.setName("MuggleLee");
student.setClassroom("高一一班");
studentDao.save(student);
throw new Exception("錯(cuò)誤");
}
}
UserService實(shí)現(xiàn)類
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private StudentService studentService;
/**
* 創(chuàng)建用戶
*/
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void createUser() {
insertUser();
try {
studentService.createStudentInfo();
} catch (Exception e) {
e.printStackTrace();
}
}
private void insertUser(){
User user = new User();
user.setName("MuggleLee");
userDao.save(user);
}
}
重啟后訪問http://localhost:8080/test死姚,可以發(fā)現(xiàn)用戶信息可以正常新增,但學(xué)生信息卻沒有新增勤篮,就證明學(xué)生新增信息被事務(wù)回滾都毒,但不影響用戶信息新增。
以上都是常見的事務(wù)失效的場(chǎng)景碰缔,希望能夠諸位在開發(fā)的時(shí)候账劲,多加注意!
如果覺得文章不錯(cuò)的話金抡,麻煩點(diǎn)個(gè)贊哈瀑焦,你的鼓勵(lì)就是我的動(dòng)力!對(duì)于文章有哪里不清楚或者有誤的地方梗肝,歡迎在評(píng)論區(qū)留言~
參考資料:
極客時(shí)間——專欄:Java業(yè)務(wù)開發(fā)常見錯(cuò)誤100例