5.1 Spring 事务概述
事务(Transaction)是基于关系型数据库(RDBMS)的企业应用的重要组成部分。在软件开发领域,事务扮演者十分重要的角色,用来确保应用程序数据的完整性和一致性。
事务具有 4 个特性:原子性、一致性、隔离性和持久性,简称为 ACID 特性。
- 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的动作要么都做要么都不做。
- 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态,一致性和原子性是密切相关的。
- 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
- 持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。
事务允许我们将几个或一组操作组合成一个要么全部成功、要么全部失败的工作单元。如果事务中的所有的操作都执行成功,那自然万事大吉。但如果事务中的任何一个操作失败,那么事务中所有的操作都会被回滚,已经执行成功操作也会被完全清除干净,就好像什么事都没有发生一样。
5.1.1 事务管理方式
Spring 支持以下 2 种事务管理方式:
事务管理方式 | 说明 |
---|---|
编程式事务管理 | 编程式事务管理是通过编写代码实现的事务管理。 这种方式能够在代码中精确地定义事务的边界,我们可以根据需求规定事务从哪里开始,到哪里结束。 |
声明式事务管理 | Spring 声明式事务管理在底层采用了 AOP 技术,其最大的优点在于无须通过编程的方式管理事务,只需要在配置文件中进行相关的规则声明,就可以将事务规则应用到业务逻辑中。 |
选择编程式事务还是声明式事务,很大程度上就是在控制权细粒度和易用性之间进行权衡。
- 编程式对事物控制的细粒度更高,我们能够精确的控制事务的边界,事务的开始和结束完全取决于我们的需求,但这种方式存在一个致命的缺点,那就是事务规则与业务代码耦合度高,难以维护,因此我们很少使用这种方式对事务进行管理。
- 声明式事务易用性更高,对业务代码没有侵入性,耦合度低,易于维护,因此这种方式也是我们最常用的事务管理方式。
5.1.2 事务管理器
Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。
在 Spring 中提供了一个 org.springframework.transaction.PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下:
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;}
该接口中各方法说明如下:
名称 | 说明 |
---|---|
TransactionStatus getTransaction(TransactionDefinition definition) | 用于获取事务的状态信息 |
void commit(TransactionStatus status) | 用于提交事务 |
void rollback(TransactionStatus status) | 用于回滚事务 |
Spring 为不同的持久化框架或平台(例如 JDBC、Hibernate、JPA 以及 JTA 等)提供了不同的 PlatformTransactionManager 接口实现,这些实现类被称为事务管理器实现。
实现类 | 说明 |
---|---|
org.springframework.jdbc.datasource.DataSourceTransactionManager | 使用 Spring JDBC 或 iBatis 进行持久化数据时使用。 |
org.springframework.orm.hibernate3.HibernateTransactionManager | 使用 Hibernate 3.0 及以上版本进行持久化数据时使用。 |
org.springframework.orm.jpa.JpaTransactionManager | 使用 JPA 进行持久化时使用。 |
org.springframework.jdo.JdoTransactionManager | 当持久化机制是 Jdo 时使用。 |
org.springframework.transaction.jta.JtaTransactionManager | 使用 JTA 来实现事务管理,在一个事务跨越多个不同的资源(即分布式事务)使用该实现。 |
这些事务管理器的使用方式十分简单,我们只要根据持久化框架(或平台)选用相应的事务管理器实现,即可实现对事物的管理,而不必关心实际事务实现到底是什么。
5.1.3 事务的隔离级别
Spring 中提供了以下隔离级别,我们可以根据自身的需求自行选择合适的隔离级别。
方法 | 说明 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改,可能导致脏读、幻读和不可重复读 |
ISOLATION_READ_COMMITTED | Oracle 默认级别,允许读取已提交的并发事务,防止脏读,可能出现幻读和不可重复读 |
ISOLATION_REPEATABLE_READ | MySQL 默认级别,多次读取相同字段的结果是一致的,防止脏读和不可重复读,可能出现幻读 |
ISOLATION_SERIALIZABLE | 完全服从 ACID 的隔离级别,防止脏读、不可重复读和幻读 |
5.1.4 事务的传播行为
事务传播行为(propagation behavior)指的是,当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。例如,事务方法 A 在调用事务方法 B 时,B 方法是继续在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。
事务方法指的是能让数据库表数据发生改变的方法,例如新增数据、删除数据、修改数据的方法
Spring 提供了以下 7 种不同的事务传播行为。
名称 | 说明 |
---|---|
PROPAGATION_MANDATORY | 支持当前事务,如果不存在当前事务,则引发异常。 |
PROPAGATION_NESTED | 如果当前事务存在,则在嵌套事务中执行。 |
PROPAGATION_NEVER | 不支持当前事务,如果当前事务存在,则引发异常。 |
PROPAGATION_NOT_SUPPORTED | 不支持当前事务,始终以非事务方式执行。 |
PROPAGATION_REQUIRED | 默认传播行为,如果存在当前事务,则当前方法就在当前事务中运行,如果不存在,则创建一个新的事务,并在这个新建的事务中运行。 |
PROPAGATION_REQUIRES_NEW | 创建新事务,如果已经存在事务则暂停当前事务。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果不存在事务,则以非事务方式执行。 |
5.2 Spring 基于 XML 实现事务管理
5.2.1 Spring 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=UTF-8" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
</bean>
<!--jdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入dataSource对象-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--开启组件扫描-->
<context:component-scan base-package="top.shiyiri.spring5" />
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--配置通知-->
<tx:advice id="txadvice">
<!--指定哪种规则的方法上添加事务-->
<tx:attributes>
<tx:method name="accountMoney" propagation="REQUIRED" isolation="REPEATABLE_READ"/>
<!-- <tx:method name="account*"/>-->
</tx:attributes>
</tx:advice>
<!--配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pt" expression="execution(* top.shiyiri.spring5.service.UserService.accountMoney(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt" />
</aop:config>
</beans>
5.2.2 Service 层
package top.shiyiri.spring5.service;
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 top.shiyiri.spring5.dao.UserDao;
@Service
public class UserService {
@Autowired
private UserDao userDao;
//转账的方法
public void accountMoney() {
// try {
//第一步,开启事务
//第二部,进行业务操作
//Lucy少100
userDao.reduceMoney();
//模拟异常
int i = 1/0;
//Mary多一百
userDao.addMoney();
//第三步,没有发生异常,提交事务
// } catch (Exception e) {
// //出现异常,事务回滚
// }
}
}
5.3 Spring 基于注解实现事务管理
5.3.1 在 Spring 配置文件配置事务管理器
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=UTF-8" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
</bean>
<!--jdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入dataSource对象-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--开启组件扫描-->
<context:component-scan base-package="top.shiyiri.spring5" />
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
5.3.2 在 Spring配置文件开启事务注解
-
在 spring 配置文件中引入名称空间 tx
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd ">
-
开启事务注解
<!--开启事务注解--> <tx:annotation-driven transaction-manager="transactionManager" />
5.3.3 在 Service 类上添加事务注解
- @Transactional,这个注解可以添加到类上面,也可以添加到方法上面
- 如果把这个注解添加类上面,这个类里面所有的方法都添加事务
- 如果把这个注解添加方法上面,为这个方法添加事务
5.3.3.1 声明式事务管理参数配置
- propagation:事务传播行为【见 5.1.4】
- ioslation:事务隔离级别【见 5.1.3]】
- timeout:超时时间
- 事务需要在一定时间内进行提交,如果不提交进行回滚
- 默认值式 -1,设置时间以秒为单位进行计算
- readOnly:是否只读
- 读:查询操作。写:添加修改删除操作
- readOnly 默认值 false,表示可以增删改查
- readOnly 设置为 true,只能查询
- rollbackFor:回滚。设置出现哪些异常进行事务回滚
- noRollBackFor:不回滚。设置出现哪些异常不进行事务回滚
5.3.4 Dao 层创建
package top.shiyiri.spring5.dao;
public interface UserDao {
void addMoney();
void reduceMoney();
}
package top.shiyiri.spring5.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void addMoney() {
String sql = "update t_account set money=money+? where username=?";
jdbcTemplate.update(sql, 100, "mary");
}
@Override
public void reduceMoney() {
String sql = "update t_account set money=money-? where username=?";
jdbcTemplate.update(sql, 100, "lucy");
}
}
5.3.5 Service 层创建
package top.shiyiri.spring5.service;
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 top.shiyiri.spring5.dao.UserDao;
//noRollbackFor = {ArithmeticException.class},
@Transactional(readOnly = false, timeout = -1, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
@Service
public class UserService {
@Autowired
private UserDao userDao;
//转账的方法
public void accountMoney() {
// try {
//第一步,开启事务
//第二部,进行业务操作
//Lucy少100
userDao.reduceMoney();
//模拟异常
int i = 1/0;
//Mary多一百
userDao.addMoney();
//第三步,没有发生异常,提交事务
// } catch (Exception e) {
// //出现异常,事务回滚
// }
}
}
5.3.6 测试类
@Test
public void testAccount() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.accountMoney();
}
5.3.7 TxConfig
创建配置类,使用配置类替代 xml 配置文件
package top.shiyiri.spring5.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackages = {"top.shiyiri.spring5"})
@EnableTransactionManagement //开启事务
public class TxConfig {
//创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
//创建JdbcTemplate对象
@Bean
public JdbcTemplate getJdbcTemplate(){
//到IoC容器中根据类型找到dataSource
JdbcTemplate jdbcTemplate = new JdbcTemplate();
//注入dataSource
jdbcTemplate.setDataSource(getDruidDataSource());
return jdbcTemplate;
}
//创建事务管理器对象
@Bean
public DataSourceTransactionManager getTransactionManager(){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(getDruidDataSource());
return transactionManager;
}
}
5.3.8 测试类
@Test
public void testAccount2() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.accountMoney();
}
6.1 Spring 整合日志框架
整个 Spring5 框架的代码基于 Java8,运行时兼容 JDK9,许多不建议使用的类和方法在代码库中删除
Spring5 整合了 log4j2
-
第一步,引入 log4j2 相关 jar 包
-
第二步,创建 log4j2.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?> <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--Configuration后面的status用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,可以看到log4j2内部各种详细输出--> <configuration status="INFO"> <!--先定义所有的appender--> <appenders> <!--输出日志信息到控制台--> <console name="Console" target="SYSTEM_OUT"> <!--控制日志输出的格式--> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </console> </appenders> <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效--> <!--root:用于指定项目的根日志,如果没有单独指定Logger,则会使用root作为默认的日志输出--> <loggers> <root level="info"> <appender-ref ref="Console"/> </root> </loggers> </configuration>
-
测试
package top.shiyiri.spring5.test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Aunean * @date 2022/2/18 20:58 */ //自定义日志 public class UserLog { private static Logger logger = LoggerFactory.getLogger(UserLog.class); public static void main(String[] args) { logger.info("hello log4j2"); logger.warn("hello log4j2"); } }
6.2 Spring5 @Nullable 注解
- @Nullable 注解可以使用在方法上面,属性上面,参数上面,表示方法返回可以为空,属性值可以为空,参数值可以为空
- 注解用在方法上面,方法返回值可以为空
- 注解使用在方法参数里面,方法参数可以为空
- 注解使用在属性上面,属性值可以为空
6.3 Spring5 函数式风格编程
//Spring5特性,函数式风格创建对象,交给spring进行管理
@Test
public void testGenericApplicationContext() {
//1.创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2.调用context的方法对象进行注册
context.refresh();
context.registerBean(User.class, User::new);
//3.获取在Spring注册的对象,如果没指定名字,则需要使用全类名
User user = (User) context.getBean("top.shiyiri.spring5.test.User");
System.out.println(user);
//指定bean的名字
context.registerBean("user1", User.class, User::new);
Object user1 = context.getBean("user1");
System.out.println(user1);
}
/*输出:
top.shiyiri.spring5.test.User@614ddd49
top.shiyiri.spring5.test.User@694e1548
*/
6.4 Spring5 整合 JUnit5
package top.shiyiri.spring5.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import top.shiyiri.spring5.service.UserService;
/**
* Spring框架整合 JUnit4
*/
@RunWith(SpringJUnit4ClassRunner.class) //单元测试框架
@ContextConfiguration("classpath:bean1.xml") //加载配置文件
public class JTest4 {
@Autowired
private UserService userService;
@Test
public void test1() {
userService.accountMoney();
}
}
package top.shiyiri.spring5.test;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import top.shiyiri.spring5.service.UserService;
/**
* Spring框架整合 JUnit5
*/
//@ExtendWith(SpringExtension.class) //单元测试框架
//@ContextConfiguration("classpath:bean1.xml") //加载配置文件
//使用一个复合注解替代上面两个注解完成整合
@SpringJUnitConfig(locations="classpath:bean1.xml")
public class JTest5 {
@Autowired
private UserService userService;
@Test
public void test1() {
userService.accountMoney();
}
}
完整代码
https://gitee.com/aunean/spring5_txdemo1