如題,最近碰到了一個問題,在public方法上添加@Transaction沒有生效,事務沒有回滾。
我自己模擬了一個功能,向數據庫表User里面插入用戶數據。說一下代碼背景,
數據庫MySQL,持久化層Mybatis,項目使用SpringBoot。
數據表User如下,已有一條數據:
下面是代碼
- Application啟動類
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import tk.mybatis.spring.annotation.MapperScan;@SpringBootApplication(scanBasePackages = {"com.example.demo","com.example.demo.dao"})
//啟動事務
@EnableTransactionManagement
//mapper接口掃描
@MapperScan("com.example.demo.dao")
@RestController
public class DemoApplication {@GetMapping("/")String home() {return "Spring is here!";}public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
- 實體類User,對應數據表user表
@Data
@Table(name = "`user`")
public class User {@Id@Column(name = "`id`")@GeneratedValue(strategy = GenerationType.IDENTITY)private int id;@Column(name = "`name`")private String name;@Column(name = "`created_time`")private Date createdTime;
}
- Mapper類
package com.example.demo.dao;import com.example.demo.domain.po.User;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;/*** TOrderMapper** @author zhouxy@133.cn* @date 2022/8/15**/
@Repository
public interface UserMapper extends Mapper<User>, MySqlMapper<User> {
}
- Service接口,處理業務邏輯,添加@Transaction
package com.example.demo.service;import com.example.demo.domain.po.User;
import org.springframework.stereotype.Service;/*** @Description: TODO* @Author: zhouxy* @CreateTime: 2023-10-17*/
public interface IUserService {Integer addUser(User user);
}
- service實現類
package com.example.demo.service.impl;import com.example.demo.dao.UserMapper;
import com.example.demo.domain.po.User;
import com.example.demo.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;/*** @Description: TODO* @Author: zhouxy* @CreateTime: 2023-10-17*/
@Service
public class UserService implements IUserService {@Autowiredprivate UserMapper userMapper;@Override@Transactionalpublic Integer addUser(User user) {Integer result = userMapper.insertSelective(user);}
}
- AOP類
package com.example.demo.aop;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;/*** @Description: TODO* @Author: zhouxy* @CreateTime: 2023-05-17*/
@Aspect
@Component
@Slf4j
public class LogAop {@Pointcut(value = "execution(* com.example.demo.service.*.*(..))")public void controller() {log.info("223432");}@Around("controller()")public Object around(ProceedingJoinPoint joinPoint) {log.info("before================");try {Object res = joinPoint.proceed();return res;} catch (Throwable throwable) {log.error("", throwable);}finally {log.info("after================");}return null;}
}
- 測試類
import com.example.demo.DemoApplication;
import com.example.demo.dao.UserMapper;
import com.example.demo.domain.po.User;
import com.example.demo.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;/*** MybatisTest** @author zhouxy@133.cn* @date 2021/7/20**/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class MybatisTest {@Autowiredprivate IUserService userService;@Testpublic void test() {User user = new User();user.setName("LISI222");userService.addUser(user);}
}
當正常執行的時候,會在數據庫中插入數據。
打印日志:
此時在實現類中加入一行異常,看一下出現異常之后,事務是否會回滾,數據能否成功插入到數據表中。
package com.example.demo.service.impl;import com.example.demo.dao.UserMapper;
import com.example.demo.domain.po.User;
import com.example.demo.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;/*** @Description: TODO* @Author: zhouxy* @CreateTime: 2023-10-17*/
@Service
public class UserService implements IUserService {@Autowiredprivate UserMapper userMapper;@Override@Transactionalpublic Integer addUser(User user) {Integer result = userMapper.insertSelective(user);//添加一行異常,查看事務是否會因為拋出異常回滾剛才的插入操作throw new RuntimeException();}
}
我們看執行完之后,數據庫的變化,我剛才插入的是用戶:LISI222
顯示的是插入成功,看后臺日志顯示,事務并沒有做回滾操作,說明當前事務并沒有捕獲到異常信息。仔細分析日志頭尾可知,日志打印操作包裹了service實現類的addUser方法,應該是aop操作在@Transaction之前執行,導致異常被aop處理了,aop最終也沒有拋出異常,所以@Transaction方法沒有捕獲到異常信息。
2023-11-24 10:53:24.418 INFO [,,,] 29600 --- [ main] com.example.demo.aop.LogAop : before================
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c3884f5]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5542418c] will be managed by Spring
==> Preparing: INSERT INTO `user` ( `id`,`name` ) VALUES( ?,? )
==> Parameters: 0(Integer), LISI222(String)
<== Updates: 1
==> Executing: SELECT LAST_INSERT_ID()
<== Columns: LAST_INSERT_ID()
<== Row: 11
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c3884f5]
2023-11-24 10:53:24.566 ERROR [,,,] 29600 --- [ main] com.example.demo.aop.LogAop : java.lang.RuntimeException: nullat com.example.demo.service.impl.UserService.addUser(UserService.java:24) ~[classes/:na]at com.example.demo.service.impl.UserService$$FastClassBySpringCGLIB$$9c38b50f.invoke(<generated>) ~[classes/:na]····異常信息打印,忽略2023-11-24 10:53:24.566 INFO [,,,] 29600 --- [ main] com.example.demo.aop.LogAop : after================
這里仍然正常提交事務,說明沒有捕獲到異常信息
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c3884f5]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c3884f5]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c3884f5]
現在是因為aop中沒有將異常拋出,做了友好化處理,將異常處理成了返回了錯誤碼。所以需要在aop處理返回結果之前,事務就要捕獲到當前事務中拋出的異常并進行回滾或者不提交事務。
我在網上查找資料的時候查找到@Order注解,@Order的作用是定義Spring IOC容器中Bean的執行順序的優先級(這里的順序也可以理解為存放到容器中的先后順序),所以我給LogAop類加了一個@Order注解,讓LogAOP在最后執行,這樣@Transaction類就可以提前執行,捕獲異常,回滾數據。@Order默認為最低優先級,因此可以直接設置@Order即可
@Aspect
@Component
@Slf4j
@Order(10) //定義LogAOP的順序最后執行,值越高,優先級越低
public class LogAop {
·····
}
這次再執行一遍,查看是否成功回滾事務。
再看下日志,事務在aop之前執行完成。
2023-11-24 10:59:04.879 INFO [,,,] 28412 --- [ main] com.example.demo.aop.LogAop : before================
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@786e2c5e]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@78854721] will be managed by Spring
==> Preparing: INSERT INTO `user` ( `id`,`name` ) VALUES( ?,? )
==> Parameters: 0(Integer), LISI333(String)
<== Updates: 1
==> Executing: SELECT LAST_INSERT_ID()
<== Columns: LAST_INSERT_ID()
<== Row: 12
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@786e2c5e]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@786e2c5e]
這里關閉了事務,并且沒有提交操作。
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@786e2c5e]
2023-11-24 10:59:05.271 ERROR [,,,] 28412 --- [ main] com.example.demo.aop.LogAop : java.lang.RuntimeException: nullat com.example.demo.service.impl.UserService.addUser(UserService.java:24) ~[classes/:na]at com.example.demo.service.impl.UserService$$FastClassBySpringCGLIB$$9c38b50f.invoke(<generated>) ~[classes/:na]
2023-11-24 10:59:05.272 INFO [,,,] 28412 --- [ main] com.example.demo.aop.LogAop : after================
使用@Order注解可以調整運行級別,但是需注意一個問題,aop中不要做一些業務操作,因為transaction不會捕獲到aop中拋出的異常。可以使用try…catch捕獲aop中的數據庫操作,并進行回滾。