看懂这篇文章可能需要有一定的spring源码基础,在分析原理之前,先简单讲一下应用。。。
使用spring boot结合mybatis开发时,会在pom.xml中引入下面依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
而这个依赖实际上引入了以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
可以看到实际上引入了mybatis-spring ,这个jar包是为了让mybatis与spring两个框架更好的集成、更方便开发人员而开发的。
使用mybatis时,常用的标记Mapper所在位置的两种方式为:
1.在每个Mapper(示例代码的Dao层)文件上加上@Mapper注解,如下:
2.在spring boot启动类上加上@MapperScan注解,并指定包名,如下:
鄙人的代码结构如下:
下面将按照这两种配置方式来分析mybatis是如何利用spring的扩展点的,首先分析 @Mapper方式,故事还得从spring初始化讲起。。。
1.在spring初始化时,会调用org.springframework.context.support.AbstractApplicationContext#refresh,方法中有这样一行代码:
// Invoke factory processors registered as beans in the context.
// 调用 BeanFactory 后置处理器
invokeBeanFactoryPostProcessors(beanFactory);
2.这行代码会一直调用到 org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry,方法中有这样一行代码:
processConfigBeanDefinitions(registry);
3.这行代码会一直调用到 org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsFromRegistrars,代码如下:
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
// 这行代码的意思就是循环调用实现了ImportBeanDefinitionRegistrar的类的registerBeanDefinitions方法
registrars.forEach((registrar, metadata) ->
registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
}
可以看到registrars中有一个元素,如下:
4.MybatisAutoConfiguration的静态内部类AutoConfiguredMapperScannerRegistrar实现了ImportBeanDefinitionRegistrar,并重写了registerBeanDefinitions()方法,主要代码如下:
public static class AutoConfiguredMapperScannerRegistrar
implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 这句日志说明了这个方法的作用
logger.debug("Searching for mappers annotated with @Mapper");
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
// 拿到需要扫描的包名,默认是顶层包,鄙人的为:com.example.demo
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
for (String pkg : packages) {
logger.debug("Using auto-configuration base package '{}'", pkg);
}
}
// 设置AnnotationClass: Mapper.class
scanner.setAnnotationClass(Mapper.class);
// 注册过滤器,会把包含了 Mapper.class 的过滤器注册进去
scanner.registerFilters();
// 扫描包
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException ex) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
}
}
}
5.调用org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan方法来扫描类,代码如下:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 扫描类路径以查找候选组件
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
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;
}
6.进入 findCandidateComponents(basePackage) 方法,这个方法是其父类ClassPathScanningCandidateComponentProvider.java 中的方法,方法中又调用了 scanCandidateComponents() 方法,如下:
先看上面那个 isCandidateComponent(metadataReader) ,如下:
只有这个方法返回true时才会继续执行第二个 isCandidateComponent(sbd) ,如下:
满足这两个条件才会将符合条件的类添加到candidates中,在示例代码中,最终有两个符合条件的类:MenuDao和StudentDao,如下:
7.完成Mapper(示例代码的Dao层类)的扫描后,回到org.mybatis.spring.mapper.ClassPathMapperScanner#doScan,继续完成BeanDefinition的处理,调用 processBeanDefinitions(beanDefinitions) ,这个方法中有一个很重要的操作:将自动注入模式设置为 byType,这个操作对使用@Autowired注解注入Mapper时有重大意义,如下:
完成这个操作后,按照spring初始化流程理解即可。
接下来分析第二种方式: @MapperScan(basePackages = “com.example.demo.dao”)
1.查看 @MapperScan 注解:
这个注解使用 @Import 引入了 MapperScannerRegistrar.class ,这个@Import 的作用可以简单理解为让 @MapperScan 拥有 MapperScannerRegistrar 的功能,所以重点看一下 MapperScannerRegistrar.class ,如下:
MapperScannerRegistrar 也实现了 ImportBeanDefinitionRegistrar ,并重写了registerBeanDefinitions()
2.看到 registerBeanDefinitions() 方法最后的代码:
这时候 basePackages = “com.example.demo.dao” ,然后也会调用org.mybatis.spring.mapper.ClassPathMapperScanner#doScan 方法来扫描,后续的流程基本上与第一种方式相同了。
通过两种方式对比发现:用 @MapperScan 来指定 Mapper 所在包名时,在扫描前得到的 basePackages 范围更小,所以扫描时效率会更高。
在使用mybatis时,如果结合了注解和xml两种方式来编写sql,需要在spring boot配置文件中指定xml的路径,如下:
mybatis:
mapper-locations: classpath:mapping/*.xml
示例就是同时使用了两种方式,这种情况下,先处理Mapper.java,后处理Mapper.xml ,那么xml是什么时候处理的呢?
在调用org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionFactory创建org.apache.ibatis.session.SqlSessionFactory对象时,会调用到org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory,在这个方法中会判断 mapperLocations 是否为空,对应上面那个配置,如果不为空则会循环解析xml,如下: