前言
当我们使用Mybatis调用Mapper接口中的方法的时候,可以确定Mybatis已经把SQL语句给拿到了,否则后续的步骤就无法进行了。那么Mybatis是在什么时候初始化Mapper并且拿到其中的内容的呢?本篇博客主要从源码上探究这个问题,本篇可能会涉及到【Mybatis-Spring源码分析(一) MapperScan】里面的部分知识点。更多Spring内容进入【Spring解读系列目录】。
初始化流程
当Mybatis
实例化一个MapperFactoryBean
的时候,Mybatis
会首先执行一个初始化。初始化做的事情就是把Mapper
接口里面的所有方法的注解拿出来,放到一个Map
中,名字叫做Map<String, MappedStatement> mappedStatements
。流程图大概就是下面的样子,这个有兴趣的同学可以跟着断点看下,笔者把断点的位置给大家放出来。
位置:org.apache.ibatis.session.Configuration#addMappedStatement
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms); //在这一行上断点
}
怎么做到的初始化
通过上面的流程可以看到,SQL
语句的初始化是Mapper
接口实例化以后立刻就被放到map
中去了。那么问题就来了:一个类怎么样才能在初始化的时候做一些赋值或者拿到一些东西呢?之前笔者的帖子写过我们可以用@PostConstract
或者实现BeanPostProcessor
接口,这些都可以。但是Mybatis
不是这么做的,它用的是另外一个方法afterPropertiesSet()
,这个方法属于InitializingBean
接口,用法示例如下。
@Service
public class CityService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("CityService.afterPropertiesSet()") ;
}
}
InitializingBean
这个接口属于Spring框架,它的作用就是在Bean初始化的时候进行一些操作。从名字上看afterPropertiesSet
,就是说在所有的属性和依赖都准备好以后再执行。
Mybatis初始化流程
之前的博客已经详细的说过,所有Mapper接口都会被转化为MapperfactoryBean
。所以Mybatis
初始化的流程是这样的。由于MapperfactoryBean
继承自SqlSessionDaoSupport
,而SqlSessionDaoSupport
继承自DaoSupport
,DaoSupport
又实现了InitializingBean
。
SqlSessionDaoSupport
是Mybatis
提供的类,在里面重写了checkDaoConfig()
方法,由于SqlSessionDaoSupport
是抽象类,因此这个方法只能由它的子类去实现。也就是说最终还是由MapperfactoryBean#checkDaoConfig()
实现的。
那么这个逻辑是怎么走的呢?当MapperfactoryBean
被实例化的时候,由于最终实现了InitializingBean
接口,因此会先调用DaoSupport#afterPropertiesSet()
方法:
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
checkDaoConfig();
//。。。。暂时无关,略
}
然后调用其本身的checkDaoConfig()
方法,由于这是一个抽象方法:
protected abstract void checkDaoConfig() throws IllegalArgumentException;
因此又转给了它的子类SqlSessionDaoSupport#checkDaoConfig()
去执行,但是SqlSessionDaoSupport
本身就是抽象类:
public abstract class SqlSessionDaoSupport extends DaoSupport
所以就必须让MapperfactoryBean#checkDaoConfig()
实现具体的逻辑:
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
//最终把Mapper接口添加到里面,并在这里面初始化Mapper接口
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
继续进入Configuration#addMapper()
里面:
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
最终在MapperRegistry#addMapper()
里面初始化这些类:
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//put进去,并且给Mapper的type给上对应的value,也就是代理
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
然后再用parser.parse();
去解析这些注解,到此Mybatis
的初始化流程结束。
总结
写了这么关于Mybatis
的博客以后,终于可以总结一下了。Mybatis-Spring
初始化分为在Spring初始化之前和Spring初始化之后。
Spring初始化之前:
通过分析源码可以得出@MapperScan
主要做了三个工作:
- 扫描出所有的
Mapper
接口所对应的BeanDefinition
。 - 把
Mapper
接口转换为FactoryBean
,或者说MapperFactoryBean
的BeanDefinition
。 - 为
BeanDefinition
添加一个构造方法的值,并使用这个Class
,在Spring示例化过程中根据这个Class返回相对应的代理对象。
Spring初始化之后:
Mybatis
主要通过Spring的初始化方法扩展点来完成对Mapper
接口信息的初始化,比如SQL
语句的初始化等等。其实就是利用MapperFactoryBean
实现了InitializingBean
接口,然后使用AfterPropertiesSet()
方法机制进行初始化。由于MapperFactoryBean
其实就是一个Mapper
,所以又可以理解为其就是一个Mapper
信息的缓存,因为是被代理的并没有真正的代码。当所有的Mapper
都被解析完毕以后,再缓存到一个Map<String, MappedStatement> mappedStatements
中,共给后面调用时使用。
为什么Mybatis
要使用这个方法去做这件事情呢?因为要等到代理Dao(Mapper)
中所有的属性都初始化结束以后,也就是所有的依赖都构建完成以后,才开始解析这些注解。