一个目录
- 前言
- 业务代码展示
- 手动挡
- 自动挡
- 事务失效的问题
- 代码地址
前言
在两年半以前,我写了一篇博客:框架的灵魂之注解基础篇:
在那篇博客的结尾,我埋了一个坑:
如今,我练习时长达两年半,终于摔锅归来!
本篇博客基于SpringBoot整合MyBatis-plus,如果有不懂这个的,
可以查看我的这篇博客:快速CRUD的秘诀之SpringBoot整合MyBatis-Plus
注意:这篇博客的重点在于:如何利用自定义注解结合Spring的AOP思想来实现自动进行事务管理,
所以事务方面的知识可以说是一点也没有,需要这方面知识的可以划走了hhh
业务代码展示
0.老规矩,首先来配置一下配置文件application.yml
:
# 配置数据源
spring:
datasource:
# 数据库路径jdbc:mysql://localhost:3306/mydb 的缩写,并配置时区
url: jdbc:mysql:///mydb?serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
# 打印MyBatis SQL 日志
logging:
level:
com.guqueyue.myTransactional.dao: debug # 写接口的包名
server:
port: 8082 #端口
1.控制层
package com.guqueyue.myTransactional.controller;
import com.guqueyue.myTransactional.entity.User;
import com.guqueyue.myTransactional.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: guqueyue
* @Description: 用户控制层
* @Date: 2023/12/19
**/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
/**
* 插入用户
* @return
*/
@RequestMapping("/insertUser")
public Integer insertUser(User user) {
System.out.println("接收到的用户为:" + user);
return userService.insertUser(user);
}
}
2.service接口
package com.guqueyue.myTransactional.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.guqueyue.myTransactional.entity.User;
/**
* @Author: guqueyue
* @Description: 用户service接口
* @Date: 2023/12/19
**/
public interface IUserService extends IService<User> {
Integer insertUser(User user);
}
3.service实现类
package com.guqueyue.myTransactional.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guqueyue.myTransactional.dao.UserMapper;
import com.guqueyue.myTransactional.entity.User;
import com.guqueyue.myTransactional.service.IUserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @Author: guqueyue
* @Description: 用户实现类
* @Date: 2023/12/19
**/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public Integer insertUser(User user) {
int result = userMapper.insert(user);
// 写一个异常
int i = 1/0;
return result;
}
}
4.持久层
package com.guqueyue.myTransactional.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.guqueyue.myTransactional.entity.User;
/**
* @Author: guqueyue
* @Description: 映射接口UserMapper
* @Date: 2023/12/19
**/
public interface UserMapper extends BaseMapper<User> {
}
我们这个时候启动项目,浏览器输入:http://localhost:8082/user/insertUser?username=张三&password=666666
我们在控制台可以看到SQL语句的执行,并且随后抛出了一个异常:
观察数据库发现,成功插入了一条数据:
但是这样肯定是不对的,如果一个方法发生了异常,一部分代码执行一部分代码没执行,这种情况是很危险的!!!
所以,我们需要回滚这个方法已经执行的SQL,不然就全乱套啦!
因此,我们需要事务管理来进行SQL的回滚。
那么,怎么实现呢?请看下文
手动挡
我们先来手动实现一下事务管理吧!
1.编写工具方法类MyTransactionalUtil
便于处理事务:
package com.guqueyue.myTransactional.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
/**
* @Author: guqueyue
* @Description: 自定义事务工具类
* @Date: 2023/12/28
**/
@Component
public class MyTransactionalUtil {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
/**
* @Description 开启事务
* @Param []
* @return org.springframework.transaction.TransactionStatus
**/
public TransactionStatus begin() {
return dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
}
/**
* @Description 提交事务
* @Param [transactionStatus]
* @return void
**/
public void commit(TransactionStatus transactionStatus) {
if (transactionStatus != null) {
dataSourceTransactionManager.commit(transactionStatus);
}
}
/**
* @Description 回滚事务
* @Param [transactionStatus]
* @return void
**/
public void rollback(TransactionStatus transactionStatus) {
if (transactionStatus != null) {
dataSourceTransactionManager.rollback(transactionStatus);
}
}
}
2.这样我们就可以直接在service实现类中注入MyTransactionalUtil
来实现事务管理啦
package com.guqueyue.myTransactional.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guqueyue.myTransactional.dao.UserMapper;
import com.guqueyue.myTransactional.entity.User;
import com.guqueyue.myTransactional.service.IUserService;
import com.guqueyue.myTransactional.util.MyTransactionalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import javax.annotation.Resource;
/**
* @Author: guqueyue
* @Description: 用户实现类
* @Date: 2023/12/19
**/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private MyTransactionalUtil myTransactionalUtil;
@Resource
private UserMapper userMapper;
@Override
public Integer insertUser(User user) {
int result = 0;
TransactionStatus begin = null;
try {
// 开启事务
begin = myTransactionalUtil.begin();
result = userMapper.insert(user);
// 写一个异常
int i = 1/0;
// 提交事务
myTransactionalUtil.commit(begin);
}catch (Exception e) {
e.printStackTrace();
// 回滚事务
myTransactionalUtil.rollback(begin);
// TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 手动回滚
return 0;
}
return result;
}
}
重新启动项目,浏览器输入:http://localhost:8082/user/insertUser?username=李四&password=7777777
同样,观察控制台,我们可以看到SQL语句执行以及异常抛出:
但是刷新并观察数据库,我们并没有发现数据插入:
说明,我们的事务管理起了作用。
那么,看到这里,你可能发现并没有用到我文章开头说的自定义注解、AOP思想呀
我知道你很急,但你先别急,在下面 ↓↓↓
自动挡
在上一章节中,我们实现了事务管理,但是非常的麻烦,每一个方法都得如法炮制一遍!
但是,我们可以很明显的发现,其实这些代码都是一样的,那么有没有简便方法呢?
当然有!不过这个世间是平衡的,这个地方少了,那么其他地方就得多。
1.首先,我们创建一个自定义注解:
package com.guqueyue.myTransactional.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author: guqueyue
* @Description: 自定义注解实现事务
* @Date: 2023/12/28
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTransactional {
}
关于自定义注解不懂的可以看我的这篇博客:框架的灵魂之注解基础篇
2.上AOP
package com.guqueyue.myTransactional.aop;
import com.guqueyue.myTransactional.util.MyTransactionalUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
/**
* @Author: guaueyue
* @Description: 通过拦截自定义注解实现事务
* @Date: 2023/12/28
**/
@Slf4j
@Aspect
@Component
public class MyTransactionAspect {
@Autowired
private MyTransactionalUtil transactionalUtil;
/**
* @Description 通过拦截自定义注解@MyTransactional来实现事务
* @annotation()里为自定义注解的路径
* @Param [joinPoint]
* @return java.lang.Object
**/
@Around(value = "@annotation(com.guqueyue.myTransactional.annotation.MyTransactional)")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
TransactionStatus begin = null;
try {
// 开启事务
begin = transactionalUtil.begin();
// 执行目标方法
result = joinPoint.proceed();
// 提交事务
transactionalUtil.commit(begin);
} catch (Throwable e) {
transactionalUtil.rollback(begin);
log.info("事务回滚...");
e.printStackTrace();
}
return result;
}
}
这里的joinPoint.proceed()
方法就是执行需要事务管理方法的意思,
我们可以很明显的看出跟上文手动管理事务的逻辑是一样的,只不过抽象出来了
// 执行目标方法
Object proceed = joinPoint.proceed();
3.使用
@MyTransactional
@Override
public Integer insertUser(User user) {
int result = userMapper.insert(user);
// 写一个异常
int i = 1/0;
return result;
}
这下我们只需要在需要事务管理的方法上面加一个自定义注解就可以实现功能了,是不是很方便优雅?
4.效果
同样的,重新启动项目,浏览器输入:http://localhost:8082/user/insertUser?username=王五&password=888888
同样,观察控制台,我们可以看到SQL语句执行以及异常抛出:
但是刷新数据库,发现什么也没有发生!
说明功能实现啦!
4.原理
那么,这个功能是怎么实现的呢?
其实很简单,Spring会拦截使用了自定义注解@MyTransactional
的方法,进行环绕增强。
这样方法就会变成类似于下面的效果(以下是伪代码无法执行)
@Autowired
private MyTransactionalUtil transactionalUtil;
/**
* @Description 通过拦截自定义注解@MyTransactional来实现事务
* @Param [joinPoint]
* @return java.lang.Object
**/
@Around(value = "@annotation(com.guqueyue.myTransactional.annotation.MyTransactional)")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
TransactionStatus begin = null;
try {
// 开启事务
begin = transactionalUtil.begin();
// 执行目标方法
@Override
public int insertUser(User user) {
int result = userMapper.insert(user);
// 写一个异常
int i = 1/0;
return result;
}
// 提交事务
transactionalUtil.commit(begin);
} catch (Throwable e) {
transactionalUtil.rollback(begin);
log.info("事务回滚...");
e.printStackTrace();
}
return result;
}
(以上是伪代码无法执行)
5.后话
当然了,这个功能其实Spring框架已经实现了,大家用官方提供的@Transactional
注解就好了,这里也只是给大家讲解一下原理:
@Transactional
@Override
public Integer insertUser(User user) {
int result = userMapper.insert(user);
// 写一个异常
int i = 1/0;
return result;
}
文章篇幅有限,就不具体验证截图示意了。
事务失效的问题
大家难免在编写代码的过程中遇到又需要事务管理,又需要异常处理的问题,如代码里面使用了文件流
然后有些新手小伙伴可能代码就变成了:
@Transactional
@Override
public Integer insertUser(User user) {
int result = 0;
try {
result = userMapper.insert(user);
// 写一个异常
FileInputStream fileInputStream = new FileInputStream("");
}catch (Exception e) {
e.printStackTrace();
}
return result;
}
重启项目,浏览器输入:http://localhost:8082/user/insertUser?username=赵六&password=999999
控制台可以看到SQL语句执行以及异常抛出:
但是刷新查看数据库发现插入了一条数据,说明事务失效了:
这个是为什么呢?
这里我就不卖关子了,因为如果你这样写的话,就成类似于这样了:
(以下是伪代码无法执行)
@Autowired
private MyTransactionalUtil transactionalUtil;
/**
* @Description 通过拦截自定义注解@MyTransactional来实现事务
* @Param [joinPoint]
* @return java.lang.Object
**/
@Around(value = "@annotation(com.guqueyue.myTransactional.annotation.MyTransactional)")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
TransactionStatus begin = null;
try {
// 开启事务
begin = transactionalUtil.begin();
// 执行目标方法
@Override
public int insertUser(User user) {
int result = 0;
try {
result = userMapper.insert(user);
// 写一个异常
FileInputStream fileInputStream = new FileInputStream("");
}catch (Exception e) {
e.printStackTrace();
}
return result;
}
// 提交事务
transactionalUtil.commit(begin);
} catch (Throwable e) {
transactionalUtil.rollback(begin);
log.info("事务回滚...");
e.printStackTrace();
}
return result;
}
(以上是伪代码无法执行)
我们可以很轻松的发现,方法内部有一个 try…catch ,所以外部的 try…catch 就失效了。
那么,怎么办呢?很简单,向外部抛出异常就好了,记得一直抛到控制层,不然代码会报错哦:
@Transactional
@Override
public Integer insertUser(User user) throws Exception{
int result = userMapper.insert(user);
// 写一个异常
FileInputStream fileInputStream = new FileInputStream("");
return result;
}
至于效果,留有读者自行验证了。
其实不用验证了,上面代码肯定是无效的啦,
因为Spring
的默认的事务规则是遇到运行异常(RuntimeException
)和程序错误(Error
)才会回滚。
而上文中的java.io.FileNotFoundException
异常属于IO异常(IOException
)
我们需要指定一下异常类型:
@Transactional(rollbackFor = Exception.class)
@Override
public Integer insertUser(User user) throws Exception {
int result = userMapper.insert(user);
// 写一个异常
FileInputStream fileInputStream = new FileInputStream("");
return result;
}
代码地址
本文代码已开源:
git clone https://gitee.com/guqueyue/my-blog-demo.git
请切换到gitee
分支,然后查看myTransactional
模块即可!
这戛然而止的结尾。