title: 11.搭建Sql执行器
tag: 笔记 手写SSM ORM
实现了ORM的主要功能后,我们就可以与之前实现的IOC容器实现整合了。
Spring整合Mybatis的思路
最近在仿照一个Spring框架时想要再写一个Mybatis框架整合到里面去,因此对Mybatis-Spring的源码进行了一些粗略的了解。
Spring整合Mybatis大概需要解决一下几个问题:
-
Spring只能扫描到Mapper的接口,如何将这个接口替换为Mapper代理类?
-
如何将Mybatis动态代理生成的Mapper注册到IOC容器中使用?
-
获取Mapper需要构建SqlSession,如何构建SqlSession,并添加到IOC容器中?
分析
我们先看SSM框架中用户如何使用注解来整合Spring
和Mybatis
,用户需要提供一个Mybatis的配置类:
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.duan.domian");
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.duan.dao");
return mapperScannerConfigurer;
}
}
这个配置类将SqlSessionFactoryBean
和MapperScannerConfigurer
作为Bean加入到了IOC容器中。
从名字可以看出:
SqlSessionFactoryBean
:Spring用于构建SqlSession的工厂方法。MapperScannerConfigurer
:Spring配置Mapper扫描并进行扫描的类。
因此这里我们可以知道**Spring
用FacotryBean
的方式将Mapper
和SqlSession
注册到IOC容器中。**
但这里还有一个问题,就是如何将Spring扫描到的接口替换为动态代理生成的Mapper呢?
我们再进入到MapperScannerConfigurer
中可以看到该类实现了一个接口,BeanDefinitionRegistryPostProcessor
,那么我们知道实现了XXXXPostProcessor
的Bean都是Spring提供的一些扩展点,会在Spring加载的一定时期进行调用,那么BeanDefinitionRegistryPostProcessor
是在何时调用的呢?
BeanDefinitionRegistryPostProcessor 是 Spring 框架中的一个接口,它允许开发者在 Spring 容器完成 BeanDefinition 的注册但尚未创建任何 Bean 实例之前进行干预操作。
所以我们可以知道Spring是在BeanDefinition
的注册完成时去扫描Mapper所在的接口,并通过某种方式将这些接口替换成了动态代理生成的Mapper。
下面我们将去源码中简略分析一下这个流程。
Spring-Mybatis源码分析
个人认为源码中最重要的类即下面几个
-
SqlSessionFactoryBean:构建SqlSession的工厂
-
MapperScannerConfigurer和ClassPathMapperScanner:Mapper接口的扫描
-
MapperFactoryBean:构建Mapper的工厂
SqlSessionFactoryBean
首先我们可以看到这个类实现这两个接口:
- FactoryBean:说明该类是一个工厂类,拿到的Bean是
getObject
方法的返回值。 - InitializingBean:Spring会在Bean初始化完成后调用
afterPropertiesSet
方法
因此该类我们主要看两个getObject
和afterPropertiesSet
即可大致了解该类:
getObject:
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
this.afterPropertiesSet();
}
return this.sqlSessionFactory;
}
这个工厂方法会返回一个SqlSessionFactory
,SqlSessionFactory
用于创建SqlSession
。
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.dataSource, "Property 'dataSource' is required");
Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = this.buildSqlSessionFactory();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
XMLConfigBuilder xmlConfigBuilder = null;
Configuration configuration;
if (this.configuration != null) {
configuration = this.configuration;
if (configuration.getVariables() == null) {
configuration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
configuration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property `configuration` or 'configLocation' not specified, using default MyBatis Configuration");
。。。。。。省略
}
在afterPropertiesSet
会调用buildSqlSessionFactory
方法来进行SqlSessionFactory
的构建。而SqlSessionFactory
的构建需要Configuration
类,Configuration
基本包含了SqlSession的全部信息。因此构建SqlSessionFactory
实际上就是构建Configuration
。
因此buildSqlSessionFactory
方法的代码中基本都是构建Configuration
的逻辑,那么构建Configuration
的信息从哪来呢?也就是该类是如何构建Configuration
的呢?
在我粗略的看了一下代码,我个人觉得它是按照如下方式构造的:
- 如果已经存在
Configuration
,则使用这个Configuration
。 - 若设置了
configLocation
属性,则会从这个XML构建Configuration
。(mybatis本就支持XML构建) - 接下来会根据用户自定义的属性就行设置。
这些自定义的属性都在该类的属性中,我们可以在将该类加入到IOC容器时进行设置。
private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);
private Resource configLocation;
private Configuration configuration;
private Resource[] mapperLocations;
private DataSource dataSource;
private TransactionFactory transactionFactory;
private Properties configurationProperties;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private SqlSessionFactory sqlSessionFactory;
........
总结:该类用于构建SqlSession,拿到Mybatis需要的各种配置。
MapperScannerConfigurer和ClassPathMapperScanner
我们先看MapperScannerConfigurer
类,该类实现了接口:
- BeanDefinitionRegistryPostProcessor :之前介绍过该接口,是在BeanDefinition注册完成后调用
postProcessBeanDefinitionRegistry
方法。 - InitializingBean:Spring会在Bean初始化完成后调用该接口的
afterPropertiesSet
方法
所以我们同样从实现接口的两个方法postProcessBeanDefinitionRegistry
方法和afterPropertiesSet
方法来了解该类的作用:
- afterPropertiesSet方法:
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.basePackage, "Property 'basePackage' is required");
}
在该Bean初始化完成后,该类只是检测一下是否传入了扫描包basePackage
,否则报错。
- postProcessBeanDefinitionRegistry方法:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
this.processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n"));
}
这个方法配置了ClassPathMapperScanner
类,并使用该类扫描了basePackage
。
因此,接下来我们继续看ClassPathMapperScanner
类。
我们首先看到该类继承了Spring的提供的扫描器ClassPathBeanDefinitionScanner
,它主要用于扫描类路径(ClassPath)下的指定包及其子包中符合条件的类,并将这些类注册为 Spring 容器中的 Bean。
既然ClassPathBeanDefinitionScanner
是用于扫描BeanDefinition
的扫描器,那么ClassPathMapperScanner
就是用于扫描Mapper
的扫描器了。
ClassPathMapperScanner相比与父类扩展了许多许多MapperScannerConfigurer
提供的配置信息,其中最重要的应当是该类重写了父类的doScan方法:
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> {
return "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.";
});
} else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
它首先调用了父类的doScan方法拿到扫描到的beanDefinitions
之后又调用了processBeanDefinitions
方法处理beanDefinitions。
因此我们可以再继续看processBeanDefinitions
方法,但该方法较长,我们主要看关键的地方:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
.......
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
definition.setAttribute("factoryBeanObjectType", beanClassName);
.......
}
我们可以看到这里将definition的BeanClass
属性修改为了mapperFactoryBeanClass
private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
,该属性的值默认为MapperFactoryBean.class
。我们知道这个类是一个工厂类,并且又给这个工厂添加了一个参数为添加了一个参数为beanClassName
,这个参数的值就是原本的BeanDefinition
的beanClassName
,也就是接口的beanClassName
。而在这个MapperFactoryBean
中就可以为这个接口生成代理对象。
总结:Spring-Mybatis
通过配置Mapper
扫描器来扫描到Mapper
接口,并在扫描完成后将这些接口的BeanClass
替换为MapperFactoryBean
来生成Mapper代理对象。
MapperFactoryBean
最后我们来了解生成Mapper
代理的工厂类MapperFactoryBean
。
- 该类继承了
DaoSupport
抽象类:
public abstract class DaoSupport implements InitializingBean {
protected final Log logger = LogFactory.getLog(this.getClass());
public DaoSupport() {
}
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
this.checkDaoConfig();
try {
this.initDao();
} catch (Exception var2) {
throw new BeanInitializationException("Initialization of DAO failed", var2);
}
}
protected abstract void checkDaoConfig() throws IllegalArgumentException;
protected void initDao() throws Exception {
}
}
我们可以看到该类使用了模板方法的设计模式,它在afterPropertiesSet
中调用了checkDaoConfig
和initDao
。之前说过afterPropertiesSet
方法会在Bean初始化后调用。因此我们只需要看MapperFactoryBean
是如何重写checkDaoConfig
和initDao
就可以知道这个类做了什么。
通过源码可以看到MapperFactoryBean
仅仅重写了checkDaoConfig
因此我们只需要看这个方法即可:
protected void checkDaoConfig() {
super.checkDaoConfig();
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception var6) {
this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
throw new IllegalArgumentException(var6);
} finally {
ErrorContext.instance().reset();
}
}
}
该类首先检测一些需要的属性(SqlSession
,mapperInteface
)是否为空,检测完成后拿到SqlSession
中的Configration
调用addMapper
将该接口注册到Configration
中。
总结
支持我们介绍完了Spring-Mybatis的整合原理大致为以下几步:
- SqlSessionFactoryBean拿到配置信息后构建SqlSession到IOC容器。
- ClassPathMapperScanner拿到扫描的配置信息后扫描Mapper接口,并将这个Mapper接口替换为MapperFactoryBean生成代理生成的Mapper。