mybatis 版本:3.4.6
mybatis-spring 版本:1.3.2
首先需要Spring配置文件中添加以下配置:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.mybatis.spring.sample.mapper" />
<!--可选项:指定在Spring上下文中有多个sqlSessionFactory的情况下使用哪个sqlSessionFactory。通常只有当有多个datasource时才需要-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
我们先看下MapperScannerConfigurer的类图:

从MapperScannerConfigurer类图上我们可以看到它实现了四个接口:BeanDefinitionRegistryPostProcessor、InitializingBean、ApplicationContextAware和BeanNameAware。
- 实现
BeanDefinitionRegistryPostProcessor接口中的postProcessBeanDefinitionRegistry方法,扫描basePackage路径下的类,并注册到Spring容器中。 - 实现
InitializingBean中的afterPropertiesSet方法,校验basePackage属性不能为空。 - 实现
ApplicationContextAware中的setApplicationContext方法,用于获取Spring容器applicationContext。 - 实现
BeanNameAware中的setBeanName方法动态的设置beanName。
应用启动时的方法运行顺序
MapperScannerConfigurer 中实现的接口方法的运行顺序如下:
- 调用
setBeanName方法设置名称。 - 调用
setApplicationContext方法设置applicationContext容器对象。 - 调用
afterPropertiesSet方法检查属性basePackage不能为空。 - 调用
postProcessBeanDefinitionRegistry方法开始扫描basePackage路径,并将路径下的类注册到Spring容器中。
前三步都非常简单,只是进行简单的配置检查和容器对象的注入。而 mapper 文件主要的注册过程是在第四步中完成,即在 postProcessBeanDefinitionRegistry方法中将 mapper 接口解析,并交由 Spring 容器管理。
scanner 扫描 basePackage 分析
我们来看下 postProcessBeanDefinitionRegistry 方法中做了哪些事情:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
//将占位符替换为真正的值
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
//默认true,将还未注册的mapper添加到Mybatis中
scanner.setAddToConfig(this.addToConfig);
//scanner 会注册拥有此注解的所有接口
scanner.setAnnotationClass(this.annotationClass);
//scanner 会注册具有指定父接口的接口类
scanner.setMarkerInterface(this.markerInterface);
//指定具体的session工厂,有多数据源时需要配置
scanner.setSqlSessionFactory(this.sqlSessionFactory);
//指定具体的session模板,有多数据源时需要配置
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
//指定具体的session工厂,有多数据源时需要配置,和setSqlSessionFactory作用一样
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
//指定具体的session模板,有多数据源时需要配置,和setSqlSessionTemplate作用一样
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
//指定 Spring 容器
scanner.setResourceLoader(this.applicationContext);
//指定 beanName 生成器
scanner.setBeanNameGenerator(this.nameGenerator);
//配置父scanner接口过滤全部符合条件的mapper接口(根据上面annotationClass、markerInterface属性)
scanner.registerFilters();
//扫描并注册basePackage路径下的mapper接口
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
从上面方法的运行顺序可以看到:
- 该方法首先根据
processPropertyPlaceHolders属性来判断是否需要加载配置文件来替换占位符。 - 接着设置
scanner对象的属性值,包括指定session工厂、session模板、设置Spring容器对象。 - 然后根据
annotationClass、markerInterface属性过滤符合条件的所有接口。 - 扫描
basePackage路径下的mapper接口,并完成mapper接口的注册。
先来解释下第一步中 processPropertyPlaceHolders 替换占位符的含义。
有时候,我们会这样配置,basePackage 属性使用占位符代替,将实际包名配置在config.properties中。
Spring中配置改为以下形式:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="${basePackage}" />
</bean>
config.properties中有如下属性:
basePackage=org.mybatis.spring.sample.mapper
在Spring配置文件中有如下配置来读取config.properties:
<bean id="propertyPlaceholderConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:config.properties</value>
</list>
</property>
<property name="ignoreUnresolvablePlaceholders" value="true" />
</bean>
但在启动的时候却发现占位符${basePackage}并没有被替换,还给你丢了个异常java.lang.IllegalArgumentException: Could not resolve placeholder ‘basePackage’。
经过分析,我们从 processPropertyPlaceHolders 方法的注释上可以发现原因:
BeanDefinitionRegistries are called early in application startup, before BeanFactoryPostProcessors.
This means that PropertyResourceConfigurers will not have been loaded and any property substitution of this class’ properties will fail.
To avoid this, find any PropertyResourceConfigurers defined in the context and run them on this class’ bean definition. Then update the values.
简单翻译下:
BeanDefinitionRegistries 是在应用启动的早期,且在BeanFactoryPostProcessors之前运行的。
这意味着PropertyResourceConfigurers还不会被加载,并且此类的所有属性的占位符替换都将会失败。
为了避免这种情况,需要查找在上下文中定义的PropertyResourceConfigurers配置,并且在此类的定义过程中运行它们,然后更新 basePackage、sqlSessionFactoryBeanName、sqlSessionTemplateBeanName的值。
解决方案:我们修改下scanner扫描器,并添加新的配置processPropertyPlaceHolders为true。
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="${basePackage}"/>
<property name="processPropertyPlaceHolders" value="true"/>
</bean>
这个时候,重新运行,会发现${basePackage}占位符已被成功替换。以下是 processPropertyPlaceHolders 方法的源码:
/*
* BeanDefinitionRegistries are called early in application startup, before
* BeanFactoryPostProcessors. This means that PropertyResourceConfigurers will not have been
* loaded and any property substitution of this class' properties will fail. To avoid this, find
* any PropertyResourceConfigurers defined in the context and run them on this class' bean
* definition. Then update the values.
*/
private void processPropertyPlaceHolders() {
Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);
if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext)
.getBeanFactory().getBeanDefinition(beanName);
// PropertyResourceConfigurer does not expose any methods to explicitly perform
// property placeholder substitution. Instead, create a BeanFactory that just
// contains this mapper scanner and post process the factory.
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition(beanName, mapperScannerBean);
for (PropertyResourceConfigurer prc : prcs.values()) {
prc.postProcessBeanFactory(factory);
}
PropertyValues values = mapperScannerBean.getPropertyValues();
//占位符替换
this.basePackage = updatePropertyValue("basePackage", values);
this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values);
this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values);
}
}
从上面源码可以看出,可以实现三个属性的占位符替换:basePackage、sqlSessionFactoryBeanName和sqlSessionTemplateBeanName。
接下来看 scanner 是如何扫描 basePackage 下的mapper接口的:
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
延伸:StringUtils.tokenizeToStringArray是Spring中字符串工具类,用于将字符串按照给定的格式进行分割。将上面的语句进行翻译:
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n");
假设 basePackage 为"controller,mapper;service;util,util;",则调用完tokenizeToStringArray后生成的字符串数组为["controller","mapper","service","util","util"]。
下面看下 doScan 的代码:
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
//调用父类ClassPathBeanDefinitionScanner中的doScan方法
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
//遍历basePackages路径
for (String basePackage : basePackages) {
//获取包中的mapper接口
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
//获取spring管理bean的作用域特性,包括名称和作用域代理行为,默认是单例和不使用代理
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
//获取bean名称,默认是类名的首字母小写
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
//其他设置,默认懒加载 lazyInit 为 false
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
//mapper如何有注解,则根据注解值进行赋值
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
//检查beanName对应的mapper是否已注册
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
//开始对象的注册
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
注意:beanNameGenerator.generateBeanName用于获取beanName。这里有两种默认处理方式:
第一种如果类名是CommonMapper这种首字母大写,第二个字母小写的,则返回commonMapper;
第二种情况,当首字母和第二个字母都是大写,则原样返回,例如类名是URLMapper,则返回的仍然是URLMapper。
接下来继续看 registerBeanDefinition 方法,类的注入过程,随着一层一层的debug,
我们发现,最终的对象注册是在 DefaultListableBeanFactory 类中的registerBeanDefinition 方法中完成的:
/** bean对象map,key为beanName */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();
/** beanName列表,按照注册的顺序添加 */
private final List<String> beanDefinitionNames = new ArrayList<String>();
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
//bean名称和对象不能为空
Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
if (beanDefinition instanceof AbstractBeanDefinition) {
try {
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}
//将beanDefinitionMap锁定,只能串行处理
synchronized (this.beanDefinitionMap) {
Object oldBeanDefinition = this.beanDefinitionMap.get(beanName);
//如果oldBeanDefinition不为空则说明该beanName已经有对应的对象
if (oldBeanDefinition != null) {
//是否允许相同beanName的对象覆盖,不允许则抛出异常
if (!this.allowBeanDefinitionOverriding) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
"': There is already [" + oldBeanDefinition + "] bound.");
}
else {
if (this.logger.isInfoEnabled()) {
this.logger.info("Overriding bean definition for bean '" + beanName +
"': replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
}
}
}
else {
//将beanName按照顺序插入到beanDefinitionNames列表中
this.beanDefinitionNames.add(beanName);
this.frozenBeanDefinitionNames = null;
}
//将beanName及beanDefinition加入/覆盖到beanDefinitionMap容器中
this.beanDefinitionMap.put(beanName, beanDefinition);
resetBeanDefinition(beanName);
}
}
总结:
- 配置文件中添加
MapperScannerConfigurer及basePackage路径。 - 调用
BeanDefinitionRegistryPostProcessor中的postProcessBeanDefinitionRegistry方法配置 scanner属性。 scanner扫描器调用scan方法开始进行mapper文件扫描。ClassPathBeanDefinitionScanner中的doScan方法对mapper接口进行抽取封装成一个BeanDefinition对象。DefaultListableBeanFactory中registerBeanDefinition方法将 beanName对应的BeanDefinition对象存放到beanDefinitionMap容器中,完成bean定义对象的注册。
最后再配上一张时序图,仅供参考,不到之处,尽请指正:

本文详细解析了MyBatis与Spring框架的整合过程,包括配置MapperScannerConfigurer、BeanDefinition注册、占位符处理及mapper接口扫描流程。
52





