目的
- 复习Spring Boot项目中IoC加载的主要流程
- 了解如何配置自定义的AutoConfiguration
- (重点) 了解MyBatis如何在Spring Boot项目中加载配置和初始化
注:本文针对的是MyBatis和Spring Boot关联内容,没有详细讲解MyBatis本身的配置和初始化,这部分会在专题的其他文章中再详述。
先修知识
- 需要简单了解Spring Boot的IoC加载主要流程
- 需要了解如何使用MyBatis
- 最好了解单纯的Java+MyBatis项目中,MyBatis如何加载配置和初始化
sample项目
直接用官网提供的Quick-Start的Sample项目做本文的样例项目。别管这个项目有多简单,只有真的上手去跑,去看代码,才有感觉。
Quick Start · mybatis/spring-boot-starter Wiki · GitHub
使用下面的curl命令拉取sample项目后,按照上面官方Wiki文档的内容依次将所需文件内容写入项目中
curl -s https://start.spring.io/starter.tgz\
-d name=mybatis-sample\
-d artifactId=mybatis-sample\
-d dependencies=mybatis,h2\
-d baseDir=mybatis-sample\
-d type=maven-project\
| tar -xzvf -
这个项目的基本信息:
jdk17,spring-boot 3.2.4,mybatis-spring-boot-starter 3.0.3,数据库使用内存数据库h2
mybatis-spring-boot-starter 3.0.3 这个MAVEN包中又包含:
- mybatis
- mybatis-spring
- mybatis-spring-boot-autoconfigure
- spring-boot-starter
- spring-boot-starter-jdbc
Sample项目运行结果:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.4)
2024-04-01T16:47:21.323+08:00 INFO 66794 --- [mybatis-sample] [ main] c.e.m.MybatisSampleApplication : Starting MybatisSampleApplication using Java 17 with PID 66794 (/Users/xxx/Documents/study/mybatis_study/mybatis-sample/target/classes started by zhuangsuyu in /Users/xxx/Documents/study/mybatis_study/mybatis-sample)
2024-04-01T16:47:21.326+08:00 INFO 66794 --- [mybatis-sample] [ main] c.e.m.MybatisSampleApplication : No active profile set, falling back to 1 default profile: "default"
2024-04-01T16:47:22.158+08:00 INFO 66794 --- [mybatis-sample] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2024-04-01T16:47:22.314+08:00 INFO 66794 --- [mybatis-sample] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:a1672050-cc5a-4f4f-b0c9-b28b3b635f20 user=SA
2024-04-01T16:47:22.315+08:00 INFO 66794 --- [mybatis-sample] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-04-01T16:47:22.585+08:00 INFO 66794 --- [mybatis-sample] [ main] c.e.m.MybatisSampleApplication : Started MybatisSampleApplication in 1.594 seconds (process running for 1.997)
1,San Francisco,CA,US
2024-04-01T16:47:22.648+08:00 INFO 66794 --- [mybatis-sample] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2024-04-01T16:47:22.651+08:00 INFO 66794 --- [mybatis-sample] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
AutoConfiguration的配置
我们知道Spring Boot的设计核心原则之一是简化配置(约定优于配置):让一些常用的配置直接默认,如果是特殊需要定制的配置,用户可以用自己的Bean实例去替换。
而实现“约定优于配置”这一原则的核心功能之一就是AutoConfiguration
在不同的Spring Boot版本中,AutoConfiguration的写法略有不同,下面展示的是不同版本中,Mybatis官方提供的MybatisAutoConfiguration源码配置方式,稍作了解即可。
在spring-boot.2.7.x 之前版本里,
在目录 META-INF/
下新建文件 spring.factories
文件中内容填写:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
在spring-boot.2.7.x 之后版本里,
在目录 META-INF/spring/
下新建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中内容填写:
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
简述SpringIoC的加载过程
在Spring Boot启动过程中,我们需要知道AbstractApplicationContext类中有个非常关键的refresh()
方法。
refresh()
方法中又有两个重要的方法:
invokeBeanFactoryPostProcessors(beanFactory);
调用已经注册过了的Bean工厂后置处理器,用来解析和加载各种配置类,注册到beanDefinitionMap- 先分别getBean之前的Bean工厂后置处理器(如internalConfigurationAnnotationProcessor)
- 然后利用ConfigurationClassPostProcessor这个Bean工厂后置处理器,解析各种加了
@Configuration
的配置类,@ComponentScan
包,@Import
注解等
finishBeanFactoryInitialization(beanFactory);
实例化所有剩余的单例bean- 利用反射的方式实例化
- 根据DI,填充属性
- 初始化
以下是一个常见的refresh()
方法,粘贴在这里帮助大家对我们要讲的两个重点方法在哪个位置有个大致印象。
@Override
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
// 一些准备工作...
try {
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 【重点】调用ConfigurationClassPostProcessor解析配置类,会注册生成beanDefinitionMap
invokeBeanFactoryPostProcessors(beanFactory);
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
initMessageSource();
initApplicationEventMulticaster();
onRefresh();
registerListeners();
// 【重点】 Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
finishRefresh();
}
catch (RuntimeException | Error ex ) {
// 一些catch工作...
} finally {
contextRefresh.end();
}
} finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}
源码总体流程图
从refresh()方法中的两个重点方法出发,一步步会通过默认配置、用户自定义配置将MyBatis需要的信息加载到框架中。
这里我先将我整理的源码总体流程图贴在这里,后面的内容会详细、局部地去说明各个环节的执行流程。
图中左上方灰色方块底的模块是发生在Spring Boot包中的,绿色方块底的模块发生在mybatis-spring包中,图正中偏右下的红色方块底是mybatis-spring-boot-autoconfigure提供的能力,蓝紫色方块底是最单纯的mybatis包本身了。
mybatis-spring包在各个包中起到了连接作用。
xml配置文件的解析流程
先说xml配置文件。xml配置文件一般长下面这样,文件本身放在项目的resources目录下。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://xxx"/>
<property name="username" value="xxxx"/>
<property name="password" value="xxxx"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="aaaa/bbbb.xml"/>
</mappers>
</configuration>
如果配置了mybatis-config.xml配置文件,该文件的解析和配置是在本文所述的refresh()第二个核心方法(即实例化各个bean的阶段)发生的,如下图所示。
在调用SqlSessionFactoryBean
的factory.getObject()
方法时,会在buildSqlSessionFactory()
方法中进行parse,使用mybatis包中的XMLConfigBuilder解析xml配置文件。
当然,项目也可能自己注入了一个自定义的SqlSessionFactory,仍然也是会调用到factory.getObject()
方法的,如下所示:
package com.example.mybatissample.config;
@Configuration
@MapperScan("com.example.mybatissample")
public class MybatisConfig {
@Autowired
DataSource dataSource;
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(
@Value("classpath:/mybatis/mybatis-config.xml") Resource configLocation
) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setConfigLocation(configLocation);
return sqlSessionFactoryBean.getObject();
}
}
当然,在实际项目中,我们也可能完全不写xml配置文件。这样便于管理配置信息,避免配置分散在不同文件里。
Mapper接口和xml映射文件的扫描加载
mapper接口
我们知道有以下两种配置mapper接口类的方法,这些配置方式到底是怎么影响扫描的?
- @MapperScan,配合@Repository(后者可以不配置,不写最多可能会导致有的版本的idea在注入的地方有红色波浪线)
- @Mapper,如果不使用@MapperScan统一扫描包,那么每个mapper接口上都一定要注解@Mapper。注意,在使用了@MapperScan的情况下,自定义的包外面单独的@Mapper就没用了。
所以在大型项目中,一般都会采用@MapperScan的方式。
在没有@MapperScan的情况下,AutoConfiguredMapperScannerRegistrar类中的registerBeanDefinitions()方法会在load阶段调用,用来生成默认的MapperScannerConfigurer类的beanDefinition,该默认配置中的配置的basePackage是整个项目的Application类所在的包。
mapper接口的扫描也发生在SpringIoC的生成beanDefinition阶段,通过MapperScannerConfigurer
在有@MapperScan的情况下
MybatisAutoConfiguration 类下的 MapperScannerRegistrarNotFoundConfiguration 类的 ConditionalOnMissingBean 条件不满足,所以不会注册默认的MapperScannerConfigurer类的beanDefinition,而是使用用户自定义的方式。
@org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
@Override
public void afterPropertiesSet() {
logger.debug(
"Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}
}
mapper接口的映射文件(xml)
配置mapper接口的映射文件
- setMapperLocations
- 在xml配置文件中<mappers>下的<mapper resource="">标签<mapper resdource="/mybatis/mapper/StudentMapperDAO.xml"/>
- 接口文件与映射文件在同一路径下,接口名与映射文件名相同,并且映射文件命名为接口类名时, 在xml配置文件中<mappers>下的<mapper class="">标签
<mapper class="com.example.mybatissample.mapper.StudentMapper"/>
- 接口文件与映射文件在同一路径下,接口名与映射文件名相同,并且映射文件命名为接口类名时, 在xml配置文件中<mappers>下的<package>标签
<mapper class="com.example.mybatissample.mapper"/>
映射文件的解析是在的refresh()第二个核心方法(即实例化各个bean的阶段)发生的解析的。
使用mybatis包中的XMLConfigBuilder解析xml配置文件的最后,会交由XMLMapperBuilder类负责映射文件的解析,生成mappedStatements存放在configuration对象中。这部分就是纯mybatis源码里的内容了,不在这里赘述。
private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfsImpl(settings);
this.loadCustomLogImpl(settings);
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginsElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlersElement(root.evalNode("typeHandlers"));
this.mappersElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
总结
在这篇文章里,我们先梳理了Spring Boot IoC的基本流程,确定了refresh()中两个最重点的方法。通过将这两个重点方法与MyBatis配置加载和初始化相结合的方式,明确了xml配置文件、Mapper接口以及映射文件的加载、解析分别发生在这两个方法的什么环节。
换句话说,本质上我们是先在脑海中通过Spring Boot IoC的基本流程绘制了一张简要地图,然后在这张地图中圈出了哪些地点是MyBatis加载、解析、初始化的重要地方。
核心知识点简要总结,Spring Boot IoC的基本流程里refresh()中有两个重点方法,分别是为了形成beanDefinitionMap的invokeBeanFactoryPostProcessors(beanFactory);
和为了实例化各种剩余bean的
finishBeanFactoryInitialization(beanFactory)
;
mapper接口的扫描发生在第1个重点方法;xml配置文件的解析和xml映射文件的解析,都发生在第2个重点方法中。