Spring自定义事务注解

一、事务的作用

1、保证数据的一致性原则,遵循ACID
2、传统的事务mysql,通过行锁机制,当多个线程同时去操作同一行数据的时候,最后只有一个线程能够触发操作

二、事务分类

1、编程事务(手动挡)

通过代码实现begin commit rollback等操作

① 获取项目事务管理器DataSourceTransactionManger
②可以通过事务管理实现提交/回滚操作

2、声明事务(自动挡)

不用写代码,通过spring提供的注解,开启事务操作
只需要在方法上加上一个注解@Transaction

注意:@Transaction失效问题

三、Spring手动事务底层实现

声明事务的底层是基于传统的编程事务实现封装,所以认识编程事务非常重要。

1、环境准备

1.1)数据准备
create database if not exists spring_db character set utf8;
use spring_db;
create table if not exists tbl_account(
    id int primary key auto_increment,
    name varchar(20),
    money double
);
insert into tbl_account values(null,'Tom',1000);
insert into tbl_account values(null,'Jerry',1000);
1.2)项目准备
1.2.1)创建项目

【spring_aop_custom_tx_anno】

spring不加事务注解 spring的事务注解_spring

1.2.2)导包

pom.xml

<dependencies>
    <!--spring 核心包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!--spring jdbc依赖包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!--druid连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.16</version>
    </dependency>
    <!--mysql驱动包-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <!--mybatis依赖包-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>
    <!--mybatis整合spring依赖包-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.0</version>
    </dependency>
    <!--切入点表达式依赖,目的是找到切入点方法,也就是找到要增强的方法-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
</dependencies>
1.2.3)配置

编写配置类

  • SpringConfig.java
package com.alibaba.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.*;

import javax.sql.DataSource;
//声明当前类为配置类
@Configuration
//定义组件扫描路径
@ComponentScan("com.alibaba")
//加载外部属性文件
@PropertySource("classpath:jdbc.properties")
//开启spring AOP注解支持
@EnableAspectJAutoProxy
public class SpringConfig {

}

  • JdbcProperties.java
package com.alibaba.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
/**
 * @Author: zhuan
 * @Desc: 从属性文件jdbc.properties读取配置
 * @Date: 2022-04-27 19:47:15
 */
@PropertySource("classpath:jdbc.properties")
public class JdbcProperties {

		@Value("${jdbc.driver}")
		private String driver;
		@Value("${jdbc.url}")
		private String url;
		@Value("${jdbc.username}")
		private String username;
		@Value("${jdbc.password}")
		private String password;

		public String getDriver() {
				return driver;
		}

		public void setDriver(String driver) {
				this.driver = driver;
		}

		public String getUrl() {
				return url;
		}

		public void setUrl(String url) {
				this.url = url;
		}

		public String getUsername() {
				return username;
		}

		public void setUsername(String username) {
				this.username = username;
		}

		public String getPassword() {
				return password;
		}

		public void setPassword(String password) {
				this.password = password;
		}
}

  • MybatisConfig.java
package com.alibaba.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
//声明当前是配置类,@Configuration可以被@ComponentScan注解扫描到
@Configuration
//@Import注解,把JdbcProperties对象注入IOC容器
@Import(JdbcProperties.class)
public class MybatisConfig {

		/**
		 * 功能描述: 创建数据源,并注入IOC容器
		 * @param pros
		 * @return : javax.sql.DataSource
		 */
		@Bean
		public DataSource druidDataSource(JdbcProperties pros){
				System.out.println();
				DruidDataSource druidDataSource = new DruidDataSource();
				druidDataSource.setDriverClassName(pros.getDriver());
				druidDataSource.setUrl(pros.getUrl());
				druidDataSource.setUsername(pros.getUsername());
				druidDataSource.setPassword(pros.getPassword());
				return druidDataSource;
		}
		/**
		 * 功能描述: 创建数据源事务管理器,并注入IOC容器
		 * @param dataSource
		 * @return : org.springframework.jdbc.datasource.DataSourceTransactionManager
		 */
		@Bean
		public DataSourceTransactionManager transactionManager(DataSource dataSource){
				DataSourceTransactionManager dtm = new DataSourceTransactionManager();
				dtm.setDataSource(dataSource);
				return dtm;
		}
		/**
		 * 功能描述: 创建Mapper扫描配置对象,并注入IOC容器
		 * @return : org.mybatis.spring.mapper.MapperScannerConfigurer
		 */
		@Bean
		public MapperScannerConfigurer mapperScannerConfigurer(){
				MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer();
				scannerConfigurer.setBasePackage("com.alibaba.dao");
				return scannerConfigurer;
		}
		/**
		 * 功能描述: 创建SqlSessionFactoryBean对象,并注入IOC容器
		 * @param dataSource 
		 * @return : org.mybatis.spring.SqlSessionFactoryBean
		 */
		@Bean
		public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
				SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
				//指定数据源
				sqlSessionFactoryBean.setDataSource(dataSource);
				//指定实体类类型别名扫描包
				sqlSessionFactoryBean.setTypeAliasesPackage("com.alibaba.domain");
				return sqlSessionFactoryBean;
		}

}

  • jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
1.2.4)代码

实体类代码

  • Account.java
package com.alibaba.domain;

/**
 * @Author: zhuan
 * @Desc: 账户表---实体类
 * @Date: 2022-04-25 10:14:23
 */
public class Account {
		private Integer id;
		private String name;
		private Double money;

		public Integer getId() {
				return id;
		}
		public void setId(Integer id) {
				this.id = id;
		}
		public String getName() {
				return name;
		}
		public void setName(String name) {
				this.name = name;
		}
		public Double getMoney() {
				return money;
		}
		public void setMoney(Double money) {
				this.money = money;
		}
		@Override
		public String toString() {
				return "Account{" +
											 "id=" + id +
											 ", name='" + name + '\'' +
											 ", money=" + money +
											 '}';
		}
}

第一步:准备DAO层代码

  • AccountDao.java
package com.alibaba.dao;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
 * @Author: zhuan
 * @Desc: DAO层转账接口
 * @Date: 2022-04-27 19:49:04
 */
public interface AccountDao {

		@Update("update tbl_account set money = money + #{money} where name = #{name}")
		void inMoney(@Param("name") String name, @Param("money") Double money);

		@Update("update tbl_account set money = money - #{money} where name = #{name}")
		void outMoney(@Param("name") String name, @Param("money") Double money);
}

第二步:准备Service层接口、实现

  • AccountService.java
package com.alibaba.service;

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方,账号名称-name
     * @param in 转入方,账号名称-name
     * @param money 金额 Double
     */
    public void transfer(String out,String in ,Double money) ;
}
  • AccountServiceImpl.java
package com.alibaba.service.impl;

import com.alibaba.dao.AccountDao;
import com.alibaba.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    /**
     * 功能描述: 转账功能
     */
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }
}

第三步:准备事务管理工具

  • TransactionUtil.java
package com.alibaba.common;

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: zhuan
 * @Desc: 事务处理工具
 * @Date: 2022-04-27 19:53:29
 */
@Component
public class TransactionUtil {

    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    /**
     * 开启事务
     */
    public TransactionStatus begin(){
        //默认传播行为,不设置事务的隔离级别
        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
        return transaction;
    }

    /**
     * 提交事务
     */
    public void commit(TransactionStatus transactionStatus){
        dataSourceTransactionManager.commit(transactionStatus);
    }

    /**
     * 回滚事务
     */
    public void rollBack(TransactionStatus transactionStatus){
        dataSourceTransactionManager.rollback(transactionStatus);
    }
}

2、自定义注解

定义注解@MyTransactional

  • MyTransactional.java
package com.alibaba.annotation;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransactional {
    
}

3、Aop切面拦截

  • TransactionalAop.java
package com.alibaba.aop;

import com.alibaba.common.TransactionUtil;
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;

@Component
@Aspect
public class TransactionalAop {

    @Autowired
    private TransactionUtil transactionUtil;
    /**
     * 定义了Aop切面拦截我们的方法上是否有加上MyTransactional
     * */
    @Around(value = "@annotation(com.alibaba.annotation.MyTransactional)")
    public Object around(ProceedingJoinPoint joinPoint){
        TransactionStatus begin = null;
        try {
            //目标方法
            System.out.println(">>>开启事务");
            //1.开启事务
            begin = transactionUtil.begin();
            //2.执行目标增强方法(切入点)
            Object result = joinPoint.proceed();
            //3.1 没有异常则提交事务
            transactionUtil.commit(begin);
            System.out.println(">>>提交事务");
            return result;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            //3.2 报异常,回滚事务
            transactionUtil.rollBack(begin);
            System.out.println(">>>回滚事务");
            return "系统错误";
        }
    }
}

4、方法加上注解

package com.alibaba.service.impl;

import com.alibaba.annotation.MyTransactional;
import com.alibaba.dao.AccountDao;
import com.alibaba.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    /**
     * 功能描述: 转账功能----------------添加注解的方法---------------
     */
    @MyTransactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }
}

5、编写测试类

  • CustomTxAnnoTest.java
package com.alibaba;

import com.alibaba.aop.TransactionalAop;
import com.alibaba.config.SpringConfig;
import com.alibaba.service.AccountService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CustomTxAnnoTest {

    public static void main(String[] args) {
        //第一步:加载spring容器
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        //第二步:从spring容器中获取AccountService实例
        AccountService accountService = ctx.getBean(AccountService.class);
        //Tom 转给 Jerry 200
        accountService.transfer("Tom","Jerry",200D);
    }
}

6、测试

spring不加事务注解 spring的事务注解_其他_02

spring不加事务注解 spring的事务注解_mybatis_03

7、总结

没有添加自定义事务注解前,转账会出现问题。

扣除了Tom账户的钱,但是Jerry的钱并没有增加,数据不一致。

添加了自定义注解【@MyTransactional】后,不会出现上述问题,保证了数据的一致性。

注意:Spring的事务,本质也是通过AOP实现的