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】
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、测试
7、总结
没有添加自定义事务注解前,转账会出现问题。
扣除了Tom账户的钱,但是Jerry的钱并没有增加,数据不一致。
添加了自定义注解【@MyTransactional】后,不会出现上述问题,保证了数据的一致性。
注意:Spring的事务,本质也是通过AOP实现的