文章目录
1、Spring声明式事务
1.1 Spring声明式事务约定
对于声明式事务,使用注解@Transactional进行标注,告诉Spring在什么地方启用数据库事务即可。
注解@Transactional可以标注在类或方法上,当它标注在类上时,代表这个类所有公共非静态的方法都将启用事务功能。
在注解@Transactional中,还允许配置许多的属性,如事务的隔离级别、传播行为和异常类型,即确定方法发生什么异常下回滚事务或者发生什么异常下不回滚事务等。这些配置内容,是在Spring IoC容器在加载时就会将这些配置信息解析出来,然后把这些信息存到事务定义器(TransactionDefinition接口的实现类)里,并且记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务。
在整个过程中,我们所需要做的只是给需要事务的类或方法标注注解@Transactional和配置其属性而已。
1.2 注解@Transactional配置项
注解@Transactional配置项包括:
- value
- transactionManager
- timeout
- readOnly
- rollbackFor
- rollbackForClassName
- noRollbackFor
- noRollbackForClassName
- propagation
- isolation
关于注解@Transactional配置项的详细讲解,请参考系列博客《Spring使用篇系列博客传送门》中的第十一篇文章《Spring使用篇(十一)—— Spring与MyBatis事务管理》中的4.2小节内容。
1.3 Spring事务管理器
在上述的事务流程中,事务的打开、回滚和提交都是由事务管理器来完成的。在Spring中,事务管理器的顶层接口为PlatformTransactionManager。
在Spring Boot与MyBatis进行整合后,在Spring数据库事务方面,事务管理器最常用的是DataSourceTransactionManager,它是一个实现了接口PlatformTransactionManager的类。
PlatformTransactionManager接口的源码如下所示:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.transaction;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
public interface PlatformTransactionManager {
//获取事务,它还会设置数据属性
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
Spring在事务管理时,就是将这些方法按照约定织入对应的流程中,其中getTransaction方法的参数是一个事务定义器(TransactionDefinition),它是依赖于我们配置的注解@Transactional的配置项生成的,于是通过它就能够设置事务的属性了,而提交和回滚事务也可以通过commit和rollback方法来执行。
在Spring Boot中,当你依赖于mybatis-spring-boot-starter之后,它会自动创建一个DataSourceTransactionManager对象作为事务管理器。如果依赖于spring-boot-starter-data-jpa,则它会自动创建JpaTransactionManager对象作为事务管理器,所以我们一般不需要自己创建事务管理器而是直接使用它们即可。
1.4 在Spring Boot中使用事务
该演示项目让然使用上一篇博客《Spring Boot 2.x使用篇(二)—— 访问数据库》中的数据库。
第一步,在pom文件中添加项目相关依赖,具体如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-learning-demo</artifactId>
<groupId>com.ccff.springboot.demo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter6</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--引入第三方数据源DBCP依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<!--MyBatis相关依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>
第二步,创建POJO User类和性别枚举类型SexEnum,代码如下:
package com.ccff.springboot.demo.chapter6.pojo;
import com.ccff.springboot.demo.chapter6.enumration.SexEnum;
import org.apache.ibatis.type.Alias;
/**
* Created by wangzhefeng01 on 2019/8/7.
*/
@Alias(value = "user") //定义MyBatis的别名
public class User {
private Long id = null;
private String userName = null;
private SexEnum sex = null;
private String note = null;
public User() {
}
public User(Long id, String userName, SexEnum sex, String note) {
this.id = id;
this.userName = userName;
this.sex = sex;
this.note = note;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public SexEnum getSex() {
return sex;
}
public void setSex(SexEnum sex) {
this.sex = sex;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
package com.ccff.springboot.demo.chapter6.enumration;
/**
* Created by wangzhefeng01 on 2019/8/7.
*/
public enum SexEnum {
MALE(1,"男"),
FEMALE(2,"女");
private Integer id;
private String value;
SexEnum(Integer id, String value) {
this.id = id;
this.value = value;
}
public static SexEnum getSexEnumById(Integer id){
for (SexEnum sex : SexEnum.values()) {
if (sex.getId() == id){
return sex;
}
}
return null;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
第三步,创建MyBatis Mapper接口文件,具体代码如下:
package com.ccff.springboot.demo.chapter6.dao;
import com.ccff.springboot.demo.chapter6.pojo.User;
import org.springframework.stereotype.Repository;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
@Repository
public interface UserDao {
User getUser(Long id);
int insertUser(User user);
}
第四步,在com.ccff.springboot.demo.chapter6.mapper包下创建与该Mapper接口文件对应的Mapper映射XML文件userMapper.xml,具体如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.springboot.demo.chapter6.dao.UserDao">
<select id="getUser" parameterType="long" resultType="user">
select id, user_name as userName, sex, note from t_user where id = #{id}
</select>
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user(user_name,note) value(#{userName},#{note})
</insert>
</mapper>
第五步,创建IUserService接口与其实现类UserServiceImpl,具体如下:
package com.ccff.springboot.demo.chapter6.service;
import com.ccff.springboot.demo.chapter6.pojo.User;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
public interface IUserService {
User getUser(Long id);
int insertUser(User user);
}
package com.ccff.springboot.demo.chapter6.service.impl;
import com.ccff.springboot.demo.chapter6.dao.UserDao;
import com.ccff.springboot.demo.chapter6.pojo.User;
import com.ccff.springboot.demo.chapter6.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
@Service
public class UserServiceImpl implements IUserService {
@Autowired
UserDao userDao = null;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
public User getUser(Long id) {
return userDao.getUser(id);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
public int insertUser(User user) {
return userDao.insertUser(user);
}
}
在上面的代码中,在方法上标注了注解@Transactional,意味着这两个方法将启用Spring数据库事务机制。在事务配置中,采用了读写提交的隔离级别,限制了超时时间为一秒。
第六步,创建控制器用来进行测试,具体代码如下:
package com.ccff.springboot.demo.chapter6.controller;
import com.ccff.springboot.demo.chapter6.pojo.User;
import com.ccff.springboot.demo.chapter6.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
IUserService userService = null;
@GetMapping("/getUser")
public User getUser(Long id){
return userService.getUser(id);
}
@GetMapping("/insertUser")
public Map<String,Object> insertUser(String userName, String note){
User user = new User();
user.setUserName(userName);
user.setNote(note);
//结果会回填主键,返回插入条数
int affectedColnumNum = userService.insertUser(user);
Map<String,Object> result = new HashMap<>();
result.put("success",affectedColnumNum == 1);
result.put("user",user);
return result;
}
}
第七步,在application.properties配置文件中对MySQL数据库、第三方数据源DBCP、MyBatis以及日志信息进行相关配置。具体配置如下:
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter5
spring.datasource.username=root
spring.datasource.password=20161224cc
# =========================== 配置第三方DBCP2数据源 ===========================
# 指定数据库连接池类型
spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
# 最大等待连接中的数量,设0为没有限制
spring.datasource.dbcp2.max-idle=10
# 最大连接活动数
spring.datasource.dbcp2.max-total=50
# 最大等待毫秒数,单位为ms,超过时间会出错误信息
spring.datasource.dbcp2.max-wait-millis=10000
# 数据库连接池初始化连接数
spring.datasource.dbcp2.initial-size=5
# =========================== 配置第三方DBCP2数据源 ===========================
# =========================== 配置MyBatis ===========================
# MyBatis映射文件通配
mybatis.mapper-locations=classpath:com/ccff/springboot/demo/chapter6/mapper/*.xml
# MyBatis扫描别名包,和注解@Alias联用
mybatis.type-aliases-package=com.ccff.springboot.demo.chapter6.pojo
# 配置typeHandler的扫描包
mybatis.type-handlers-package=com.ccff.springboot.demo.chapter6.typehandler
# =========================== 配置MyBatis ===========================
# =========================== 配置日志 ===========================
logging.level.root=debug
logging.level.org.springframework=debug
logging.level..org.org.mybatis=debug
# =========================== 配置日志 ===========================
同时,修改Spring Boot的启动类Chapter6Application,为该类添加注解@MapperScan,用于通过扫描的方式装配Mapper接口,具体修改如下:
package com.ccff.springboot.demo.chapter6;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Repository;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
@SpringBootApplication
@MapperScan(basePackages="com.ccff.springboot.demo.chapter6.dao", annotationClass = Repository.class)
public class Chapter6Application {
public static void main(String[] args) {
SpringApplication.run(Chapter6Application.class, args);
}
}
启动Spring Boot的启动类Chapter6Application,在浏览器内输入链接得到如下结果,说明环境搭建成功。
测试插入用户信息方法结果如下:
2、事务的隔离级别
关于事务的隔离级别的详细讲解,请参考系列博客《Spring使用篇系列博客传送门》中的第十一篇文章《Spring使用篇(十一)—— Spring与MyBatis事务管理》中的3.1小节内容。
3、事务的传播行为
3.1 传播行为详解
关于事务的隔离级别的详细讲解,请参考系列博客《Spring使用篇系列博客传送门》中的第十一篇文章《Spring使用篇(十一)—— Spring与MyBatis事务管理》中的3.2小节内容。
3.2 在Spring Boot中测试传播行为
传播行为共分为7种,其中常用的传播行为是以下三种:
- REQUIRED
- REQUIRES_NEW
- NESTED
接下来在Spring Boot中使用事务的演示案例中测试上述三种常见的传播行为。
第一步,创建新的服务接口IUserBatchService及其实现类UserBatchServiceImpl,代码如下:
package com.ccff.springboot.demo.chapter6.service;
import com.ccff.springboot.demo.chapter6.pojo.User;
import java.util.List;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
public interface IUserBatchService {
public int insertUsers(List<User> userList);
}
package com.ccff.springboot.demo.chapter6.service.impl;
import com.ccff.springboot.demo.chapter6.pojo.User;
import com.ccff.springboot.demo.chapter6.service.IUserBatchService;
import com.ccff.springboot.demo.chapter6.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
@Service
public class UserBatchServiceImpl implements IUserBatchService {
@Autowired
IUserService userService = null;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int insertUsers(List<User> userList) {
int count = 0;
for (User user : userList){
//调用子方法,将使用注解@Transactional定义的传播行为
count += userService.insertUser(user);
}
return count;
}
}
在insertUsers方法中将调用IUserService中的insertUser方法,只是insertUser方法中没有定义传播行为,它会采用REQUIRED,也就是沿用当前的事务,所以它将与insertUsers方法使用同一个事务。
在UserController控制器类中添加insertUsers方法用于测试,具体代码如下:
package com.ccff.springboot.demo.chapter6.controller;
import com.ccff.springboot.demo.chapter6.pojo.User;
import com.ccff.springboot.demo.chapter6.service.IUserBatchService;
import com.ccff.springboot.demo.chapter6.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by wangzhefeng01 on 2019/8/8.
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
IUserService userService = null;
@Autowired
IUserBatchService userBatchService = null;
@GetMapping("/getUser")
public User getUser(Long id){
return userService.getUser(id);
}
@GetMapping("/insertUser")
public Map<String,Object> insertUser(String userName, String note){
User user = new User();
user.setUserName(userName);
user.setNote(note);
//结果会回填主键,返回插入条数
int affectedColnumNum = userService.insertUser(user);
Map<String,Object> result = new HashMap<>();
result.put("success",affectedColnumNum == 1);
result.put("user",user);
return result;
}
@GetMapping("/insertUsers")
public Map<String,Object> insertUsers(String userName1, String note1, String userName2, String note2){
User user1 = new User();
user1.setUserName(userName1);
user1.setNote(note1);
User user2 = new User();
user2.setUserName(userName2);
user2.setNote(note2);
List<User> userList = new ArrayList<>();
userList.add(user1);
userList.add(user2);
//结果会回填主键,返回插入条数
int affectedColnumNum = userBatchService.insertUsers(userList);
Map<String,Object> result = new HashMap<>();
result.put("success",affectedColnumNum > 0);
result.put("userList",userList);
return result;
}
}
在浏览器内输入相应地址后得到如下结果:
4、注解@Transactional自调用失效问题
Spring数据库事务的约定,其实现原理是AOP,而AOP的原理是动态代理,在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,这样Spring就不能把代码织入约定的流程中,于是就产生了注解@Transactional自调用失效问题。
解决注解@Transactional自调用失效问题,有如下两个思路:
- 用一个Service去调用另一个Service,这样就是代理对象的调用
- 可以从Spring IoC容器中获取代理对象去启用AOP
关于注解@Transactional自调用失效问题的详细讲解,请参考系列博客《Spring使用篇系列博客传送门》中的第十一篇文章《Spring使用篇(十一)—— Spring与MyBatis事务管理》中的6小节内容。