前文介绍了Spring最基本的功能,Spring是一个DI(依赖注入)容器和AOP(面向切面)容器,但这仅仅是手段,远不是目标,Spring的目标是整合与简化其他Java框架的开发,通过DI管理其他框架和组件,利用AOP分离繁复部分,使得复杂问题透明化。下图罗列了Spring框架的部分功能,不难看出,Spring感觉就像北冥神功:海纳百川,皆为我用。
这里我们打算使用Spring整合MyBatis,让MyBatis变得前所未有的简洁好用。
1 使用SqlSessionTemplate实现MyBatis的整合与简化
前文提到,MyBatis有两种使用方式:一是“命名查询”方式,二是“Mapper接口”方式,这里先介绍第一种,这种方式下,DAO是需要编码实现的。
(1)导入所需依赖(jar包)
要实现Spring整合MyBatis,需要添加以下依赖:
1)需要使用”spring-orm”组件,提供模板模式和事务支持;
2)需要使用“mybatis-spring”组件,该组件由MyBatis官方提供;
3)需要数据源“commons-dbcp”组件,该组件负责提供连接池,提供JDBC连接并提高数据库的连接性能。
具体Maven依赖如下(注意新添加的红色部分)。
<!-- mysql jdbc -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.8</version>
</dependency>
<!-- dbcp 数据源(连接池),必须 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<!-- MyBatis 核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.5</version>
</dependency>
<!-- MyBatis与Spring整合包 ,必须,整合Spring的关键 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.1</version>
</dependency>
<!-- Junit测试,可选,仅用于单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<!-- Spring DI容器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
<!-- Spring ORM 数据访问组件 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
(2)配置SqlSessionFactory对象
在Spring的beans配置文件中配置DBCP数据源 和 MyBatis的Session工厂对象
需要注意的是:
1)“dataSource”对象关键要配置JDBC所需要的4个连接常量
2)“sqlSessionFactory”对象需要配置:“dataSource”属性来获取连接;“mapperLocations”属性来指定Mapper XML文件的位置,它会做统一扫描;“typeAliasesPackage”属性指定数据实体的默认包名,以使得mapper中可以直接用实体类的简称。
<?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"
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">
<!-- 配置DBCP数据源(连接池) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/MyCinema" />
<property name="username" value="root" />
<property name="password" value="1234" />
<!-- 初始化连接大小 -->
<property name="initialSize" value="0"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="20"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="20"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="1"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="60000"></property>
</bean>
<!-- 配置MyBatis的SessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:mapper/*.xml" />
<property name="typeAliasesPackage" value="mycinema.entity" />
</bean>
</beans>
使用Spring配置SqlSessionFactory后,理论上MyBatis的原生配置文件mybatis.xml就可以不要了。但如果有些复杂的MyBatis配置需要保留mybatis.xml的话,我们也可以使用如下写法。
<!-- 配置MyBatis的SessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis.xml" />
<property name="mapperLocations" value="classpath:mapper/*.xml" />
<property name="typeAliasesPackage" value="mycinema.entity" />
</bean>
(3)配置SqlSessionTemplate对象
SqlSessionTemplate对象负责简化原生MyBatis的SqlSession操作,有了它,我们不需要关心SqlSession的open和close,甚至是事务的commit,我们只需要执行所需操作即可。
<!-- 配置SqlSessionTemplate对象 -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
(4)为DAO的依赖注入SqlSessionTemplate并使用它
DAO对象的实现如下
public class CategoryMapperImpl implements CategoryMapper {
private SqlSessionTemplate sqlSessionTemplate;
//用于依赖注入
public void setSqlSessionTemplate(SqlSessionTemplate value){
this.sqlSessionTemplate = value;
}
public Category fetchById(int id) {
return sqlSessionTemplate.selectOne("mycinema.dao.CategoryMapper.fetchById", id);
}
public void add(Category c) {
sqlSessionTemplate.insert("mycinema.dao.CategoryMapper.add", c);
}
}
为DAO配置sqlSessionTemplate
<!-- 吧sqlSessionTemplate对象依赖注入到DAO对象 -->
<bean id="categoryMapper" class="mycinema.dao.impl.CategoryMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSessionTemplate" />
</bean>
2 使用Mapper接口动态代理的方式实现MyBatis的整合与简化
上述方式虽然已经对mybatis进行了简化,但“命名查询”的方式属于弱类型编程,容易出错,而且必须编写Mapper接口的实现对象有点多此一举,为此,应该还有更简单的方式。Spring可以通过AOP技术,为Mapper接口直接生成动态代理对象,我们根本不需要为DAO层提供实现,直接把Spring代理的Mapper注入给业务就好了。
(1)配置MapperScannerConfigurer,使用Spring动态代理Mapper接口
删除掉我们实现的CategoryMapperImpl实现类和SqlSessionTemplate对象配置,这时,我们所做的Mapper单元测试必然会报错。
这时只需要添加MapperScannerConfigurer,Spring就能为我们自动生成Mapper对象了。其中“basePackage”指定了Mapper接口所在包位置;“sqlSessionFactoryBeanName”需要指定sqlSessionFactory的bean的id。
<!-- 扫描DAO接口所在包名,Spring会自动代理生成其下的接口实现类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mycinema.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
配置完成后,我们的Mapper单元测试又可以通过了。
(2)继续使用以来注入实现业务对象
业务实现类
public class CategoryBizImpl implements CategoryBiz{
private CategoryMapper categoryMapper;
public void setCategoryMapper(CategoryMapper categoryMapper) {
this.categoryMapper = categoryMapper;
}
public Category fetchById(int id) {
return categoryMapper.fetchById(id);
}
public void add(Category c) {
categoryMapper.add(c);
}
}
依赖注入配置
<!-- 业务对象 -->
<bean id="categoryBiz" class="mycinema.biz.impl.CategoryBizImpl">
<property name="categoryMapper" ref="categoryMapper" />
</bean>
3 声明式事务管理
在实际的数据访问开发中,在增删改中引入事务管理是非常必要的,只有在事务管理下,相关的数据才能保持一致性,否则有可能产生重大业务错误。但在DAO模式下,增删改数据往往是单表操作,而事务则常常包含多个DAO对象和方法,需要确保多个DAO方法在同一连接(Connection)和事务(Transaction)下执行变得非常复杂。
Spring利用AOP切面技术,为数据访问提供了基于业务层(一个业务方法往往代表一个事务,可以包含多个DAO方法)的声明式事务管理,完全透明地解决了事务难题。所谓声明式的事务管理:即只需配置,无须编程,利用AOP技术,把事务代码横切织入到数据访问代码中。
Spring针对不同的数据访问方式,提供了不同的事务管理器,如下所示:
3.1 为MyBatis配置声明式事务
这里讨论的是可以为MyBatis所使用的DataSourceTransactionManager。
(1)导入事务AOP所需要的依赖。
这里需要用到AOP和切面描述,因此需要在原来基础上添加Spring的切面依赖。
<!-- Spring 切面,可用于配置事务切面 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
(2)在Spring配置文件的文档声明中加入aop和tx(事务)配置声明。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
……
</beans>
(3)配置MyBatis/JDBC事务管理器。
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
4)配置AOP事务通知。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="find*" read-only="true" timeout="60"/>
<tx:method name="fetch*" read-only="true" timeout="60"/>
<tx:method name="add*" propagation="REQUIRED" timeout="60"/>
<tx:method name="update*" propagation="REQUIRED" timeout="60"/>
<tx:method name="delete*" propagation="REQUIRED" timeout="60"/>
<tx:method name="register" propagation="REQUIRED" timeout="60"/>
</tx:attributes>
</tx:advice>
5)配置AOP切面(通知+切入点)。
<aop:config>
<aop:advisor
pointcut="execution(* mycinema.biz..*.*(..))"
advice-ref="txAdvice"/>
</aop:config>
2.2 理解事务参数
配置Spring声明式事务管理时,<tx:method>配置元素除用于声明业务方法名外,还提供了5大属性用于控制事务细节:rollback-for、propagation、isolaction、read-only和timeout。合理的设置这5个属性,对于正确控制事务处理细节有重要意义。
(1)timeout(设置超时)
为了确保不会造成死锁或长期等待过分增加数据库负担,可以为事务提供一个超时时间,让事务在超过设定的秒数后自动回滚事务。
(2)rollback-for(设置触发回滚的异常类型)
一般情况下,在事务范围中发生异常需要回滚事务。rollback-for用于设置异常类型,当该异常(以及其子类异常)抛出时事务回滚。若没有特别指出,默认值是RuntimeException。
(3)read-only(只读)。
若一个事务只需要对数据库执行读操作,那就应该把事务声明为只读,让Spring对该事务的执行实行优化策略。
(4)propagation(传播行为)。
用于声明执行该业务方法时是否启用当前事务,还是启动一个新的事务。
(5)isolaction(隔离级别)。
多个事务同时运行操作同一批数据会导致并发,有可能会导致以下问题:
- 脏读(Dirty Reads):一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。
- 幻读(Phantom Reads):事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。
- 不可重复读(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果。
理想状态下,事务之间应该是完全相互隔离的,但完全隔离会影响性能,因为隔离需要锁定数据库中的记录。在实际中,并非所有应用都要求事务完全隔离,因此Spring提供了若干个隔离级别,以提高事务管理的灵活度。
4 使用注解配置
4.1 使用注解配置依赖注入和事务管理
使用XML配置大量的Spring Bean确实比较繁琐,幸好Spring提供了很好的注解配置机制。在实际使用中,我们通常会用XML配置不是自己编写的Bean(例如各种框架已经编写好的对象),而使用注解配置自己所编写的Bean。
下面演示使用注解配置Spring和MyBatis的整合与实务管理。
(1)在spring配置文件中开启注解配置
<?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">
<!-- 配置DBCP数据源(连接池) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/MyCinema" />
<property name="username" value="root" />
<property name="password" value="1234" />
</bean>
<!-- 配置MyBatis的SessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:mapper/*.xml" />
<property name="typeAliasesPackage" value="mycinema.entity" />
</bean>
<!-- 扫描DAO接口所在包名,Spring会自动代理生成其下的接口实现类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mycinema.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 包注解扫描 -->
<context:component-scan base-package="mycinema" />
<!-- 标记用注解驱动事务管理 -->
<tx:annotation-driven/>
</beans>
(2)业务层中使用注解配置依赖注入和事务管理
在业务代码中可以使用@Transactional注解指定某一方法或整个业务对象的事务规则,@Transactional注解中同样可以配置“readOnly”、“propagation”,“isolation”、“timeout”、“rollbackFor”这5大事务属性。
@Service
@Transactional(readOnly=true,timeout=60)
public class CategoryBizImpl implements CategoryBiz{
@Autowired
private CategoryMapper categoryMapper;
//使用类头上的默认事务配置
public Category fetchById(int id) {
return categoryMapper.fetchById(id);
}
//覆盖类头上配置的事务规则
@Transactional(propagation=Propagation.REQUIRED, timeout=60)
public void add(Category c) {
categoryMapper.add(c);
}
}
4.2 使用注解驱动单元测试
为了方便单元测试Spring同样提供了单元测试组件“spring-test”,该组件可以让我们在单元测试中使用依赖注入。
(1)导入spring-test组件依赖
<!-- Spring 测试 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
2)在单元测试中使用依赖注入
“spring-test”组件提供的@ContextConfiguration注解用于指定Spring配置信息的位置;而@RunWith注解用于指定Spring测试的运行加载器,与JUnit4配对的是SpringJUnit4ClassRunner类。
配置这两个注解后,我们就能使用@Autowired直接注入被测试的对象了。
@ContextConfiguration("classpath:spring-beans.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class CategoryMapperTest {
@Autowired
private CategoryMapper target;
@Test
public void testFetchById() {
assertEquals("喜剧", target.fetchById(1).getName());
}
@Test
public void testAdd() {
Category c = new Category(0,"Test");
target.add(c);
System.out.println(c.getId());
}
}
3)带回滚式的单元测试
对DAO执行单元测试的一个大麻烦是:一旦执行过增删改方法后,数据库就会“脏”掉了,里面的数据就不是原来的样子了,者对于我们判断查询方法是否正确造成了影响。如果DAO单元测试之后,数据能够像事务一样被回滚,那就太好了。
Spring为我们提供了这样的方法,只要在测试代码上加上“@Transactional”和“@Rollback”注解,我们就能使用带回滚功能的DAO测试。每个测试方法在直接结束之后,会把事务回滚掉,而不是提交。数据库就不会被弄“脏”了。
@ContextConfiguration("classpath:spring-beans.xml")
@RunWith(SpringJUnit4ClassRunner.class)
@Transactional
@Rollback
public class CategoryMapperTest {
@Autowired
private CategoryMapper target;
@Test
public void testFetchById() {
assertEquals("喜剧", target.fetchById(1).getName());
}
@Test
public void testAdd() {
Category c = new Category(0,"Test");
target.add(c);
System.out.println(c.getId());
}
}