看懂這篇文章可能需要有一定的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,如下: