Mybatis-spring-boot-starter自动配置的原理分析

时间:2023-03-10 04:54:42
Mybatis-spring-boot-starter自动配置的原理分析

相信大家在使用SpringBoot的过程中,经常会使用到mybatis,通过使用mybatis-spring-boot-starter依赖进行自动配置,省去了自己依赖配置和Bean配置的很多麻烦。

有这么方便的starter,使大家不禁好奇,它究竟是怎么让我们能够不要任何配置就可以使用mybatis的,背后的原理究竟是什么?

本文将以mybatis-spring-boot-starter作为例子,探究它背后的秘密。

首先我们建立一个SpringBoot工程,并且添加mybatis-spring-boot-starter依赖(在此我们假设读者已经熟悉SpringBoot基本知识和操作):

Mybatis-spring-boot-starter自动配置的原理分析

如果我们此时启动程序,Mybatis会随程序自动启动。

starter减少了我们依赖配置和代码中对象关系映射配置,我们分成两部分分析:

第一步我们分析starter的依赖,从pom文件检查工程的依赖:

  <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>

可以看到,starter将需要的依赖全部引入,免去了我们配置的麻烦。

综上所述,spring的starter免去了我们配置的麻烦,是个很好用的系统,全文完,谢谢观看。

哈哈哈,如果文章写到这里就结束,大家肯定要打我了(读者内心:你写的这什么东西)

光靠引入依赖包并不足以说明为什么这些组件就能自动被装载,并且已经配置好了,所以我们分析下这些被引用的依赖,找到其中起作用的包。

首先mybatis,mybatis-spring都可以被忽略掉,这些就是正常的mybatis组件。

那可疑的就是这个mybatis-spring-boot-autoconfigure了。

直接打开这个jar包看里面的内容:

Mybatis-spring-boot-starter自动配置的原理分析

可疑的就是这个MybatisAutoConfiguration和spring.factories文件,其中spring.factories的内容:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

首先百度一下spring.factories,这个是Spring的SPI机制,这个文件的内容也已经说的很明显了,自动配置,连注释都给带上了,哈哈。

代码通过SPI机制加载了两个类,其中最重要的是:

org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

我们打开这个类,查看其中代码:

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean { //......

可以看到,这是一个Spring配置类(因为有Configuration注解)

这个类需要在classpath中有SqlSessionFactory和SqlSessionFactoryBean定义时才会起效,并且要有一个DataSource的candidate注册到spring容器中

当这些条件都满足时,这个Configuration会注册一个SqlSessionFactorySqlSessionTemplate :

  @Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { ...... @Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ......

而MyBatis整个数据库访问,都是围绕这两个对象展开的。

至此已经解开Mybatis自动配置的第一个问题了,还有第二个:我们定义的那些Dao是如何被扫描的?

请继续在这个类中往下看,类中间包裹了一个static subclass:

  /**
* This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
* {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
* similar to using Spring Data JPA repositories.
*/
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar { private BeanFactory beanFactory; @Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { //......
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}

这个类实现了ImportBeanDefinitionRegistrar,也就是会向Spring容器中注入一个bean定义,而这个bean定义就是MapperScannerConfigurer。

我们继续看MapperScannerConfigurer:

public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
//......

标红的部分已经很能说明问题了:这个类是BeanDefinitionRegistry的后处理器,可能会修改Spring容器中的BeanDefinition,果然,在下面我们就能找到对应的方法:

  /**
* {@inheritDoc}
*
* @since 1.0.2
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
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.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

其中最重要的部分就是这个标红的scan,我们跟进去看下(其实不跟进去大家应该也看出所以然来了,这里有个basePackage,是不是很熟悉?):

/**
* Perform a scan within the specified base packages,
* returning the registered bean definitions.
* <p>This method does <i>not</i> register an annotation config processor
* but rather leaves this up to the caller.
* @param basePackages the packages to check for annotated classes
* @return set of beans registered if any for tooling registration purposes (never {@code null})
*/
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;
}

这里就是从basePackages中扫描到所有合适的类(准确的说应该是接口),并且将这些类(接口)生成对应的BeanDefinition,这是为了让Spring在实例化你定义的Dao时,能够成功,如果没有这一步,启动肯定会报“Cannot find candidate for autowired property xxx”这类错误,因为你在工程里面声明的本来就是个接口嘛。

并且请注意到最后面标红的applyScopedProxyMode,这会为你的Dao生成代理,这也是为何你调试代码时,调用到Dao时你把鼠标放上去,显示的Dao类型是一个MapperProxy<T>。

至此,Mybatis自动加载的原理基本上可以总结如下:

1. mybatis-spring-boot-starter将mybatis需要的依赖全部引入

2. starter同时通过SPI机制引入了一个配置Class:MybatisAutoConfiguration,它负责注册SqlSessionFactory和SqlSessionTemplate到Spring容器中,我们使用Mybatis时绝大部分功能靠这两个Bean实现

3. 引入了AutoConfiguredMapperScannerRegistrar这个bean到Spring容器,它负责将MapperScanner引入Spring容器,然后MapperScanner会将工程中所有的Dao扫描出来转换为BeanDefinition并且注册到Spring容器中

4. 当开发的工程中使用了某个Dao时,Spring能够从容器中找到这个Dao对应的BeanDefinition,将其实例化并且注入,这样开发者就可以使用了,这也是为何我们只定义了Dao的接口,但是工程运行时能够有实例Bean的原因