02-Spring 与 MyBatis 集成
与 Spring 集成
官方提供了一个集成实现 mybatis-spring,将 MyBatis 集成到 Spring 框架中。
Configuration 的配置或者说 SqlSessionFactory 的初始化,交由 SqlSessionFactoryBean 来完成。
首先,看下 SqlSessionFactoryBean 的类签名:
java
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ContextRefreshedEvent> {}
首先,它是一个 FactoryBean,说明当 SqlSessionFactoryBean 注入到容器中,并且当我们向容器请求 SqlSessionFactory,会调用 FactoryBean#getObject 方法,返回一个对象。
其次,它实现了 InitializingBean 接口,即 SqlSessionFactoryBean 初始化后,会回调 InitializingBean#afterPropertiesSet 方法。
最后,它实现了 ApplicationListener,即它会监听容器的生命周期事件,并做相应的动作。
SqlSessionFactoryBean 会根据当前具体情况,创建一个 Configuration 对象,并调用 SqlSessionFactoryBuilder#build 方法,创建一个 SqlSessionFactory 对象。
当调用 getObject 方法时,返回的就是创建好的 SqlSessionFactory 对象。
仅仅使用 SqlSessionFactoryBean 是没办法扫描到 Mapper 接口和定义了 Mapped SQL Statement 的 XML 文件的。
要想 MyBatis 正常工作,通常需要在 SqlSessionFactoryBean 中指定 DataSource 和 mapperLocations,例如:
java
@Value("classpath*:mappers/**/*.xml") // XML 文件所在的路径,Ant 格式的路径
Resource[] mapperLocations;
@Bean
public DataSource dataSource(){
// 数据库链接
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.addMapperLocations(mapperLocations);
return factoryBean;
}
有了 SqlSessionFactoryBean,在应用中就可以通过 @Autowired
来获取 SqlSessionFactory 实例对象,然后通过 MyBatis API 来进行数据库操作。
mybatis-spring 做了更进一步的工作,提供了三个便利的接口:
- SqlSessionTemplate
- SqlSessionDaoSupport
- MapperFactoryBean extends SqlSessionDaoSupport
SqlSessionTemplate 是对 SqlSession 的封装,提供了线程安全的访问方法。
而且,它还实现了 SqlSession 接口,所以应用代码中任何使用 SqlSession 的地方,都可以替换为 SqlSessionTemplate。
java
@Bean
public SqlSession sqlSession(@Autowired SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
按照上述方式将 SqlSessionTemplate 注入到容器中,所有需要 SqlSession 的地方,都可以通过 @Autowired Sqlsession session;
来获得这个线程安全的实现。
SqlSessionDaoSupport 是一个工具类,只需要继承这个类,就能在类中使用 getSqlSession().xxx
形式来进行数据库操作。
为了能够正常获得数据库会话,需要传递 SqlSessionFactory 到该类中,如下所示:
java
@Bean
public StudentDao studentDao(@Autowired SqlSessionFactory sqlSessionFactory) {
StudentDao dao = new StudentDao();
dao.setSqlSessionFactory(sqlSessionFactory);
return dao;
}
public static class StudentDao extends SqlSessionDaoSupport implements StudentMapper {
@Override
public Student selectByNo(String sno) {
return getSqlSession().selectOne("self.samson.example.mybatis.StudentMapper.selectByNo", sno);
}
}
MapperFactoryBean 是 SqlSessionDaoSupport 的一个子类实现,主要是优化使用方式。
java
@Bean
public MapperFactoryBean<StudentMapper> userMapper() throws Exception {
MapperFactoryBean<StudentMapper> factoryBean = new MapperFactoryBean<>(StudentMapper.class);
factoryBean.setSqlSessionFactory(sqlSessionFactory());
return factoryBean;
}
MapperFactoryBean 实现了 Spring 中的 FactoryBean 接口,它对 getObject 方法的实现如下:
java
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface); // this.mapperInterface 是创建时传入的类,例如上面的 StudentMapper.class
}
所以,这种方式允许你在引用代码中直接使用 @Autowired StudentMapper mapper;
这种方式直接获得特定类型的 Mapper 对象。
Mapper 发现
进一步优化使用方式,mybatis-spring 从 1.2.0 版本开始,提供了自动 Mapper 发现机制,即 @MapperScan
。
自动发现方式有两类,共三种:
-
XML 文件中,使用
<mybatis:scan/>
标签 -
Java 文件中,使用
@MapperScan
注解 -
Java 配置中,向容器中注入
MapperScannerConfigurer
对象
这里仅介绍 @MapperScan
方式的实现流程。
@MapperScan
本质上也是通过向容器中注入 MapperScannerConfigurer 对象来实现的。
<mybatis:scan/>
与 @MapperScan
本质上也一样。
使用方式:
java
@MapperScan // 默认使用当前类所在的包
//或
@MapperScan(basePackages = {"self.samson.example.spring"})
@MapperScan
通过 @Import(MapperScannerRegistrar.class)
引入了 MapperScannerRegistrar。
MapperScannerRegistrar 是一个 ImportBeanDefinitionRegistrar,会向容器中注入特定的 BeanDefinition。
java
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// 省略配置过程
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
向容器中注入 MapperScannerConfigurer 的目的是:
MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor 实现。
在它的 postProcessBeanDefinitionRegistry 方法中,会通过 ClassPathMapperScanner 扫描 basePackages 中配置的包信息,并将扫描到的 Mapper 加入到容器中,如下所示。
java
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 省略其他配置
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
这里需要补充一下 Spring 容器刷新的过程,能够更好地理解 MapperScannerConfigurer 的流程。
MapperScannerConfigurer 作为一个 BeanDefinitionRegistryPostProcessor 实现,在 1 时被调用。
此时,它使用 ClassPathMapperScanner 扫描所有的候选类,然后添加到容器中。
此时添加的并不是对象实例,仅仅是 BeanDefinition。
实例是在步骤 2 时创建的。
SqlSessionFactoryBean 中的 mapperLocations 方式与 @MapperScan 方式的不同
在使用过程中,发现下述两种方式的细微不同之处:
-
方式一,使用 SqlSessionFactoryBean 中的 mapperLocations 方式指定 XML 文件位置,会自动解析对应的 XML 文件,并将 namespace 中的 Mapper 添加到 MyBatis 框架中。
应用层在使用时,可以通过
getMapper(xx.class)
方式获得。 -
方式二,使用 @MapperScan 方式指定 Mapper 所处的类,会自动解析 Mapper 接口,添加到 MyBatis 框架中。
应用层在使用时,可以通过
getMapper(xx.class)
方式获得,或使用 Spring 提供的 @Autowired 使用。
通过 SqlSession#getMapper 获得的对象,都是 MapperProxy 对象。
它内部都包含了对 SqlSession、Mapper 接口类的引用。
方式一、二的第一个不同之处是,方式一返回的对象中的 SqlSession 是 MyBatis 中原生的 DefaultSqlSession 实现;
方式二返回的对象中,SqlSession 是 mybatis-spring 提供的 SqlSessionTemplate 实现。
另外一个不同之处,方式一不能通过容器来获得具体的 Mapper 实例。
造成这种差异的原因如下:
SqlSessionFactoryBean 通过 XMLMapperBuilder 来解析 XML 文件,并将 Mapper 注册到 MapperRegistry 中。
其实并没有将 Mapper 注册到 Spring 容器中。
@MapperScan 通过 ClassPathMapperScanner 将 Mapper 的定义添加到 Spring 容器中,且 scope 为 singleton。
Spring 容器在刷新时,会初始化 Mapper 单例,并添加到容器中。
ClassPathMapperScanner 向容器中添加 BeanDefinition 的过程如下:
java
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); // 找到所有的接口,并创建 BeanDefinition
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions); // 修改 BeanDefinition
}
return beanDefinitions;
}
ClassPathMapperScanner 在 processBeanDefinitions 中,把 BeanDefinition 中的 beanClass 设置为 MapperFactoryBean.class。
java
definition.setBeanClass(this.mapperFactoryBeanClass);
并且会尝试设置 MapperFactoryBean 中的 sqlSessionFactory。
如果 MapperScannerConfigurer 中没有配置 sqlSessionFactory,这里就会跳过,并设置 BeanDefinition 中的自动注入方式为按类型注入:
java
if (!explicitFactoryUsed) {
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
在 Spring 初始化单例 Bean 时,会把 ClassPathMapperScanner 处理的 BeanDefinition 进行实例化。
此时,根据 java.beans.Introspector
中 writeMethod
确定未满足的依赖,并尝试设置:
java
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory); // 这里其实就是简单的 new SqlSessionTemplate(sqlSessionFactory);
}
}
然后,MapperFactoryBean 又实现了 InitializingBean 接口,所有会在实例化后调用它的 afterPropertiesSet:
java
@Override
protected void checkDaoConfig() {
super.checkDaoConfig(); // 检查 sqlSessionTemplate 非空
notNull(this.mapperInterface, "Property 'mapperInterface' is required"); // 检查 mapperInterface 非空
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { // addToConifg 在 ClassPathMapperScanner 中设置为 true
try {
configuration.addMapper(this.mapperInterface); // 注册到 MyBatis 框架中
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
到这里位置,Spring 容器中的 Bean 与 MyBatis 中的 Mapper 实例就映射起来了。
在应用层中,既可以使用 SqlSession#getMapper 获取指定 Mapper 对象,也可以通过 @Autowired 直接注入指定的对象。