Spring如何使用自定义注解来实现自动管理事务?

人可以做他(她)想做的,但不能要他(她)想要的

一个目录

  • 前言
  • 业务代码展示
  • 手动挡
  • 自动挡
  • 事务失效的问题
  • 代码地址

前言

在两年半以前,我写了一篇博客:框架的灵魂之注解基础篇:
在这里插入图片描述
在那篇博客的结尾,我埋了一个坑:
在这里插入图片描述
如今,我练习时长达两年半,终于摔锅归来!
在这里插入图片描述

本篇博客基于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模块即可!

这戛然而止的结尾。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/347103.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mac安装部署gitbook教程

mac安装部署gitbook教程 前言一、安装准备二、GitBook安装三、项目初始化 前言 一些自己实际操作的记录。 一、安装准备 Node.js gitbook基于Node.js&#xff0c;所以需要提前安装。 下载地址&#xff1a;https://nodejs.org/en/&#xff0c;可以下载比较新的版本。(但我的建议…

深入到 TLP:PCI Express 设备如何通信

前言 当我为PCI express编写Xillybus IP核时&#xff0c;我很快发现很难开始&#xff1a;在线资源和官方规格用关于螺母和螺栓的血腥细节轰炸你&#xff0c;但对机器应该做什么却很少说。因此&#xff0c;一旦我努力自己弄清楚这一点&#xff0c;我就决定写这个小指南&#xf…

源码篇--Redis 五种数据类型

文章目录 前言一、 字符串类型&#xff1a;1.1 字符串的编码格式&#xff1a;1.1.1 raw 编码格式:1.1.2 empstr编码格式:1.1.3 int 编码格式:1.1.4 字符串存储结构展示: 二、 list类型&#xff1a;2.1 List 底层数据支持&#xff1a;2.2 List 源码实现&#xff1a;2.3 List 结构…

【C++】反向迭代器模拟实现

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.利用适配器的思想…

【产品设计】B端导航菜单的三大模式

导航是每一个网站的灵魂所在&#xff0c;用户依赖导航进行不同页面的切换&#xff0c;找到自己所需的。那么该如何将庞大的B端系统中的导航菜单做好呢&#xff1f; 导航菜单是一个网站的灵魂&#xff0c;用户依赖导航在各个页面中进行跳转。 导航菜单一般分为顶部导航和侧边导…

成本更低、更可控,云原生可观测新计费模式正式上线

云布道师 在上云开始使用云产品过程中&#xff0c;企业一定遇见过两件“讨厌”事&#xff1a; 难以理解的复杂计费逻辑&#xff0c;时常冒出“这也能收费”的感叹&#xff1b; 某个配置参数调节之后&#xff0c;云产品使用成本不可预估的暴涨。 可观测作为企业 IT 运维必须品…

Python工具:pathlib

文件的路径实际上是一件很困扰的时间&#xff08;各种平台有时候规则不一样&#xff0c;有时候还需要考虑字符转义的问题&#xff09;&#xff0c;因此我直接推荐使用模块 pathlib&#xff0c;当然&#xff0c;如果您不介意的话&#xff0c;可以使用 os.path 做较为低级的路径操…

SpringCloudAlibaba系列之Nacos实战

目录 注意事项 参考资源 Nacos配置中心 初始化项目 进行Nacos相关配置 运行 Nacos注册中心 dubbo方式 对外暴露接口dubbo-api 服务提供者dubbo-provider 服务消费者dubbo-consumer 负载均衡客户端方法 服务提供者 服务消费者 注意事项 不管是使用Nacos配置中心&…

程序员手把手教你参与开源!拿捏!

一、前言 有一些同学提问&#xff0c;希望在自己的简历上增加一些有含金量的项目经历&#xff0c;最好能够去参与一些开源项目的开发&#xff0c;但由于对一个庞大的开源项目缺乏认知&#xff0c;难以着手。同时也担心自己能力不足&#xff0c;不知道自己写的代码是否会被接纳。…

MATLAB环境下一种音频降噪优化方法—基于时频正则化重叠群收缩

语音增强是语音信号处理领域中的一个重大分支&#xff0c;这一分支已经得到国内外学者的广泛研究。当今时代&#xff0c;随着近六十年来的不断发展&#xff0c;己经产生了许多有效的语音增强算法。根据语音增强过程中是否利用语音和噪声的先验信息&#xff0c;语音增强算法一般…

文件备份管理软件系统

1、我解决的问题 避免因为硬盘故障&#xff0c;导致数据丢失; 避免因为中了病毒&#xff0c;文件被加密&#xff0c;无法取回; 避免了员工恶意删除文件; 规范企业内部的文件管理&#xff0c;使它井井有条; 防范于未然&#xff0c;不必再为可能的风险担忧; 2、我的优点 我支持定…

语义分割 | 基于 VGG16 预训练网络和 Segnet 架构实现迁移学习

Hi&#xff0c;大家好&#xff0c;我是源于花海。本文主要使用数据标注工具 Labelme 对猫&#xff08;cat&#xff09;和狗&#xff08;dog&#xff09;这两种训练样本进行标注&#xff0c;使用预训练模型 VGG16 作为卷积基&#xff0c;并在其之上添加了全连接层。基于标注样本…

什么是调频直放站,调频直放站的功能和作用是什么?

调频直放站&#xff0c;顾名思义是一种对调频广播信号进行放大处理的通信设备&#xff0c;将调频广播信号引入到地下空间或隧道内&#xff0c;实现调频广播信号覆盖&#xff0c;扩大调频广播信号的覆盖范围。 1、调频直放站的组成 调频直放站从结构上来讲&#xff0c;一般由远…

线性代数:矩阵的定义

目录 一、定义 二、方阵 三、对角阵 四、单位阵 五、数量阵 六、行&#xff08;列&#xff09;矩阵 七、同型矩阵 八、矩阵相等 九、零矩阵 十、方阵的行列式 一、定义 二、方阵 三、对角阵 四、单位阵 五、数量阵 六、行&#xff08;列&#xff09;矩阵 七、同型矩…

python数据和分析——pandas基础内容

Pandas 的两个主要的数据结构是 Series 和 DataFrame&#xff1a; Series 是一维标记数组&#xff0c;类似于带有标签的列表。它可以包含不同类型的数据&#xff0c;并且可以通过索引进行访问和操作。DataFrame 是二维表格型数据结构&#xff0c;类似于 SQL 表或 Excel 电子表…

jQuery遍历(树遍历)

1、.children&#xff08;&#xff09;: 获得匹配元素集合中每个元素的直接子元素&#xff0c;选择器选择性筛选。 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><script src"jQuery.js"&g…

go api(get post传参,数据库,redis) 测试

介绍&#xff1a;分别测试get请求&#xff0c;post请求&#xff0c;请求链接数据库&#xff0c;以及redis操作。 1.api代码 package mainimport (_ "database/sql""encoding/json""github.com/gin-gonic/gin""go-test/com.zs/database&quo…

橘子学Mybatis08之Mybatis关于一级缓存的使用和适配器设计模式

前面我们说了mybatis的缓存设计体系&#xff0c;这里我们来正式看一下这玩意到底是咋个用法。 首先我们是知道的&#xff0c;Mybatis中存在两级缓存。分别是一级缓存(会话级)&#xff0c;和二级缓存(全局级)。 下面我们就来看看这两级缓存。 一、准备工作 1、准备数据库 在此之…

自动化网络故障管理

故障管理是网络管理的组成部分&#xff0c;涉及检测、隔离和解决问题&#xff0c;如果实施得当&#xff0c;网络故障管理可以使连接、应用程序和服务保持在最佳水平&#xff0c;提供容错能力并最大限度地减少停机时间&#xff0c;专门为此目的设计的平台或工具称为故障管理系统…

UDS Flash刷写用例简单介绍

文章目录 1.Boot的功能1.1 目的1.2 功能 2.测试用例设计2.1 设计框架2.2 正向测试2.1.1 刷写流程2.1.2 重复刷写2.1.3压力刷写 2.3 逆向测试2.2.1 断电后刷写2.2.2 中断通讯后刷写2.2.3 篡改刷写数据2.2.4 修改软件校验数据2.2.5 修改刷写流程2.2.6 高负载刷写2.2.7 高低压刷写…