1 MyBatis 回顾
1.1 MyBatis 开发回顾
开发环境准备
在 IDEA 中创建 Maven 工程,在 pom.xml 文件中加入以下依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
在 resources 目录中创建 mybatis-config.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置环境 -->
<environments default="mysql">
<!-- 配置mysql的环境 -->
<environment id="mysql">
<!-- 配置事务类型 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据源/连接池 -->
<dataSource type="POOLED">
<!-- 配置连接数据库的4个基本类型 -->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/javaee"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
</configuration>
开发步骤
-
创建实体:
com.hihanying.mybatis.origin.entity.Userprivate Integer id; private String username; private Date birthday; private String sex; private String address; -
在 mybatis-config.xml 文件中为实体创建别名,
<typeAliases> <!--方便后续书写 Mapper 文件时不需要再写全限定类名,只需引用别名--> <typeAlias type="com.hihanying.mybatis.origin.entity.User" alias="user"/> </typeAliases> -
在数据库中创建 uer 表
CREATE DATABASE javaee; -- 创建数据库 USE javaee; -- 使用数据库 DROP TABLE IF EXISTS `user` -- 创建表user CREATE TABLE `user` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL COMMENT '用户名称', `birthday` DATETIME DEFAULT NULL COMMENT '生日', `sex` CHAR(1) DEFAULT NULL COMMENT '性别', `address` VARCHAR(256) DEFAULT NULL COMMENT '地址', PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8; -- 添加数据 INSERT INTO `user`(`id`,`username`,`birthday`,`sex`,`address`) VALUES (41,'老王','2018-02-27 17:47:08','男','北京'),(42,'小二王','2018-03-02 15:09:37','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'传智播客','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正'); -
创建DAO接⼝
package com.hihanying.mybatis.origin.dao; import com.hihanying.mybatis.origin.entity.User; import java.util.List; public interface UserDAO { List<User> findAll(); public void save(User user); } -
实现 Mapper ⽂件:rc\main\resources\com.hihanying.mybatis.origin\dao\UserDAOMappser.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.hihanying.mybatis.origin.dao.UserDAO"> <select id="findAll" resultType="user"> select * from user </select> <insert id="save" parameterType="user"> insert into user(username, address) values (#{username}, #{address}) </insert> </mapper> -
在 mybatis-config.xml 文件中注册 Mapper ⽂件
<mappers> <mapper resource="com.hihanying.mybatis.origin\dao\UserDAOMapper.xml"/> </mappers> -
MyBatis API 调⽤
import com.hihanying.mybatis.origin.dao.UserDAO; import com.hihanying.mybatis.origin.entity.User; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; public class MybatisTest { public static void main(String[] args) throws IOException { // 1.读取配置文件 SqlMapConfig.xml InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); // 2.通过配置文件创建 SqlSessionFactory 工厂 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); // 3.通过 SqlSessionFactory 工厂创建 SqlSession 对象 SqlSession sqlSession = factory.openSession(); // 4.通过 SqlSession 对象创建 IUserDao 接口的代理对象 UserDAO userDao = sqlSession.getMapper(UserDAO.class); // 5.使用代理对象 userDao 执行方法 List<User> users = userDao.findAll(); for (User user : users) { System.out.println(user); } User user = new User(); user.setUsername("suns"); user.setAddress("Beijing"); userDao.save(user); sqlSession.commit(); // 6.释放资源 sqlSession.close(); is.close(); } }
2 Spring 与 MyBatis 整合
2.1 整合思路分析
MyBatis 在开发过程中存在问题
- 配置繁琐:在 MyBatis 开发中,每创建一个实体,就要创建一个 Mapper 文件,且要在 mybatis-config.xml 文件中创建别名以及注册 Mapper 文件,配置较为繁琐
- 代码冗余:在MyBatis API 调用时,每次都需要读取配置文件,创建 SqlSessionFactory 工厂,创建 SqlSession 对象进而创建 Dao 接口的代理对象,从而调用方法,且执行完毕后需要提交事务,关闭资源,这都是一些重复的工作
Spring 提供的方案
-
SqlSessionFactoryBean为了获取
SqlSessionFactory工厂,Spring 提供了SqlSessionFactoryBean,用于封装SqlSessionFactory工厂创建的代码及配置,即以下代码与mybatis-config.xml:// 1.读取配置文件 SqlMapConfig.xml InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); // 2.通过配置文件创建 SqlSessionFactory 工厂 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is);观察发现,
mybatis-config.xml配置文件中主要包括三个方面的内容:dataSource、typeAliases、mappers,而 Spring 通过注入的方式将这3个配置信息提供给 SqlSessionFactoryBean。-
dataSource属性:自建 Bean,通过引用的方式注入创建连接池对象
-
typeAliasesPackage:只需指定实体所在的包,Spring 会自动为其中的实体创建别名,就是类名 -
mapperLocations:通配设置*Mapper.xml
-
-
MapperScannerConfigure同样,为了获得 Dao 对象,Spring 提供了
MapperScannerConfigure,用于封装 Dao 对象创建的代码:// 3.通过 SqlSessionFactory 工厂创建 SqlSession 对象 SqlSession sqlSession = factory.openSession(); // 4.通过 SqlSession 对象创建 IUserDao 接口的代理对象 UserDAO userDao = sqlSession.getMapper(UserDAO.class);如果想封装这段代码,需要 Spring 为
MapperScannerConfigure提供SqlSessionFactory工厂以及 DAO接口的 class 对象,因此,Spring 通过注入的方式提供了这两个信息。sqlSessionFactoryBeanName属性:提供SqlSessionFactory工厂 Bean 的 idbasePackage属性:设置 DAO 接口所在的包
通过 Spring 创建对象时我们需要对象的 id 值,而这个 id 值默认值是接口名的首单词首字母小写。
2.2 整合开发步骤
1. 搭建环境
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
其中,druid 是 阿里巴巴提供的连接池
**2. 配置文件 **
src\main\resources\applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 创建 连接池 对象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/javaee"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!-- sqlSqlSessionFactoryBean 注入,创建 sqlSqlSessionFactory 对象-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.hihanying.mybatis.origin.entity"/>
<property name="mapperLocations">
<list>
<value>classpath:com.hihanying.mybatis.origin.dao/*Mapper.xml</value>
</list>
</property>
</bean>
<!-- MapperScannerConfigure 注入,创建 DAO 对象-->
<bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<property name="basePackage" value="com.hihanying.mybatis.origin.dao"/>
</bean>
</beans>
3. 调用
import com.hihanying.mybatis.origin.dao.UserDAO;
import com.hihanying.mybatis.origin.entity.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MybatisSpringTest {
public static void main(String[] args) {
// 获取对象
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDAO userDAO = (UserDAO) ctx.getBean("userDAO");
// 使用对象
User user = new User();
user.setUsername("han");
user.setAddress("Beijing");
userDAO.save(user);
}
}
2.3 整合细节分析
问题:Spring与 MyBatis 整合后,为什么 DAO 不提交事务,但是数据能够插⼊数据库中?
-
MyBatis 提供的连接池对象默认手工控制事务
Connection.setAutoCommit(false) -
Druid(C3P0、DBCP)作为连接池默认自动控制事务
Connection.setAutoCommit(true)
答案:因为 Spring与 MyBatis 整合时,引⼊了外部连接池对象,保持⾃动的事务提交这个机制,不需要⼿⼯进⾏事务的操作,也能进⾏事务的提交。如果想⼿⼯控制事务(多条 sql 语句⼀起成功,⼀起失败),Spring 通过事务控制解决这个问题。
3. Spring 事务处理
3.1 事务概述
保证业务操作完整性的一种数据库机制
数据库事务的四大特性:ACID(Atomicity、Consistent、Isolation、Durable)
- 原子性:事务包含的所有数据库操作要么全部成功,要不全部失败回滚
- 一致性:一个事务执行之前和执行之后都必须处于一致性状态(能量守恒)。
- 隔离性:一个事务未提交的业务结果是否对于其它事务可见。
- 持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
3.2 控制事务
- JDBC控制事务的方式
- 开启:Connection.setAutoCommit(false);
- 提交:Connection.commit();
- 回滚:Connection.rollback();
- Mybatis控制事务的方式
- Mybatis⾃动开启事务
- 提交:sqlSession(Connection).commit();
- 回滚sqlSession(Connection).rollback();
控制事务的底层都是Connection对象完成的。
3.3 开发原理
Spring 通过 AOP 的方式进行事务开发
1. 目标对象
-
目标对象中对应的目标方法只完成核心功能(业务处理+DAO调用)
-
DAO 作为 Service 的成员变量,通过依赖注入的方式进行赋值
2. 附加功能
通过实现 MethodInterceptor 的方式添加事务
public Object invoke(MethodInvocation invocation){
try{
Connection.setAutoCommit(false);
Object ret = invocation.proceed();
Connection.commit();
}catch(Exception e){
Connection.rollback();
}
return ret;
}
Spring 将上述代码封装在 org.springframework.jdbc.datasource.DataSourceTransactionManager 中,观察上述代码,我们可以知道 DataSourceTransactionManager 需要 Connection 对象,即需要 DataSource 的注入。
另外可以通过注解:@Aspect @Around 实现
3. 设置切入点
通过注解的方式设置切入点:@Transactional
- 类上:类中所有的⽅法都会加⼊事务
- ⽅法上:这个⽅法会加⼊事务
4. 组装切面
通过配置 xml 文件中的标签 <tx:annotation-driven transaction-manager=""/> 实现切入点和附加功能的组装
3.4 开发步骤
1. 搭建开发环境
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
2. 目标对象
package com.hihanying.mybatis.origin.service;
import com.hihanying.mybatis.origin.dao.UserDAO;
import com.hihanying.mybatis.origin.entity.User;
public class UserServiceImpl implements UserService {
private UserDAO userDAO;
public UserDAO getUserDAO() {
return userDAO;
}
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
@Override
public void register(User user) {
userDAO.save(user);
}
}
<bean id="userService" class="com.hihanying.mybatis.origin.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>
此处引用的 Bean userDAO 无需在配置文件中添加,因为
MapperScannerConfigure在获取 DAO 对象时,自动为其设定 id ,值为对应DAO 接口名的首字母小写。
3. 附加功能
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
4. 设置切入点
通过将 @Transactional 注解加在目标类 UserServiceImpl 上,表示对这个类的所有方法都加入对应的附加功能
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class UserServiceImpl implements UserService {...}
5. 组装切面
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
注意:
- 此处的
tx:annotation-driven引入的是xmlns:tx="http://www.springframework.org/schema/tx"- 还可以设置 proxy-target-class 属性的值(默认false: JDK;设置true: CGlib)来切换动态代理的底层实现。
6. 测试
为了测试是否将事务(附加功能)加入了 UserService (目标对象),我们在 UserServiceImpl 类中加入以下异常。
@Override
public void register(User user) {
userDAO.save(user);
throw new RuntimeException("测试");
}
如果加上了事务,上面两行代码就构成了一个整体,符合事务的原子性,也就是说这两行代码要么一起成功,要么也一起失败,如果抛出异常,整体都要失败,事务需要回滚,新的 User 对象不会加入数据库。
@Test
public void test2() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService UserService = (UserService) ctx.getBean("userService");
User user = new User();
user.setUsername("suns");
user.setAddress("123456");
userService.register(user);
}
查看日志与输出,可以看到在第1行,新的 Parameters: suns1(String), 123456(String)已经更新,但由于抛出异常,在第6行开始了事务的回滚。
......
2020-10-12 23:17:31 DEBUG save:137 - ==> Parameters: suns1(String), 123456(String)
2020-10-12 23:17:31 DEBUG save:137 - <== Updates: 1
2020-10-12 23:17:31 DEBUG SqlSessionUtils:49 - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ba745bc]
2020-10-12 23:17:31 DEBUG SqlSessionUtils:49 - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ba745bc]
2020-10-12 23:17:31 DEBUG SqlSessionUtils:49 - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ba745bc]
2020-10-12 23:17:31 DEBUG DataSourceTransactionManager:833 - Initiating transaction rollback
2020-10-12 23:17:31 DEBUG DataSourceTransactionManager:341 - Rolling back JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@15b986cd]
2020-10-12 23:17:31 DEBUG DataSourceTransactionManager:385 - Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@15b986cd] after transaction
java.lang.RuntimeException: 测试
4. Spring中的事务属性
4.1 事务属性概述
事务属性是描述事物特征的一些列值,包括:
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
通过注解 @Transactional(isloation=,propagation=,readOnly=,timeout=,rollbackFor=,noRollbackFor=,) 可以设置属性的值
4.2 事务属性详解
1. 隔离属性
概述
隔离属性描述了事务解决并发问题所设置的值。
并发产生的问题及解决方案
-
脏读
-
描述:事务 A 读取了事务 B 中没有提交的数据,此时如果事务 B 发生错误并执行回滚操作,那么事务 A 读取的就是脏数据。
-
解决:配置事务注解的隔离属性为 READ_COMMITTED:
@Transactional(isolation=Isolation.READ_COMMITTED)
-
-
不可重复读
- 描述:事务 A 在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 ,在此期间事务 B 更新了这条数据,导致事务 A 重复读取这条数据时出现了前后不一致的问题。(注意和脏读不同,不可重复读是指在一个事务(A)中)
- 解决:配置事务注解的隔离属性为 REPEATABLE_READ:
@Transactional(isolation=Isolation.REPEATABLE_READ) - 本质:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
-
幻读
- 描述:事务 A 多次对整表进⾏查询统计,前一次查询统计后,事务 B 执行了新增或删除数据的操作并提交,之后事务 A 再次查询统计和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,称为幻读。
- 解决:配置事务注解的隔离属性为 SERIALIZABLE:
@Transactional(isolation=Isolation.SERIALIZABLE) - 本质:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
不可重复读和幻读的区别:
- 不可重复读是读取了其他事务更改的数据,针对update操作
- 幻读是读取了其他事务新增的数据,针对insert和delete操作
总结:
- 并发安全: SERIALIZABLE > REPEATABLE_READ > READ_COMMITTED
- 运⾏效率: READ_COMMITTED > REPEATABLE_READ > SERIALIZABLE
数据库对于隔离属性的⽀持
| 隔离属性的值 | MySQL | Oracle |
|---|---|---|
| ISOLATION_READ_COMMITTED | ✅ | ✅ |
| IOSLATION_REPEATABLE_READ | ✅ | ❌ |
| ISOLATION_SERIALIZABLE | ✅ | ✅ |
Oracle不⽀持REPEATABLE_READ,而采⽤的是多版本⽐对的⽅式解决不可重复读的问题
默认隔离属性
@Transactional 事务默认的隔离属性值是 ISOLATION_DEFAULT:表示会调⽤不同数据库所设置的默认隔离属性
- MySQL : REPEATABLE_READ
- Oracle: READ_COMMITTED
查看数据库默认隔离属性的方法
-
MySQL:
select @@tx_isolation; -
Oracle
SELECT s.sid, s.serial#, CASE BITAND(t.flag, POWER(2, 28)) WHEN 0 THEN 'READ COMMITTED' ELSE 'SERIALIZABLE' END AS isolation_level FROM v$transaction t JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');
隔离属性在实际应用中的建议
推荐使⽤ Spring 指定的默认值:ISOLATION_DEFAULT
- MySQL : REPEATABLE_READ
- Oracle: READ_COMMITTED
在实际应用中,并发访问的情况较少,如果真遇到并发问题,可以使用乐观锁解决
- Hibernate(JPA) Version
- MyBatis 通过拦截器⾃定义开发
悲观锁和乐观锁
悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。
何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。
此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
2. 传播属性
概述
传播属性描述了事务解决嵌套问题的特征。
事务的嵌套是指⼀个⼤的事务中,包含了若⼲个⼩的事,这种情况通常出现在若干个业务相互调用时。例如,由于业务的互相调用,可能会使得在事务 A 中嵌套了事务 B 和事务 C ,如果在执行事务 C 的时候遇到异常导致失败,那么事务 A 会执行回滚操作,但此时事务 B 已经提交,无法回滚,因此破坏了事务 A 的原子性。
传播属性的值及其⽤法
| 传播属性的值 | 外部不存在事务 | 外部存在事务 | 用法 | 备注 |
|---|---|---|---|---|
| REQUIRED | 开启新的事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.REQUIRED) | 增删改 |
| SUPPORTS | 不开启事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.SUPPORTS) | 查 |
| REQUIRES_NEW | 开启新的事务 | 挂起外部事务,创建新的事务 | @Transactional(propagation = Propagation.REQUIRES_NEW) | 日志记录 |
| NOT_SUPPORTED | 不开启事务 | 挂起外部事务 | @Transactional(propagation = Propagation.NOT_SUPPORTED) | 不常用 |
| NEVER | 不开启事务 | 抛出异常 | @Transactional(propagation = Propagation.NEVER) | 不常用 |
| MANDATORY | 抛出异常 | 融合到外部事务中 | @Transactional(propagation = Propagation.MANDATORY) | 不常用 |
REQUIRED 是传播属性的默认值
传播属性在应用时的建议
- 增删改操作:直接使⽤默认值 REQUIRED
- 查询操作:显示指定传播属性的值为 SUPPORTS
3. 只读属性
针对于只进⾏查询操作的业务⽅法,可以加⼊只读属性,提供运⾏效率,默认值是 false
4. 超时属性
描述:超时属性指定了事务等待的最⻓时间(单位是秒)
问题:当前事务访问数据时,有可能访问的数据被别的事务进⾏加锁的处理,那么此时本事务就必须进⾏等待。
解决:通过设置事务注解 @Transactional 超时属性 timeout 的值来指定事务等待的最长时间:@Transactional(timeout=2)
超时属性的默认值是 -1,表示最终由对应的数据库来指定超时属性的值
5. 异常属性
描述:Spring 事务处理过程中默认对于 RuntimeException 及其⼦类异常采⽤的是回滚的策略,而默认对于 Exception 及其⼦类采⽤的是提交的策略。
用法:通过设置事务注解 @Transactional 异常属性的值可以指定异常是否采用回滚的策略
// 表示对于 Exception 使用回滚的操作,而对于 RuntimeException 不使用回滚的操作
@Transactional(rollbackFor = {java.lang.Exception.class},noRollbackFor= {java.lang.RuntimeException.class})
建议:实际应用中 RuntimeExceptin 及其⼦类使⽤事务异常属性的默认值,即回滚操作。
4.3 基于标签的事务配置⽅式
基于注解的事务配置
<!--1. 目标对象-->
<bean id="userService" class="com.hihanying.mybatis.origin.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>
<!--2. 附加功能-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--4. 组装切面-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
// 3. 设置切入点,配置事务属性
import org.springframework.transaction.annotation.Transactional;
@Transactional//(propagation=Propagation.SUPPORTS,readOnly=true)
public class UserServiceImpl implements UserService {...}
基于标签的事务配置
将上述第3、4步注解的方式转换为标签的形式
5. Spring 整合 MVC 框架
5.1 Java Web 开发回顾
5.2 Spring 整合 MVC 框架
概述
为什么要整合MVC框架
MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式:
- Model(模型)表示应用程序核心(比如数据库记录列表)。
- View(视图)显示数据(数据库记录)。
- Controller(控制器)处理输入(写入数据库记录)。
MVC 框架提供了控制器 Controller 调⽤ Service 处理业务:
- 处理客户端的请求响应,接收请求参数(
request.getParameter("")) - 控制程序的运行流程,调用相关 Service 处理业务
- 将请求处理后的结果响应给客户,这种结果往往是视图解析(渲染):JSP、JSON、Freemarker、Thyemeleaf
可以整合那些MVC框架
struts1、webwork、jsf、struts2、springMVC
核⼼思路
1. 关于 Spring 的 ApplicationContext 工厂的创建和保存
前面章节我们创建业务 Bean 的工厂是在测试方法中通过以下代码实现的:
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
考虑两个问题
-
而在实际 Web 开发过程中应该如何创建工厂呢?将
ClassPathXmlApplicationContext改为WebXmlApplicationContext即可ApplicationContext ctx = new WebXmlApplicationContext("/applicationContext.xml"); -
又如何保证⼯⼚只被创建一次且可以被共⽤呢?
- 在 Web 开发中,
ServletContextListener在ServletContext对象创建的同时,只会被调⽤⼀次,把⼯⼚创建的代码,写在ServletContextListener中,就会保证⼯⼚只被创建一次。 - 在 Web 开发中,作用域有 request、session、ServletContext,可以用来存储对象,而 ServletContext 是唯一的,如果将创建好的工厂通过
ServletContext.setAttribute("xxxx",ctx);存储在 ServletContext 作用域中,就可以被共用了。
- 在 Web 开发中,
Spring 的解决方式
Spring 将上述内容封装进了⼀个 ContextLoaderListener 类,查看源码可以发现 ContextLoaderListener 类实现了ServletContextListener,并在 contextInitialized() 方法中,创建工厂并将其写入 ServletContext 。
if (this.context == null) {
//创建工厂
this.context = createWebApplicationContext(servletContext);}
if (this.context instanceof ConfigurableWebApplicationContext) {
configureAndRefreshWebApplicationContext((ConfigurableWebApplicationContext)this.context, servletContext);}
//创将厂存入servletContext
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
因此,我们只需要在 web.xml 文件中配置ContextLoaderListener , 就可以实现容器启动时,自动创建 ApplicationContext 工厂并放入 ServletContext 。
<listener>
<listenerclass>org.springframework.web.context.ContextLoaderListener</listenerclass>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
2. 关于 Controller 如何调用 Service
通过依赖注入的方式,将 Service Bean 注入到 Controller 中,也就是说,在 Controller 中创建 Service 字段,并提供 Setter 和 Getter 方法。
5.2 Spring与Struts2整合
搭建开发环境
**1. 引入依赖
<dependencies>
<!--Spring 整合 Struts2-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>2.5.25</version>
</dependency>
<!--Spring 组件-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--整合 日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
2. 配置文件
-
applicationContext.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans> -
struts.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts> </struts> -
log4j.properties###### 配置根 log4j.rootLogger = debug,console ###### ?志输出到控制台显示 log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.Target=System.out log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
3. 初始化配置
- Spring:在 web.xml 中配置 ContextLoaderListener
- Struts2:在 web.xml 中配置 Filter
src\main\webapp\WEB-INF\web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!--配置 ContextLoaderListener ,并将 applicationContext.xml 配置到监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--配置 Struts2-->
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
代码编写
1. 开发 Service 对象
com.hihanying.struts2.UserService
package com.hihanying.struts2;
public interface UserService {
public void register ();
}
com.hihanying.struts2.UserServiceImpl
package com.hihanying.struts2;
public class UserServiceImpl implements UserService {
@Override
public void register() {
System.out.println("ServiceImpl.register");
}
}
src\main\resources\applicationContext.xml
<bean id="userService" class="com.hihanying.struts2.UserServiceImpl"/>
2. 开发 Action 对象
com.hihanying.struts2.RegAction
package com.hihanying.struts2;
import com.opensymphony.xwork2.Action;
public class RegAction implements Action {
private UserService userService;
public UserService getUserService() { return userService; }
public void setUserService(UserService userService) { this.userService = userService; }
@Override
public String execute() throws Exception {
userService.register();
return Action.SUCCESS; // 返回事务
}
}
注意:
- 注意 Action 使用的包是:
com.opensymphony.xwork2.Action- RegAction 依赖 UserService,就可以将UserService作为成员变量,并提供 Setter 和Getter 方法,通过 Spring 进行注入。
src\main\resources\applicationContext.xml
<bean id="regAction" class="com.hihanying.struts2.RegAction" scope="prototype">
<property name="userService" ref="userService"/>
</bean>
注意:
- struts2 每一个请求都会创建一个
RegAction, 因此应该指定scope="prototype"控制创建次数
src\main\resources\struts.xml
<struts>
<package name="ssm" extends="struts-default">
<action name="reg" class="regAction">
<result name="success">/index.jsp</result> <!--结果视图-->
</action>
</package>
</struts>
注意:
- action 所对应的 name:表示客户通过 url:name.action 访问此 action,进而找到对应的 Action 类并创建对象为其提供服务
- action 所对应的 class:可以使用 Spring 中创建好的对应对象的 id,不需要写全限定类名
部署
-
点击 Edit Configuration …
-
添加 Tomcat Server -> Local,并编辑 Name 为服务器命名
-
在 Server 标签中配置 Application server,配置 HTTP port
-
在 Deployment 标签中添加对应的 war,并修改访问地址 Application context
-
应用后启动服务器,当后台打印出以下日志信息时表示启动成功。
[2020-10-15 03:28:00,743] Artifact web:war: Artifact is deployed successfully [2020-10-15 03:28:00,743] Artifact web:war: Deploy took 4,109 milliseconds -
测试流程分析
-
通过
http://localhost:8989/web/reg.action发起访问,struts2 通过 reg 对应的 class 找到对应的 Spring Bean,即 regAction。<action name="reg" class="regAction"> <result name="success">/index.jsp</result> <!--结果视图--> </action> -
regAction 对象配置了
scope="prototype"表示请求时创建,Spring 为其注入了 userService<bean id="regAction" class="com.hihanying.struts2.RegAction" scope="prototype"> <property name="userService" ref="userService"/> </bean> -
创建 RegAction 对象后,在重写的方法中执行了
userService.register();,因此控制台输出ServiceImpl.register@Override public String execute() throws Exception { userService.register(); return Action.SUCCESS; } -
运行完成之后,返回 Action.SUCCESS,跳转到结果视图
/index.jsp
-
6. 整合 Spring+Struts2+MyBatis
Spring+Struts2+MyBatis:
- Spring+Struts2:Controller层
- Spring+MyBatis:DAO层
6.1 开发过程
搭建开发环境
1. 引⼊依赖
<dependencies>
<!--Spring 整合 Struts2-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>2.5.25</version>
</dependency>
<!--整合 MyBatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
<!--Spring 组件-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--整合 日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
2. 引入配置文件
-
applicationContext.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans> -
struts.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts> </struts> -
log4j.properties###### 配置根 log4j.rootLogger = debug,console ###### ?志输出到控制台显示 log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.Target=System.out log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -
xxxMapper.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=""> </mapper>
3. 初始化配置
-
src\main\webapp\WEB-INF\web.xml<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> </web-app> -
Spring:在 web.xml 中配置 ContextLoaderListener
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> -
Struts2:在 web.xml 中配置 Filter
<filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
代码编写
1. DAO 层(Spring+Mybatis)
src\main\resources\applicationContext.xml
<!--配置数据源信息,通过 druid 提供-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/javaee?useSSL=false"/>
<property name="name" value="root"/>
<property name="password" value="root"/>
</bean>
<!--配置 SqlSessionFactoryBean 获取 SqlSessionFactory 工厂-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" value="dataSource"/>
<property name="typeAliasesPackage" value="com.hihanying.entity"/>
<property name="mapperLocations">
<list>
<value>classpath:com.hihanying.mapper/*Mapper.xml</value>
</list>
</property>
</bean>
<!--获取 DAO 接口的代理对象-->
<bean id="configure" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="com.hihanying.dao"/>
</bean>
创建实体类:com.hihanying.entity.User
package com.hihanying.entity;
import java.io.Serializable;
public class User implements Serializable {
private Integer id;
private String name;
private String password;
public User() { }
public User(Integer id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
// Setter and Getter
}
在 javaee 数据库中创建表
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) NOT NULL COMMENT '用户名称',
`password` VARCHAR(32) NOT NULL COMMENT '密码',
`sex` CHAR(1) DEFAULT NULL COMMENT '性别',
`address` VARCHAR(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
创建DAO接口类:com.hihanying.dao.UserDAO
package com.hihanying.dao;
import com.hihanying.entity.User;
public interface UserDAO {
public void save(User user);
}
实现 Mapper 文件:src\main\resources\com.hihanying.mapper\UserDAOMapper.xml
<mapper namespace="com.hihanying.dao.UserDAO">
<insert id="save" parameterType="User">
insert into user (name, password) values (#{name},#{password})
</insert>
</mapper>
2. Service 层(Spring AOP)
-
创建目标类:UserService(注入UserDAO)
-
com.hihanying.service.UserService
package com.hihanying.service; import com.hihanying.entity.User; public interface UserService { public void register(User user); } -
com.hihanying.service.UserServiceImpl
package com.hihanying.service; import com.hihanying.dao.UserDAO; import com.hihanying.entity.User; public class UserServiceImpl implements UserService { private UserDAO userDAO; public UserDAO getUserDAO() { return userDAO; } public void setUserDAO(UserDAO userDAO) { this.userDAO = userDAO; } @Override public void register(User user) { userDAO.save(user); } } -
src\main\resources\applicationContext.xml
<!--配置 UserService Bean,注入 UserDAO--> <bean id="userService" class="com.hihanying.service.UserServiceImpl"> <property name="userDAO" ref="userDAO"/> </bean>
-
-
附加功能(事务):DataSourceTransactionManager(注入dataSource)
src\main\resources\applicationContext.xml
<!--配置 DataSourceTransactionManager 添加事务--> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> -
切入点+事务属性(注解)
com.hihanying.service.UserServiceImpl
import org.springframework.transaction.annotation.Transactional; @Transactional public class UserServiceImpl implements UserService {...}- 由于 register 方法是增删改操作,所以事务属性使用默认值即可。
-
组装切面
src\main\resources\applicationContext.xml
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>- 注意:
xmlns:tx="http://www.springframework.org/schema/tx"
- 注意:
3. Controller 层(Spring+Struts2)
-
开发控制器 implements Action 注⼊Service
com.hihanying.action.RegAction
package com.hihanying.action; import com.hihanying.entity.User; import com.hihanying.service.UserService; import com.opensymphony.xwork2.Action; public class RegAction implements Action { private User user; private UserService userService; public User getUser() { return user; } public void setUser(User user) { this.user = user; } public UserService getUserService() { return userService; } public void setUserService(UserService userService) { this.userService = userService; } @Override public String execute() throws Exception { userService.register(user); return Action.SUCCESS; } } -
Spring的配置⽂件:src\main\resources\applicationContext.xml
<bean id="reg" class="com.hihanying.action.RegAction" scope="prototype"> <property name="userService" ref="userService"/> </bean> -
struts.xml
<struts> <package name="ssm" extends="struts-default"> <action name="reg" class="regAction"> <result name="success">/regOK.jsp</result> <!--结果视图--> </action> </package> </struts> -
注册页面:src\main\webapp\reg.jsp
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>注册</title> </head> <body> <form method="post" action="${pageContext.request.contextPath}/reg.action"> UserName<input type="text" name="user.name"/><br/> PassWord<input type="password" name="user.password"/><br/> <input type="submit" value="reg"> </form> </body> </html> -
成功跳转页面:src\main\webapp\regOK.jsp
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>成功</title> </head> <body> <h1>regOK</h1> </body> </html>
6.2 Spring开发过程中多配置⽂件的处理
问题解决
Spring 会根据需要,把配置信息分⻔别类的放置在多个配置⽂件中,便于后续的管理及维护。虽然提供了多个配置⽂件,但是后续应⽤的过程中,还要进⾏整合。
- DAO:applicationContext-dao.xml
- Service:applicationContext-service.xml
- Action:applicationContext-action.xml
整合方式
-
通配符⽅式
-
测试环境
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext-*.xml"); -
web环境
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext-*.xml</param-value> </context-param>
-
-
import 标签:在 applicationContext.xml 中整合其他配置内容
<import resource="applicationContext-dao.xml " /> <import resource="applicationContext-service.xml " /> <import resource="applicationContext-action.xml " />