事务
事务概念
何为事务:
事务是数据库操作最基本的单元,指的是逻辑上的一组操作,要么都成功,如果其中有一点失败,那整体操作都失败。
典型场景:银行转账
事物的四大特性(ACID):
原子性:
- 执行过程中不可分割,要么都成、要么都失败
一致性:
- 操作之前和操作之后总量不会变化
隔离性:
- 在多事务操作时,它们之间不会产生影响(两个人同时操作同一条记录,他们之间不会产生影响)
持久性:
- 事务完成后提交到数据库
事务操作
事务案例操作(银行转账):
创建数据库表:
USE user_db;
CREATE TABLE t_account2(
id VARCHAR(20) PRIMARY KEY,
username VARCHAR(50),
money INT
);
创建 service,搭建 dao,完成对象创建和注入关系:
- 在 service中注入 dao,在 dao中注入 JdbcTemplate,
- 在 JdbcTemplate注入 DataSource
- 在 dao中创建两个方法,一个是加钱的方法,一个是减钱的方法,在 service中创建一个转账方法
- 以上逻辑,如果正常运行没有问题,但是如果代码执行过程出现异常,会出现问题
- 所以应该使用事务来解决代码出现异常的问题
事务操作_Spring事务管理介绍:
在开发中,事务一般添加到 JavaEE三层结构中的 Service层中(业务逻辑层)
在 Spring中进行事务管理操作,一般分为两种方式:
- 编程式事务管理(代码臃肿)
- 声明式事务管理(推荐)
声明式事务管理(两种):
- 基于注解方式(推荐)
- 基于 xml配置方式
在 Spring进行声明式事务管理,底层使用 AOP
- 在 Spring中,声明式事务管理用到的就是 AOP技术
Spring事务管理 API:
提供一个接口,代表事务管理器,此接口针对不同的框架提供不同的实现类
- PlatformTransactionManager:事务总接口
- DataSourceTransactionManager:jdbc、mybatis事务接口实现类
事务操作_声明式注解方式:
- 在 Spring配置文件中(xml)配置(创建)事务管理器
- 在 Spring配置文件中,开启事务注解(注解方式)
- 引入名称空间 tx,类似于 context(代码扫描)
- 开启事务注解 <tx:annotation-driven transaction-manager="">
- 在 Service类上面(或 Service类的方法上面)添加事务注解
@Transactional:此注解可以添加到类、方法上面- 添加在类上面,那类中所有的方法都添加了事务
- 添加到指定方法上面,那此方法会添加事务
银行转账案例_声明式注解完整代码:
- xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
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">
<!--
context: 开启组件扫描:
-->
<context:component-scan base-package="com.atjava.spring5_2"></context:component-scan>
<!--
添加数据库连接池:
-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<!--
<property name="url" value="jdbc:mysql:///user_db" />
<property name="username" value="root" />
<property name="password" value="root" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
-->
<property name="url" value="jdbc:mysql:///user_db"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!--
创建 jdbcTemplate对象:
-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建事务管理器:-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入被操作的数据库源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启事务注解:-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
</beans>
- service
package com.atjava.spring5_2.service;
import com.atjava.spring5_2.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lv
* @create 2022-01-12 23:38
*/
@Service(value = "accountService")
//@Transactional // 事务注解
public class AccountService {
// 注入属性:
@Autowired
@Qualifier(value = "accountDaoImpl")
private AccountDao accountDao;
// 用户转账操作:使用事务
@Transactional
public void turnAccount() {
// 事务操作流程:
// try {
// // 第一步:开启事务
// // 第二步:进行业务操作
// accountDao.reduce();
// // 模拟异常
// int n = 10/0;
// accountDao.add();
//
// // 第三步:无异常发生,提交事务
// } catch(Exception e) {
// // 第四步:出现异常,事务回滚
// }
// 事务:声明式注解方式
accountDao.reduce();
int n = 10 / 0;
accountDao.add();
}
}
- dao
package com.atjava.spring5_2.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
/**
* @author lv
* @create 2022-01-12 23:42
*/
@Repository(value = "accountDaoImpl")
public class AccountDaoImpl implements AccountDao {
@Autowired
@Qualifier(value = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public void reduce() {
Object[] obj = {100, "1"};
String sql = "update t_account set money=money-? where id=?";
int update = jdbcTemplate.update(sql, obj);
System.out.println("reduce: " + update);
}
@Override
public void add() {
Object[] obj = new Object[2];
obj[0] = 100;
obj[1] = "2";
String sql = "update t_account set money=money+? where id=?";
int update = jdbcTemplate.update(sql, obj);
System.out.println("add: " + update);
}
}
- test
@Test
public void testAccount() {
//PlatformTransactionManager事务接口
//DataSourceTransactionManager,jdbc、mybatis事务实现类
ApplicationContext context =
new ClassPathXmlApplicationContext("bean2.xml");
AccountService service = context.getBean("accountService", AccountService.class);
service.turnAccount();
}
事务操作_声明式事务管理参数配置:
在 service类上添加注解 @Transactional,在此注解里面可以配置事务相关参数
- 事务方法:对数据库表数据进行变化的操作
@Transactional(xxx) 配置事务相关参数:
-
propagation:事务传播行为
- 多事务方法(数据库的增删改方法)之间进行调用,这个过程中事务是如何进行管理的
- 事务传播行为是指:一个有事务的方法中调用一个无事务的方法,或一个无事务方法中调用一个有事务方法等,它们之间是如何处理的
- @Transactional(propagation=Propagational.REQUIRED)
-
isolation:事务隔离级别
- 事务有特性称为隔离性,多事务操作之间不会产生影响,不考虑隔离性会产生很多问题
- 三个读问题:脏读、不可重复读、虚(幻)读
- @Transactional(isolation = Isolation.READ_COMMITTED)
-
timeout:超时时间
- 事务需要在一定时间内进行提交,如果超过设置时间,事务会自动回滚
- 默认超时时间是 -1,单位是秒
- @Transactional(timeout = -1, isolation = Isolation.READ_COMMITTED)
-
readonly:是否只读
- 读:数据库查询操作;写:数据库增删改操作
- readonly:默认值 false,表示可以查询,可以增删改操作
- 设置值 true,只能查询
- @Transactional(readonly = true, timeout = -1)
-
rollbackFor:回滚
- 设置执行过程中出现指定异常后进行事务回滚
-
noRollbackFor:不回滚
- 设置执行过程中出现指定异常后不进行事务回滚
Spring 框架事务传播行为有七种:
| 传播属性 | 描述 |
|---|---|
| REQUIRED | 如果 add方法本身有事务,调用 update方法后,update使用当前 add方法里面的事务;如果 add方法本身无事务,调用 update方法后,创建新事务 |
| REQUIRED_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,因该将它挂起 |
| xxx | xxx |
事务隔离性产生的三个问题:
- 脏读:一个未提交事务读取到另一个未提交事务的数据
- 不可重复读:一个未提交事务读取到另一个提交事务修改的数据
- 虚(幻)读:一个未提交事务读取到另一个提交事务添加的数据
事务的隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 虚读 |
|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
事务操作_声明式 xml配置方式:
在 Spring配置文件中进行配置:
- 配置(创建)事务管理器
- 配置通知(实际增强的逻辑部分称为通知)<tx:advice />
- 配置切入点(类中实际被增强的方法)和切面(将通知应用到切入点的过程)<aop:config />
- xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
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">
<!--
context: 开启组件扫描:
-->
<context:component-scan base-package="com.atjava.spring5_2"></context:component-scan>
<!--
添加数据库连接池:
-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<!--
<property name="url" value="jdbc:mysql:///user_db" />
<property name="username" value="root" />
<property name="password" value="root" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
-->
<property name="url" value="jdbc:mysql:///user_db"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!--
创建 jdbcTemplate对象:
-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--1. 创建事务管理器:-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入被操作的数据库源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--2. 配置通知:-->
<tx:advice id="txadvice">
<!--配置事务参数:-->
<tx:attributes>
<!--指定那种规则的方法上面添加事务-->
<!--<tx:method name="turn*"/>-->
<tx:method name="turnAccount" isolation="REPEATABLE_READ" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--3. 配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut
id="pt"
expression="execution(* com.atjava.spring5_2.service.AccountService.*(..))"
></aop:pointcut>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"></aop:advisor>
</aop:config>
</beans>
- test
@Test
public void testAccountXml() {
ApplicationContext context =
new ClassPathXmlApplicationContext("bean3.xml");
AccountService service = context.getBean("accountService", AccountService.class);
service.turnAccount();
}
事务操作_完全注解声明式事务管理:
创建配置类,使用配置类完全替代 xml配置文件:
- 配置类注解 @Configuration
- 组件扫描 @ComponentScan(basePackages = "com.atjava.spring5_2")
- 创建数据库连接池 DruidDataSource
- 创建 JdbcTemplate模板对象 JdbcTemplate
- 创建事务管理器 DataSourceTransactionManager
- 开启事务 @EnableTransactionManagement
- config
package com.atjava.spring5_2.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;
import javax.sql.DataSource;
/**
* @author lv
* @create 2022-01-16 22:16
*/
// 1.配置类注解
@Configuration
// 2.组件扫描
@ComponentScan(basePackages = "com.atjava.spring5_2")
// 6.开启事务
@EnableTransactionManagement
public class TxConfig {
// 3.创建数据库连接池
@Bean // 创建 bean实例
public DruidDataSource getDruidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // 设置驱动名称
dataSource.setUrl("jdbc:mysql:///user_db"); // 数据库路径
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
// 4.创建 JdbcTemplate模板对象
/* (DataSource dataSource):表示在 IOC容器中已经存在 DataSource类型的 dataSource对象,
类似于 @Autowired根据类型注入。
*/
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// jdbcTemplate.setDataSource(this.getDruidDataSource());
// 注入 dataSource
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
// 5.创建事务管理器:
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
- test
- AnnotationConfigApplicationContext(configClass)注解类
@Test
public void testAccountAllAnnotation() {
ApplicationContext context =
new AnnotationConfigApplicationContext(TxConfig.class);
AccountService service = context.getBean("accountService", AccountService.class);
service.turnAccount();
}
Spring5 框架新功能
- 整个 Spring5 框架的代码基于 Java8
- 运行时兼容 JDK9
- 许多不建议使用的类和方法已在代码库中删除
核心特性:
- Spring5 框架自带了通用的日志封装(查看程序运行、排查问题)
- Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2
- 在 Spring5框架中整合 Log4j2
核心容器:
- 支持 @Nullable注解
- 函数式风格(Lambda表达式) GenericApplicationContext
- Spring WebMVC
测试方面的改进:
- Spring5支持整合 JUnit5
在 Spring5框架中整合 Log4j2
- 引入相关 jar包
- log4j-api-2.11.2.jar
- log4j-core-2.11.2.jar
- log4j-slf4j-impl-2.11.2.jar
- slf4j-api-1.7.30.jar
- 创建 log4j2.xml配置文件
- 测试:
package com.atjava.spring5_2.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author lv
* @create 2022-01-17 21:27
*/
public class UserLog {
private static final Logger log = LoggerFactory.getLogger(UserLog.class);
public static void main(String[] args) {
// 在控制台手动添加日志:
log.info("hello log4j2 info");
log.warn("hello log4j2 warn");
/**
* 2022-01-17 21:32:35.257 [main] INFO com.atjava.spring5_2.test.UserLog - hello log4j2 info
* 2022-01-17 21:32:35.261 [main] WARN com.atjava.spring5_2.test.UserLog - hello log4j2 warn
*/
}
}
Spring5框架支持 @Nullable注解
- @Nullable注解可以使用在
方法上、属性上、参数上 - 分别表示
方法返回值可以为空、属性值可以为空、参数值可以为空
// 方法:
@Nullable
ApplicationContext getParent();
// 属性:
@Nullable
private String userName;
// 参数:
public <T> void registerBean(@Nullable String beanName, @Nullable Supplier<T> supplier, ...) {
this.reader.registerBean(beanClass, beanName, supplier, customizers);
}
Spring5框架支持函数式风格(Lambda表达式)GenericApplicationContext
通过 Lambda表达式和 GenericApplicationContext类,将独自创建的对象交给 Spring管理。
- 创建 GenericApplicationContext对象
- 调用 context的方法进行对象注册
// 函数式风格创建对象,交给 Spring进行管理
@Test
public void testGenericApplicationContext() {
// 1.创建 GenericApplicationContext对象
GenericApplicationContext context =
new GenericApplicationContext();
// 2.调用 context的方法进行对象注册
context.refresh(); // 表示进行内容清空
// Lambda表达式:() -> new User()
// 不指定对象名称
// context.registerBean(User.class, () -> new User()); // 注册对象
// 指定对象名称 user1
context.registerBean("user1", User.class, () -> new User()); // 注册对象,指定名称
// 3.测试,获取在 Spring中注册的对象
// User user = context.getBean("com.atjava.spring5_2.bean.User"); // 类的全路径
User user = context.getBean("user1", User.class); // 指定对象名称
System.out.println(user);
// com.atjava.spring5_2.bean.User@57855c9a
}
Spring5支持整合 JUnit5
整合 JUnit4:
- 引入 Spring相关针对测试的依赖包:
- 引入 JUnit4的 jar包
- spring-test-5.2.6.RELEASE.jar
- 创建测试类,使用相关注解方式完成
- @RunWith(SpringJUnit4ClassRunner.class)指定整合的单元测试框架的版本
- @ContextConfiguration("classpath:bean2.xml")加载配置文件
package com.atjava.spring5_2.test;
import com.atjava.spring5_2.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author lv
* @create 2022-01-18 22:01
*/
// 指定整合的单元测试框架的版本
@RunWith(SpringJUnit4ClassRunner.class)
/**
* 加载配置文件:
* 类似于如下代码
* ApplicationContext context =
* new ClassPathXmlApplicationContext("bean3.xml");
*
*/
@ContextConfiguration("classpath:bean2.xml")
public class JTest4 {
// 因为已经加载了配置文件,可以直接注入
@Autowired
// @Qualifier(value = "accountService")
private AccountService accountService;
@Test
public void testAccountService() {
// 直接调用:
accountService.turnAccount();
}
}
整合 JUnit5:
- 引入 JUnit5的 jar包
- 创建测试类,使用注解完成
- @ExtendWith(SpringExtension.class)指定整合的单元测试框架的版本:JUnit5
- @ContextConfiguration("classpath:bean2.xml")添加配置文件:bean2.xml
package com.atjava.spring5_2.test;
import com.atjava.spring5_2.service.AccountService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author lv
* @create 2022-01-18 22:44
*/
// 指定整合的单元测试框架的版本:JUnit5
@ExtendWith(SpringExtension.class)
// 添加配置文件:bean2.xml
@ContextConfiguration("classpath:bean2.xml")
public class JTest5 {
@Autowired
private AccountService accountService;
@Test
public void testJUnit5() {
accountService.turnAccount();
}
}
复合注解:@SpringJUnitConfig(locations = "classpath:bean2.xml")- 表示 @ExtendWith和 @ContextConfiguration的组合
package com.atjava.spring5_2.test;
import com.atjava.spring5_2.service.AccountService;
import org.junit.jupiter.api.Test;
//import org.junit.jupiter.api.extension.ExtendWith;
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;
/**
* @author lv
* @create 2022-01-18 22:44
*/
// 指定整合的单元测试框架的版本:JUnit5
// @ExtendWith(SpringExtension.class)
// 添加配置文件:bean2.xml
// @ContextConfiguration("classpath:bean2.xml")
// 复合注解:
@SpringJUnitConfig(locations = "classpath:bean2.xml")
public class JTest5 {
@Autowired
private AccountService accountService;
@Test
public void testJUnit5() {
accountService.turnAccount();
}
}