目录
一、JDBCTemplate (了解)
二、声明式事务概念
三、基于注解的声明式事物
一、准备工作
二、应用最基本的事务控制 @Transactional ★★★
三、事务属性:只读 readOnly = true ★
四、超时 timeout = ?
五、回滚异常类型 rollbackFor = Exception.class ★
六、事物隔离级别 ★
七、事物的传播行为
八、基于XML的声明式事务
四、Spring5 新特性 (了解)
1. JSR305标准相关注解
2. JSR 305
3. 相关注解
4. 整合junit5
五、Spring总结
@Transactional:对当前类所有方法、或当前方法应用事物
配置事物管理注解驱动:
//注意:要选tx命名空间的
要使用的事物管理器:DataSourceTransactionManager
为了在特定领域帮助我们简化代码,Spring 封装了很多 『Template』形式的模板类。例如:RedisTemplate、RestTemplate 等等,包括我们今天要学习的 JDBCTemplate。
事物离不开对数据库的访问,访问数据库可以使用MyBatis和JDBC。
Spring提供了整合JDBC的JDBCTemplate,因为先有Spring,再出现MyBatis,所以整合包不是由spring提供的,而是第三方提供的。
JDBCTemplate和MyBatis共同点:都是对JDBC进行了封装。不同点:MyBatis封装的更彻底,提供了更多的功能
1.添加依赖
org.springframework
spring-orm
5.3.1
Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包;导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入
2.进行配置
!
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis-example?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.user=root
jdbc.password=*******
3. 测试+具体操作
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class TestJDBCTemplate {
@Autowired
DataSource dataSource;
//新内容 jdbc模板
@Autowired
JdbcTemplate jdbcTemplate;
@Test
public void testJDBCTemplate() throws SQLException {
Connection conn = dataSource.getConnection();
System.out.println(conn);
System.out.println(jdbcTemplate);
}
@Test
public void testInsert(){
this.jdbcTemplate.update("insert into t_emp values(null,?,?)","张三",123.4);
}
@Test
public void testUpDate(){
this.jdbcTemplate.update("update t_emp set emp_salary = ? where emp_id =?",543.1,32);
}
@Test
public void testDelete(){
int n = this.jdbcTemplate.update("delete from t_emp where emp_id >= ?",27);
System.out.println(n);
}
@Test
public void testCount(){
Integer count = this.jdbcTemplate.queryForObject("select count(*) from t_emp", Integer.class);
System.out.println(count);
}
@Test
public void testFindById(){
//自动按照驼峰命名来映射 PropertyRowMapper
RowMapper rowMapper = new BeanPropertyRowMapper<>(Employee.class);
Employee emp = this.jdbcTemplate.queryForObject("select * from t_emp where emp_id =?",rowMapper,2);
System.out.println(emp);
}
@Test
public void testFindAll(){
RowMapper rowMapper = new BeanPropertyRowMapper<>(Employee.class);
List list = this.jdbcTemplate.query("select * from t_emp", rowMapper);
list.forEach(emp-> System.out.println(emp));
}
}
1、编程式事务:事务的相关操作全部通过编写代码来实现
缺点1:暴露了底层细节,没有进行封装,比较繁琐。
缺点2:代码复用性不高:需要事务的位置都要写相同的代码,代码冗余,不利于后期维护。
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
2、声明式事务:不编码,只是声明 (xml声明或者注解声明)
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
优点1:提高开发效率。
优点2:消除了冗余的代码,方便管理。
优点3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化。
使用JDBCTemplate 要用的事务管理器是:org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 Mybatis 用的也是这个类。
上级接口:PlatformTransactionManager
最上级:TransactionManager 空接口
1. 添加依赖
和JdbcTemplate相同的依赖:必须有spring-orm。
org.springframework
spring-context
5.3.1
org.springframework
spring-orm
5.3.1
org.springframework
spring-test
5.3.1
junit
junit
4.12
test
mysql
mysql-connector-java
5.1.3
com.alibaba
druid
1.0.31
2. 配置文件
和JdbcTemplate相同的配置
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis-example?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.user=root
jdbc.password=*******
3. DAO层代码
EmpDao
@Repository //声明当前类为Dao层
public class EmpDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void updateEmpNameById(Integer empId, String empName) {
String sql = "update t_emp set emp_name=? where emp_id=?";
jdbcTemplate.update(sql, empName, empId);
}
public void updateEmpSalaryById(Integer empId, Double salary) {
String sql = "update t_emp set emp_salary=? where emp_id=?";
jdbcTemplate.update(sql, salary, empId);
}
public String selectEmpNameById(Integer empId) {
String sql = "select emp_name from t_emp where emp_id=?";
String empName = jdbcTemplate.queryForObject(sql, String.class, empId);
return empName;
}
}
4. 业务代码
EmpService
@Service
public class EmpService {
@Autowired
private EmpDao empDao;
// 为了便于核对数据库操作结果,不要修改同一条记录
public void updateTwice(
// 修改员工姓名的一组参数
Integer empId4EditName, String newName,
// 修改员工工资的一组参数
Integer empId4EditSalary, Double newSalary
) {
// 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
empDao.updateEmpNameById(empId4EditName, newName);
empDao.updateEmpSalaryById(empId4EditSalary, newSalary);
}
}
5. 测试代码
public class TestTransaction {
@Autowired
private EmpService empService;
@Test
public void testUpdate(){
//全部成功
//empService.updateTwice(1,"tomcat",2,600.0);
//失败
empService.updateTwice(1,"tom",2,6003453453.0);
}
}
出现了什么问题:部分修改成功,部分修改失败。这不允许出现
1. 配置事务管理器
2. 开启基于注解的声明式事务功能 transaction-manager
3. 在需要事务的方法上使用注解(事务加载业务层) @Transactional
@Service
@Transactional //当前类所有方法都应用事物
public class EmpService {
@Autowired
private EmpDao empDao;
//修改第一个员工的姓名,修改第二个员工的工资
// 为了便于核对数据库操作结果,不要修改同一条记录
@Transactional //当前方法应用事物
public void updateTwice(
// 修改员工姓名的一组参数
Integer empId1, String newName,
// 修改员工工资的一组参数
Integer empId2, Double newSalary
) {
// 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
empDao.updateEmpNameById(empId1, newName);
empDao.updateEmpSalaryById(empId2, newSalary);
}
}
4. 添加依赖、启用logback日志
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
Service
//readOnly = true 只读事物 只有查询可以设为只读,可以提高查询效率
@Transactional(readOnly = true)
public String getEmpName(Integer empId){
String empName = this.empDao.getEmpName(empId);
return empName;
}
Dao层
public String getEmpName(Integer empId) {
String name = jdbcTemplate.queryForObject("select emp_name from t_emp where emp_id = ?", String.class, empId);
return name;
}
如果不是查询:Queries leading to data modification are not allowed
问题:查询不使用事务, 还是使用只读事务??
应用场合:
如果执行单条sql查询语句,则没有必要启用事务,数据库默认支持SQL执行期间的读一致性;
如果需要执行多条sql查询语句来合成最终的统计结果,则有必要启用事物,可以利用事务隔离级别保证数据查询的一致性。
例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
@Transactional(//timeout = 5 操作不能超过五秒)
public void updateTwice(
// 修改员工姓名的一组参数
Integer empId1, String newName,
// 修改员工工资的一组参数
Integer empId2, Double newSalary
) {
// 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
empDao.updateEmpNameById(empId1, newName);
try {
Thread.sleep(6000); //等待6秒
} catch (InterruptedException e) {
e.printStackTrace();
}
empDao.updateEmpSalaryById(empId2, newSalary);
}
如果超时:爆出异常TransactionTimedOutException: Transaction timed out
注意Thread.sleep()的位置,如果此时Sql语句已经全部执行完毕,事务还是会提交。
默认只针对运行时异常回滚,编译时异常不回滚。
出现了检查异常,如果catch,继续执行,异常被无视,不会回滚;如果异常抛出,也会提交事务,造成数据不一致。
修改方案一:rollbackFor = Exception.class
所有异常都回滚,除了SQLException
@Transactional(rollbackFor=Exception.class,noRollbackFor = SQLException.class)
public void updateTwice(){
}
所有的异常都不回滚,除了SQLException
@Transactional(rollbackFor=SQLException.class,noRollbackFor = Exception.class)
public void updateTwice(){
}
修改方案二:异常链。底层出现了检查异常,进行try catch处理,并向上层抛出一个新的异常(一般是运行异常)。既可以传递异常到上层,还可以避免方法签名中使用throws。在Spring事务中还可以实现所有异常都回滚的效果。
@Transactional //当前方法应用事务
public void updateTwice(
Integer empId1, String newName,
Integer empId2, Double newSalary
){
empDao.updateEmpNameById(empId1, newName);
try {
InputStream is = new FileInputStream("d:/abc/cdf/ghi.txt");//出现了检查异常
} catch (FileNotFoundException e) {
e.printStackTrace();
//抛出一个运行时异常
throw new RuntimeException(e.getMessage());
}
empDao.updateEmpSalaryById(empId2, newSalary);
}
常见的编译时异常:
SQLException、.IOexception、FileNotFoundException、ClassNotFoundException...
常见的运行时异常:
ArrayIndexOutOfBoundsExceptio、NullPointerException、IllegaArguementException
脏读:一个事务读取了另一个事务未提交数据;
不可重复读:同一个事务中前后两次读取同一条记录不一样。因为被其他事务修改了并且提交了。(同一事务中查询的数据应保持一致性)
幻读:一个事务读取了另一个事务新增、删除的记录情况,记录数不一样,像是出现幻觉。
不可重复读的重点是修改,幻读重点是新增或删除
隔离级别 脏读 不可重复读 幻读 READ_UNCOMMITTED (读未提交) √ √ √ READ_COMMITTED (读提交) × √ √ REPEATABLE_READ (可重复读 锁当条记录) × × √ SERIALIZABLE (序列化) × × ×
Spring的隔离级别
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
如果选择DEFAULT,默认值,由底层数据库自动判断应该使用什么隔离级别。
对于互联网高并发项目来说,如采用隔离级别SERIALIZABLE,固然安全,担心性能会受到严重影响。此时一般将隔离级别降低,保证效率,再配合悲观锁、乐观锁等技术保证安全性。
事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持。Oracle 支持的 2 种事务隔离级别:READ_COMMITED(默认) , SERIALIZABLE。MySQL 支持 4种事务隔离级别,默认REPEATABLE READ。
注意:mysql中使用了MVCC多版本控制技术,在REPEATABLE READ(可重复读)这个级别也可以避免幻读,解决了效率与安全性间取舍的问题。
事务传播行为(propagation behavior)
事务传播行为是指,多个拥有事务的方法在嵌套调用时的事务控制方式。
例如:methodA方法调用methodB方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
@Transactional 注解通过 propagation 属性设置事务的传播行为。
名称 | 含义 |
---|---|
REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是默认值。 |
REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
NESTED | 如当前存在事务,则在嵌套事务内执行。如当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作 |
问题1:事物层在业务层,事物也是应用于业务层,业务层怎么会调用业务层?
① service 应用了通知
问题2:一个事物包含多个DML操作,要么都成功,要么什么都不做。可以改变吗?
1.如果都成功,或者都失败 REQUIRED
2.如果希望empservice修改员工成功了,但是logservice添加日志失败了,不希望日志失败影响员工修改, REQUIRES_NEW
1. 加入依赖:
相比于基于注解的声明式事务,基于 XML 的声明式事务需要一个额外的依赖:
org.springframework
spring-aspects
5.3.1
2、迁移代码
将上一个基于注解的 module 中的代码转移到新module。去掉 @Transactional 注解。
3、修改 Spring 配置文件
去掉 tx:annotation-driven 标签,然后加入下面的配置:
①JCP
JCP(Java Community Process) 是一个由SUN公司发起的,开放的国际组织。主要由Java开发者以及被授权者组成,负责Java技术规范维护,Java技术发展和更新。
JCP官网地址:The Java Community Process(SM) Program
②JSR
JSR 的全称是:Java Specification Request,意思是 Java 规范提案。谁向谁提案呢?任何人都可以向 JCP 提出新增一个标准化技术规范的正式请求。JSR已成为Java界的一个重要标准。登录 JCP 官网可以查看所有 JSR 标准。
JSR 305: Annotations for Software Defect Detection
This JSR will work to develop standard annotations (such as @NonNull) that can be applied to Java programs to assist tools that detect software defects.
主要功能:使用注解(例如@NonNull等等)协助开发者侦测软件缺陷。
Spring 从 5.0 版本开始支持了 JSR 305 规范中涉及到的相关注解。
package org.springframework.lang;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierNickname;
/**
* A common Spring annotation to declare that annotated elements cannot be {@code null}.
*
* Leverages JSR-305 meta-annotations to indicate nullability in Java to common
* tools with JSR-305 support and used by Kotlin to infer nullability of Spring API.
*
*
Should be used at parameter, return value, and field level. Method overrides should
* repeat parent {@code @NonNull} annotations unless they behave differently.
*
*
Use {@code @NonNullApi} (scope = parameters + return values) and/or {@code @NonNullFields}
* (scope = fields) to set the default behavior to non-nullable in order to avoid annotating
* your whole codebase with {@code @NonNull}.
*
* @author Sebastien Deleuze
* @author Juergen Hoeller
* @since 5.0
* @see NonNullApi
* @see NonNullFields
* @see Nullable
*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull
@TypeQualifierNickname
public @interface NonNull {
}
注解名称 | 含义 | 可标记位置 |
---|---|---|
@Nullable | 可以为空 | @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) |
@NonNull | 不应为空 | @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) |
@NonNullFields | 在特定包下的字段不应为空 | @Target(ElementType.PACKAGE) @TypeQualifierDefault(ElementType.FIELD) |
@NonNullApi | 参数和方法返回值不应为空 | @Target(ElementType.PACKAGE) @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) |
1.添加依赖
org.junit.jupiter
junit-jupiter-api
5.7.0
test
3.使用复合注解 进行测试 @SpringJUnitConfig
@SpringJUnitConfig(locations = "classpath:spring.xml")
//@ExtendWith(SpringExtension.class)
//@ContextConfiguration(locations = "classpath:spring.xml")
public class TestTransaction2 {
@Autowired
private EmpService empService;
@Test
public void testUpdate() {
//全部成功
empService.updateTwice(1,"tomcat",2,600.0);
//失败
//empService.updateTwice(1,"tom",2,50012423423423.0);
}
@Test
public void testGetEname(){
String empName = empService.getEmpName(1);
System.out.println(empName);
}
}
3.区别
特征 |
JUNIT 4 |
JUNIT 5 |
声明一种测试方法 |
@Test |
@Test |
在当前类中的所有测试方法之前执行 |
@BeforeClass |
@BeforeAll |
在当前类中的所有测试方法之后执行 |
@AfterClass |
@AfterAll |
在每个测试方法之前执行 |
@Before |
@BeforeEach |
每种测试方法后执行 |
@After |
@AfterEach |
禁用测试方法/类 |
@Ignore |
@Disabled |
测试工厂进行动态测试 |
NA |
@TestFactory |
嵌套测试 |
NA |
@Nested |
标记和过滤 |
@Category |
@Tag |
注册自定义扩展 |
NA |
@ExtendWith |