Spring + SpringMVC + MyBatis

mybatis 基本已经学完了,现在我们来学习最后一个章节,即将 mybatis 与 spring 进行整合,也就是所谓的 SSM 三大框架。来重新回顾一下 SSM 三大框架,所谓 SSM 其实就是 Spring + SpringMVC + MyBatis,但其实我们也知道,SpringMVC 其实是 Spring 框架中一个 web 模块,所以本质上,应该称为 Spring + MyBatis 框架(或者也叫做 SpringMVC + MyBatis 框架),即 SM 框架,而 Spring 和 Spring MVC 我们在这之前已经系统的学习过了,MyBatis 的学习也接近尾声了,现在我们要做的就是将 MyBatis 与 Spring 整合在一起。

整合 Spring 框架

为什么要将 MyBatis 整合到 Spring 大家族中呢?还记得 Spring 的两大核心概念吗?IoC 和 AOP,IoC 即控制反转,也叫依赖注入,用来降低组件之间的耦合度,实现依赖的自动注入,而 AOP 则是面向切面编程,是 OOP 的一个延伸、补充,本质是动态代理的一个应用,我们可以利用 AOP 来做一些业务增强逻辑(业务类 + 增强类);IoC 中的 @Autowired 可以说是非常好用的,使用者根本不需要关心依赖对象是如何创建的,只要使用这个注解标注依赖对象,Spring 就会在运行时自动将依赖对象注入进来,我们直接用就行了,而我们整合 MyBatis 的原因也是如此,为了使用 Spring 提供的 IoC 容器。

先来回顾一下 SpringMVC 的配置,核心其实也就是一个 mvc.xml 配置文件,这个 mvc.xml 和 spring 学习中的 beans.xml、spring.xml 是一样的。这是在 springmvc 学习中用到的一个 springmvc + jdbctemplate 组合的 mvc.xml 配置文件,而我们的 springmvc + mybatis 其实和这个组合的作用是一样的,jdbctemplate 对标 mybatis,前者是轻量级的 jdbc 封装,后者是中量级的 jdbc 封装,而 hibernate 则是重量级的 jdbc 封装:

注意后面两个 bean,dataSource 就是配置的数据源,因为是给 jdbctemplate 用的,所以直接使用最简单的 datasource,即 driverManager 的包装,并没有数据库连接池功能,而 jdbcTemplate 则是一个简易的 orm 框架,我们会在 dao 层中使用 @Autowired 自动装配这个 bean。那么其实 springmvc + mybatis 的整合也是类似的,只不过多了一个 spring 事务管理器的 bean 配置,因为 jdbcTemplate 中的 curd 都比较简单,所以我就没有配置什么事务管理器。

而 ssm 框架整合,就是将 mybatis 的 mybatis-config.xml 全局配置文件全部转换为 spring 的 bean 配置,配置到 mvc.xml 文件中,让 spring 来管理,而 mybatis 我们知道,有这么几个核心概念:全局配置文件、mapper 接口、mapper 文件。而我们要做的就是将这个全局配置文件去掉(当然也不一定要去掉,也有人会保留它,但我选择去掉它,看起来干净一些,因为如果保留的话,mybatis 配置会东一点西一点,非常凌乱)。注意 mapper 接口和 mapper 文件我们是不会动它的,因为也不需要动;OK,来看看 mybatis-config.xml 配置文件的内容:

看似很多配置,其实就三个东西是必须要配置的,即 数据源事务管理器映射文件路径。mybatis 整合 spring 其实官方有提供适配包,我们要做的就是配置好 springmvc 环境,并且写好 mybatis 的 mapper 接口和 mapper 文件,然后导入 springmvc、mybatis、mybatis-spring 适配包,最后在 mvc.xml 中配置 mybatis-config.xml bean 就行了,这是官方提供的 spring 适配包项目地址:https://github.com/mybatis/spring,因为我们通常使用 maven 管理项目,所以直接去 https://mvnrepository.com 搜索 mybatis-spring 就行了,即在 pom.xml 中添加以下依赖:

数据源我们就使用最流行和最快速的 HikariCP(连接池)
具体可参考我之前的 Java DataSource 数据源 的最后一节。

Spring 事务管理相关的知识(编程式事务、声明式事务)
事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为 编程式声明式 的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染,因此在实际使用中声明式事务用的比较多。声明式事务有两种方式,一种是在配置文件(xml)中做相关的事务规则声明,另一种是基于 @Transactional 注解的方式。注释配置是目前流行的使用方式,因此本文将着重介绍基于 @Transactional 注解的事务管理。

Spring 支持两种类型的事务管理:

  • 编程式事务管理:这意味着您必须在编程的帮助下管理事务。这为您提供了极大的灵活性,但很难维护。
  • 声明式事务管理:这意味着您将事务管理与业务代码分开。您只能使用注解或基于 XML 的配置来管理事务。

声明式事务管理优于编程式事务管理,但它不如编程式事务管理灵活,编程式事务允许您通过 Java 代码控制事务。而声明式事务则是通过 Spring AOP 框架来将业务逻辑代码和事务管理代码分开,做到不同代码的解耦,对我们的业务代码侵入性非常低,看起来也很干净,可读性也很好,所以绝大多数情况下使用的都是声明式事务管理方式。

编程式事务
所谓编程式事务管理其实就是使用 Java 代码手动进行事务的提交或回滚,基本上和使用原生的 JDBC API 是一样的,只不过 Spring 的编程式事务管理提供了更多的功能,因为平时都很少使用编程式事务管理,所以我也就不实际操作了,直接将 tutorialspoint 的 example 代码拿过来,大家看看就行了:

Spring 的配置还是比较简单明了的,首先就是配置一个 dataSource 数据源,这里直接就使用 Spring JDBC 的 DriverManagerDataSource,本质只是 DriverManager 的封装,没有数据库连接池功能;然后就是定义 Spring 的事务管理器,我们会在业务代码中通过这个事务管理器对象来进行 commit 和 rollback 操作;Spring 提供了一个事务管理器接口,即 org.springframework.transaction.PlatformTransactionManager,每个事务管理器都要实现这个接口,所以 Spring 其实并不关心事务管理器的具体实现,这是每个平台自己的事情,常见的事务管理器有 JDBC 事务管理器(比如上面的这个就是 JDBC 事务管理器,配置好一个数据源就可以直接用了),除了 JDBC 事务管理器外,还有 Hibernate 事务管理器、JPA 事务管理器、JTA 事务管理器等。因为这里使用的是 JdbcTemplate 作为 ORM 框架,所以使用的就是 JDBC 事务管理器。PlatformTransactionManager 接口和对应平台实现的 TransactionManager 的关系有点类似于 JDBC API 和 JDBC Driver 的关系,前者只提供接口,后者负责实现这个接口。

来看一下这个 StudentJDBCTemplate 的具体实现:

但是实际上这种类型的事务管理太繁琐了,对原有代码有比较强的侵入性,而且感觉也不是很方便,还不如直接使用 JDBC 的 commit 和 rollback 呢。Spring 当然也知道这种方式不是很好用,所以后来就有了基于注解或者基于 XML 配置的声明式事务管理,所谓声明式事务管理其实就是通过注解或者 XML 告诉 Spring 来如何管理我的事务(声明一下就行了,如果执行成功,Spring 会自动帮我们提交事务,如果出现异常则自动帮我们回滚事务),而编程式事务管理则是全都是自己去管理的(自己主动去管理事务,如何时提交以及何时回滚等等),有点类似于 IoC,控制权反转了,基于 XML 配置的方式过时了,所以这里直接就介绍基于注解形式的声明式事务管理,注意,Spring 之所以能够进行声明式事务管理,还是因为它借助了 AOP 框架,将我们的业务方法包装了起来,Spring 会在调用业务方法的前后自动进行事务的开始、提交、回滚,以及其他一些操作。

类图结构
Spring 事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务,下面通过讲解 Spring 的事务接口来了解 Spring 实现事务的具体策略。Spring 事务管理涉及的接口的联系如下:
Spring 事务管理接口

事务管理器
Spring 并不直接管理事务,而是提供了多种事务管理器,它们将事务管理的职责委托给 JDBC、Hibernate、JPA、JTA 等持久化机制所提供的相关平台框架的事务来实现。Spring 事务管理器的接口是 org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring 为各个平台如 JDBC、Hibernate 等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。此接口的内容如下:

从这里可知具体的具体的事务管理机制对 Spring 来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以 Spring 事务管理的一个优点就是为不同的事务 API 提供一致的编程模型,如 JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。

JDBC 事务管理
如果应用程序中直接使用 JDBC 来进行持久化(如 MyBatis),DataSourceTransactionManager 会为你处理事务边界。为了使用 DataSourceTransactionManager,你需要使用如下的 XML 将其装配到应用程序的上下文定义中:

实际上,DataSourceTransactionManager 是通过从 dataSource 中获取的 Connection 对象来管理事务的,它通过调用 Connection 对象的 commit() 方法来提交事务,同样,在事务失败时通过调用 Connection 对象的 rollback() 方法进行回滚。

Hibernate 事务管理
如果应用程序的持久化是通过 Hibernate 实现的,那么你需要使用 HibernateTransactionManager。对于 Hibernate3,需要在 Spring 上下文定义中添加如下的 <bean> 声明:

基本上无论什么类型的事务管理器,内部都会将实际的 commit()、rollback() 操作委托给我们传递给它的对象来做,当然 HibernateTransactionManager 也一样,它会通过 sessionFactory 对象获取 hibernate 的 Transaction 对象,然后通过调用 Transaction 对象的 commit() 方法来进行事务的提交,调用 Transaction 对象的 rollback() 方法来进行事务的回滚。

JPA 事务管理
Hibernate 多年来一直是事实上的 Java 持久化标准,但是现在 Java 持久化 API 作为真正的 Java 持久化标准进入大家的视野。如果你计划使用 JPA 的话,那你需要使用 Spring 的 JpaTransactionManager 来处理事务。配置如下,其实和 HibernateTransactionManager 的配置很相似:

JpaTransactionManager 只需要装配一个 JPA 实体管理工厂(javax.persistence.EntityManagerFactory 接口的任意实现)。JpaTransactionManager 将与由该工厂所产生的 JPA EntityManager 合作来管理事务。

JTA 事务管理
如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用 JtaTransactionManager:

JtaTransactionManager 将事务管理的责任委托给 javax.transaction.UserTransaction 和 javax.transaction.TransactionManager 对象,其中事务成功完成通过 UserTransaction.commit() 方法提交,事务失败通过 UserTransaction.rollback() 方法回滚。

基本事务属性的定义
上面讲到的事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到 TransactionStatus 对象,这个方法里面的参数是 TransactionDefinition 类,这个类定义了一些基本的事务属性。那么什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了 5 个方面,如图所示:
事务的属性

TransactionDefinition 接口的定义如下:

事务的传播行为
事务的第一个方面是传播行为(propagation behavior)。当一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring 定义了 7 种传播行为(注意,外部事务就是调用方开启的事务,内部事务则是被调用方开启的事务,调用方也称为父方法,被调用方也称为子方法):

传播行为 具体含义
PROPAGATION_REQUIRED 当前方法需要在事务中运行;如果存在外部事务,则加入外部事务;如果不存在外部事务,则开启一个新的事务运行;默认传播行为。
PROPAGATION_REQUIRES_NEW 当前方法在内部事务中运行;如果存在外部事务,则外部事务将被挂起并开启新的事务运行,如果不存在外部事务,则开启一个新的事务运行。
PROPAGATION_NESTED 当前方法可作为嵌套事务来运行;如果存在外部事务,则作为外部事务的嵌套事务运行,如果不存在外部事务,则开启一个新的事务运行。
PROPAGATION_MANDATORY 当前方法必须在外部事务中运行;如果存在外部事务,则加入外部事务;如果不存在外部事务,则抛出一个运行时异常。
PROPAGATION_SUPPORTS 当前方法不需要在事务中运行;如果存在外部事务,则加入外部事务;如果不存在外部事务,则作为普通方法运行。
PROPAGATION_NOT_SUPPORTED 当前方法不支持在事务中运行;如果存在外部事务,则外部事务将被挂起,如果不存在外部事务,则作为普通方法运行。
PROPAGATION_NEVER 当前方法不能在事务中运行;如果存在外部事务,则抛出一个运行时异常;如果不存在外部事务,则作为普通方法运行。

这里需要指出的是,前面的 6 种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED 是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部事务的提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是 SavePoint 的一个应用,外部事务调用一个嵌套事务前会先创建一个 savepoint,然后再执行该嵌套事务,如果嵌套事务执行失败则会回滚到当前定义的保存点位置;一个事务中可以包括多个保存点;另外,外部事务的回滚也会导致嵌套子事务的回滚,因为它们本质上就是同一个事务,所谓嵌套事务不过是使用 savepoint 模拟出来的而已。总之自己多多联系 savepoint 来理解嵌套事务就行了。

PROPAGATION_REQUIRED
PROPAGATION_REQUIRED

PROPAGATION_REQUIRES_NEW
PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRED 的外部事务和内部事务是相关联的,如果外部事务回滚那么内部事务会一起回滚,同样的,内部事务回滚也会导致外部事务一起回滚;但是 PROPAGATION_REQUIRES_NEW 的外部事务和内部事务是互相独立的,在执行内部事务时,外部事务是处于挂起状态的,外部事务的回滚不会导致内部事务的回滚,内部事务的回滚也不会影响外部事务的回滚,它们两个事务实际上是互不影响的。

PROPAGATION_NESTED
由于嵌套事务其实是 jdbc savepoint 的一个应用,所以内部事务如果执行失败回滚的话,只会回滚到调用它之前的那个保存点上,因此我们可以利用这个特性来做一些分支操作,这可能是嵌套事务最有价值的一个地方:

事务的隔离级别
事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。

并发事务引起的问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致以下问题。

  • 脏读(Dirty reads):脏读是指一个事务读取了另一个事务改写但还未提交的数据;如果这个改写最终被提交了,那么也没什么问题,但是如果这个改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
  • 不可重复读(Nonrepeatable read):不可重复读是指一个事务执行相同的查询两次或两次以上,但是每次得到数据都不相同。这通常是因为另一个事务在两次查询期间进行了更新(update)。
  • 幻读(Phantom read):幻读是指一个事务执行相同的查询两次或两次以上,但是每次得到的数据条数都不相同。这通常是因为另一个事务在两次查询期间进行了增删(insert、delete)。注意幻读与不可重复读的区别,不可重复读是进行了 update 操作,导致数据字段变了,而幻读是指记录的条数变了(多了或者少了)。

不可重复读的重点是修改

同样的条件,你读取过的数据,再次读取出来发现值不一样了。

例如:在事务 1 中,Mary 读取了自己的工资为1000,操作并没有完成。

在事务 2 中,这时财务人员修改了 Mary 的工资为 2000,并提交了事务。

在事务 1 中,Mary 再次读取自己的工资时,工资变为了 2000

在一个事务中前后两次读取的结果并不一致,导致了不可重复读。

幻读的重点在于新增或者删除

同样的条件,第 1 次和第 2 次读出来的记录数不一样。

例如:目前工资为 1000 的员工有 10 人。事务 1 读取所有工资为 1000 的员工。

共读取到 10 条记录。这时另一个事务向 employee 表插入了一条员工记录,工资也为 1000:

事务 1 再次读取所有工资为 1000 的员工

共读取到了 11 条记录,这就产生了幻像读。

从总的结果来看,似乎不可重复读和幻读都表现为两次读取的结果不一致。但如果你从避免的角度来看,两者的区别就比较大。

  • 对于前者(不可重复读),只需要锁住满足条件的记录(记录锁)。
  • 对于后者(幻像读),要锁住满足条件及其相近的记录(间隙锁)。

隔离级别

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别,这是 Spring 的默认隔离级别
ISOLATION_READ_UNCOMMITTED 读未提交,最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、不可重复读、幻读
ISOLATION_READ_COMMITTED 读已提交,允许读取其他事务已经提交的数据,可以阻止脏读,但是不可重复读、幻读仍有可能发生
ISOLATION_REPEATABLE_READ 可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_SERIALIZABLE 串行化,最高的隔离级别,完全服从 ACID 的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

只读事务
事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行只读操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。

事务超时
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。

回滚规则
事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚(RuntimeException、Error),而在遇到检查型异常时不会回滚(这一行为与 EJB 的回滚行为是一致的),但通常我们希望的回滚行为是这样的:只要发生的异常,不管是 RuntimeException 还是 Error 还是普通的 Exception,都自动执行事务的回滚,如果要实现这个目标只需要将事务的 rollback-for 属性改为 Exception.class,这样就是只要抛出异常或错误,那么 Spring 就会自动进行事务回滚。当然,你还可以声明事务遇到特定的异常不回滚,即使这些异常是 RuntimeException 或 Error。

事务状态
上面讲到的调用 PlatformTransactionManager 接口的 getTransaction() 方法得到的是 TransactionStatus 接口的一个实现,这个接口的定义如下:

编程式和声明式事务的区别
Spring 提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦。简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于 AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。

如何实现编程式事务?
Spring 提供两种方式的编程式事务管理,分别是:使用 TransactionTemplate(推荐)和直接使用 PlatformTransactionManager。

使用 TransactionTemplate
采用 TransactionTemplate 和采用其它 Spring 模板,如 JdbcTempalte 和 HibernateTemplate 是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate 是线程安全的。代码片段:

使用 TransactionCallback 可以返回一个值。如果使用 TransactionCallbackWithoutResult 则没有返回值。

使用 PlatformTransactionManager

声明式事务
根据代理机制的不同,总结了五种 Spring 事务的配置方式,配置文件如下:

1)每个 Bean 都有一个代理

2)所有 Bean 共享一个代理基类

3)使用拦截器

4)使用 tx 标签配置的拦截器

5)全注解

此时在 Service 方法上使用 @Transactional 注解,如下:

目前流行的方式都是全注解形式的声明式事务,所以我们主要也是了解这种使用方式,spring.xml 配置为:

然后我们只需要在 Service 实现类的方法上使用 @Transactional 注解标注就行了,该注解上可以设置上述 5 个事务属性。除此以外,@Transactional 注解也可以添加到类级别上(Service 实现类上)。当把 @Transactional 注解放在类级别时,表示所有该类的公共方法都配置相同的事务属性信息。见清单 2,EmployeeService 的所有方法都支持事务并且是只读(注意,只要在类级别上使用了 @Transactional 注解,那么这个类里面的所有 public 方法都会自动配置事务)。当类级别配置了 @Transactional,方法级别也配置了 @Transactional 时,应用程序会以方法级别的事务属性信息来管理事务,换言之,方法级别的事务属性信息会覆盖类级别的相关配置信息。

  • @Transactional 注解只能应用到 public 方法上。如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,但 Spring 不会对这些方法进行事务处理。
  • 注意,仅仅 @Transactional 注解的出现不足于开启事务行为,因为注解仅仅是一种元数据。必须在配置文件中使用配置元素(也就是上面的 <tx:annotation-driven> 元素),才真正开启了事务行为。
  • 通过 <tx:annotation-driven> 元素的 proxy-target-class 属性值来控制是使用 JDK 动态代理还是 CGLIB 动态代理,如果为 false(默认值)则表示优先选择 JDK 动态代理(基于接口),当目标对象没有实现任何接口时才会使用 CGLIB 动态代理(基于类),而如果将其设为 true,则表示默认使用 CGLIB 动态代理,而不是 JDK 动态代理。
  • 由于 Spring AOP 的一些实现限制,如果在业务类的一个方法中调用另一个配置了 @Transactional 的事务方法(称为自调用),那么 Spring 的事务不会起作用,因为这个方法调用是通过 this 对象来调用的目标方法,而不是先通过代理对象来委托调用的目标方法,所以没有走代理。简单来说就是 @Transactional 注解只对从 AOP 返回的对象有效,如果直接通过 this 指针来调用当前对象的目标方法,实际上根本没有机会执行外部的代理对象的事务处理逻辑。再换句话说,对于 JDK 动态代理,你需要通过代理对象来调用目标方法,才能让代理正常生效,如果你直接通过原对象(即 this 指针这种形式的调用)来访问目标方法,那么是不会走代理的!要解决这个问题,你需要使用 AspectJ 来取代 Spring AOP 代理(JDK 动态代理、CGLIB 动态代理)。当然也有简单的方法,那就是在 Service 类中添加一个成员变量,类型就是自己,然后使用 @Autowired 来让 Spring 自动将代理对象注入进来,然后我们通过这个注入进来的代理对象调用目标方法就没问题了。

@Transactional 注解的几个属性

  • String value:如果配置了多个 transactionManager,则用于指定当前需要使用的事务管理器的 bean id(别名)。
  • String transactionManager:如果配置了多个 transactionManager,则用于指定当前需要使用的事务管理器的 bean id。
  • Propagation propagation:事务的传播行为,默认为 Propagation.REQUIRED。
  • Isolation isolation:事务的隔离级别,默认为 Isolation.DEFAULT。
  • int timeout:事务的超时事件(秒),如果超时则自动回滚,默认为 -1 表示永不超时,注意这个超时策略仅适用于 Propagation.REQUIRED、Propagation.REQUIRES_NEW 传播行为,因为它仅适用于新启动的事务。
  • boolean readOnly:事务是否为只读的,默认为 false,需要说明的是,这个属性的作用仅仅是给底层数据库的一个提示,不一定有效果。
  • Class<? extends java.lang.Throwable>[] rollbackFor:指定哪些异常将会导致事务的回滚,默认情况下只有 RuntimeException 和 Error 才会触发事务的回滚。
  • String[] rollbackForClassName:作用同上,只不过指定的是异常类的全限定类名。
  • Class<? extends java.lang.Throwable>[] noRollbackFor:指定哪些异常不会导致事务的回滚。默认为空。
  • String[] noRollbackForClassName:同上,只不过指定的是异常类的全限定类名。

SSM 框架整合-示例

项目结构

pom.xml

logback.xml

jdbc.properties

web.xml

mvc.xml

Employee.java

EmployeeMapper.java

EmployeeMapper.xml

EmployeeService.java

EmployeeServiceImpl.java

EmployeeController.java

index.jsp

employee-list.jsp

employee-edit.jsp

运行项目

日志输出:

员工列表:
员工列表

新建员工:
新建员工

编辑员工:
编辑员工

相关说明

mvc.xml 中的 mybatis 相关配置

Mapper 接口上面的 @Repository 注解
这个注解其实不是必须的,但是由于 idea 无法检测到 <mybatis:scan base-package="com.zfl9.mapper"/> 这种形式的组件扫描,所以如果你不在 mapper 接口上使用这个注解(当然其它组件扫描注解都可以),那么当你在 service 上使用 @Autowired 来自动装配 Mapper 接口实现类的时候,idea 就会报错,所以干脆就在接口上面使用这个 @Repository 注解,这样 idea 就不会有报错了。

EmployeeServiceImpl 里面的注解说明

注意我们直接使用 @Autowired 来自动装配 EmployeeMapper 的实现类,这时候有些人就有疑问了,不是说 SqlSession/Mapper 不是线程安全的么?为什么这里可以这样用呢?因为 Spring 中的 bean 默认都是单例模式,所以这个注入进来的 employeeMapper 实际上也是单例模式,而 servlet 是运行在多线程环境中的,难道不会出现线程安全问题?其实不用担心,mybatis-spring 早就考虑到了这种情况,并且我上面的这种用法和官方文档的用法也是一样的,那么这里面的机制到底是怎么样的呢?

将 spring 与 mybatis 整合之后,对一级缓存和二级缓存产生了一些影响:

  • 如果当前 service 方法未开启事务,那么每次调用 mapper 的增删改查方法使用的都是新获取的 sqlSession,每次调用使用的 sqlSession 都是不一样的,所以此时一级缓存不会生效,但是二级缓存会生效,因为 sqlSession 关闭或提交之后,就会将一级缓存中的数据转移到二级缓存中。
  • 如果当前 service 方法开启了事务,那么该方法内的所有 mapper 的增删改查方法调用都是使用的同一个 sqlSession,mybatis-spring 会使用 thread-local 保存与当前线程相关联的 sqlSession,从而避免锁的存在;因为该方法内的所有 mapper 调用都是使用的同一个 sqlSession,所以此时一级缓存是有效的,当然二级缓存也是有效的(在开启了二级缓存的情况下)。

所以这个自动装配的 employeeMapper 实际上与我们以前直接使用 mybatis 框架中的 employeeMapper 是不同的,前者是线程安全的,而且里面会做上述判断,如果当前方法未配置事务,则每次调用都是使用新的 sqlSession,如果配置了事务,则当前方法内的所有调用都是使用的同一个 sqlSession;而后者的线程安全性和生命周期都和原始 sqlSession 一样,非线程安全,且建议在方法内部进行初始化和销毁操作。

未开启事务

可以看到每次调用 employeeMapper 的方法都是使用的新的 sqlSession 对象,调用完之后这个 sqlSession 就会被关闭。

开启了事务

可以看到,两次 mapper 方法调用使用的是同一个 sqlSession 对象,所以此时一级缓存是有效的。

<mybatis:scan base-package="com.zfl9.mapper"/>
这种写法其实是 mybatis-spring 新版提供的简写方式,原先还有这种写法:

貌似使用 mybatis:scan 简写方式,idea 会报错,如果有强迫症,最好使用原先的老方法。

idea 和 spring 不建议在私有属性上使用 @Autowired 自动装配
idea 和 spring 的建议是,在构造函数、setter 方法上使用 @Autowired 自动装配,不建议直接在私有属性上使用它。

SSM 框架整合-全注解

其实和上面的 xml-based 例子没多大区别,只是将 web.xml、mvc.xml 替换为 java-based 类文件而已:

  • web.xml servlet 配置文件:使用 WebConfig.java 类替代
  • mvc.xml springmvc 配置文件:使用 MvcConfig.java 类替代

WebConfig.java

MvcConfig.java

mvc.xml 相关解释

先来说 MvcConfig.java,这个大家应该比较熟悉了,就是 spring.xml 的 java-based 版本:

注解相关

  • @Configuration:表示当前类是一个 spring 配置类,类似于一个 spring.xml 配置文件。
  • @ComponentScan:启用组件扫描,如果不指定 basePackage,则默认为当前 package(含子包)。
  • @EnableWebMvc:启用 mvc 的注解驱动功能,等价于 xml 配置文件中的 <mvc:annotation-driven/>
  • @EnableTransactionManagement:启用 tx 的注解驱动功能,等价于 xml 配置文件中的 <tx:annotation-driven/>
  • @MapperScan("com.zfl9.mapper"):扫描指定包下的映射器并自动注入,等价于 <mybatis:scan base-package="com.zfl9.mapper"/>
  • @PropertySource:引入外部属性文件,等价于 xml 的 <context:property-placeholder location="classpath:jdbc.properties"/>

如果看过 @Configuration 的 javadoc,你会发现它还被 @Component 标注了,所以 @Configuration 配置类也是一个 bean,会被自动注册到 Spring 的 Context 上下文容器中。所以适用于 @Component(及其衍生注解)的规则和方法基本上都适用于 @Configuration 标注的配置类。

继承的类

  • WebMvcConfigurer:WebMvcConfigurer 配置接口,提供了一些常用配置方法,在 spring 5.0 之后,因为是使用 java 8 构建的,所以直接在接口中提供了默认实现,于是 WebMvcConfigurerAdapter 适配器就被弃用了,因此如果你使用 spring 5.0,请记得改用为 implements WebMvcConfigurer 接口,而不是 extends WebMvcConfigurerAdapter 抽象类;但是因为我还是使用的 spring 4.3.20,所以上面仍然使用 WebMvcConfigurerAdapter 适配器。
  • WebMvcConfigurerAdapter:WebMvcConfigurer 抽象基类,提供了接口的默认实现,这样我们只重写其中一些方法就可以使用了。

Environment
org.springframework.core.env.Environment 意为“环境”,environment 包括两个方面的抽象:profile 和 property,profile 就是所谓的运行环境,比如 dev 为开发环境、test 为测试环境、prod 为生产环境,通常这些运行环境的 dataSource 等配置是不同的,所以我们可以通过在不同的 profile 下面配置相应的 dataSource 来解决这个问题;而 property 就是我们常说的 java 属性了,在上面就是可以用来引用 @PropertySource 加载的外部属性文件中的属性值,通过 environment.getProperty("jdbc.url") 就可以读取了,而不需要使用传统的 @Value("${jdbc.url}") 注解来读取。

configureDefaultServletHandling()
WebMvcConfigurer 接口定义的方法,便于我们启用 spring mvc 的默认静态资源映射配置,等价于 xml 的 <mvc:default-servlet-handler/>

configureViewResolvers()
WebMvcConfigurer 接口定义的方法,便于我们设置 spring mvc 的视图解析器,这里我们就是要 jsp 视图解析器,设置了 prefix 和 suffix。

其它的几个 @Bean 方法
没什么可讲的,你和上面的 mvc.xml 中的配置进行对比就非常清楚了。

web.xml 相关解释

其实在学习 tomcat 的时候我们就知道,从 servlet 3.0 开始,已经完全可以不需要 web.xml 部署描述符文件了,但是当时我们只是介绍了无 web.xml + @WebServlet 注解的简单形式,但其实我们可以完全通过 javaconfig 来替代 web.xml,进入“完全使用注解来配置的时代”。那么这是如何实现的呢?

servlet 3.0 提供了一个接口:ServletContainerInitializer,意为“servlet 容器初始化器”,其实就是 web.xml 的 java-based 版本。和 jdbc 4.0 的 driver 服务自动发现机制一样,ServletContainerInitializer 接口也使用了 JDK 1.6 开始提供的 SPI 机制,也就是说,实现了 servlet 3.0 规范的应用服务器会利用 SPI 机制,扫描类路径下的 META-INF/services/javax.servlet.ServletContainerInitializer 文件(在讲 SPI 机制的时候学过),获取其中以行为单位的 ServletContainerInitializer 接口实现类的全限定类名,然后通过反射 API 实例化它们,再依次调用这些对象的 onStartup(Set<Class<?>> c, ServletContext ctx) 方法来注册 servlet、filter、listener 等 web 应用的基本组件。

ServletContainerInitializer 接口只有一个方法:

第二个参数比较好理解,就是当前 servlet 的上下文对象;重点解释第一个参数。如果 ServletContainerInitializer 实现类上使用了 @HandlesTypes 注解(该注解只有一个 value 属性,用于指定一个或多个 Class 对象),那么 Servlet 容器(如 Tomcat)就会自动扫描类路径下的这些 Class 对象对应的 type 的 子类(extends)、实现类(implements)以及 当前类型(总结一句话就是,Servlet 容器会自动将与其兼容的类型注入到我们的 ServletContainerInitializer 实现类中,作为 onStartup() 方法的第一个参数传入),然后在调用初始化器的 onStartup() 方法时,就会自动将这些符合要求的 Class 对象作为一个 Set 集合传入进去(通过第一个参数),然后我们就可以在初始化器中调用这些 Class 对象的 newInstance() 方法来获取它们的实例(这些对象通常是我们感兴趣的对象),然后进行其他一些操作。

spring-web 模块提供了 ServletContainerInitializer 实现类:SpringServletContainerInitializer,且配置到了 META-INF/services 中:

通过 spring-web 模块的源码可以看到,SpringServletContainerInitializer 实现类上使用了 @HandlesTypes(WebApplicationInitializer.class) 注解,表示 SpringServletContainerInitializer 对 WebApplicationInitializer 实现类或子类感兴趣,希望 Servlet 容器在调用其 onStartup() 方法时自动传入这些类的 Class 对象。然后 SpringServletContainerInitializer 会在 onStartup() 方法中获取容器自动注入进来的 Class 对象,如果不是空的,那么说明应用程序使用了基于 javaconfig 形式的 ServletContainerInitializer 配置,所以就会调用 Class 对象的 newInstance() 方法来对其实例化,然后经过排序,再依次调用这些对象的 onStartup() 方法。

所以我们只要实现 WebApplicationInitializer 接口就可以直接使用 javaconfig 形式的 web.xml 配置了。不过 spring 为了方便,也提供了若干个 WebApplicationInitializer 接口的抽象基类,比如常用的 AbstractAnnotationConfigDispatcherServletInitializer,这个抽象类其实又是 AbstractDispatcherServletInitializer 抽象基类的扩展类,而 AbstractDispatcherServletInitializer 抽象类其实又是 AbstractContextLoaderInitializer 抽象基类的扩展类,而 WebApplicationInitializer 则实现了 WebApplicationInitializer 接口。所以,我们通常不会去直接实现 WebApplicationInitializer 接口,而是继承 AbstractAnnotationConfigDispatcherServletInitializer 抽象基类,然后重写几个重要的方法就行了。比如上面我就是这样做的。AbstractAnnotationConfigDispatcherServletInitializer 抽象类的几个常用方法:

  • getRootConfigClasses():获取 root-config 配置类(根配置类),其实就是 web.xml 中的 ContextLoaderListener 使用的 spring 配置文件,因为我们不需要这个,所以直接返回 null 即可。
  • getServletConfigClasses():获取 servlet-config 配置类(mvc 配置类),其实就是 web.xml 中的 DispatcherServlet 使用的 spring 配置文件,返回我们自己提供的 MvcConfig.class 就行了。
  • getServletMappings():设置 DispatcherServlet 的 url-pattern,这里我们就照常设置为 / 就行了(默认映射到所有 url)。
  • getServletFilters():设置 web.xml 中的 Filter 过滤器,这里就注册了一个强制规定字符编码为 UTF-8 的过滤器。

通过这些配置(记得删除 WEB-INF 目录下的 web.xml 和 mvc.xml),我们就可以配置 Tomcat 服务器,然后启动进行测试了。