概述
衔接前文
前文《MyBatis 与 Spring 整合原理》已经从 Spring 扩展点的角度深入剖析了 MapperScannerRegistrar、MapperFactoryBean、SqlSessionTemplate 等组件是如何让 MyBatis 无缝接入 Spring 容器的。在 Spring Boot 环境下,这些组件的创建和协调被进一步自动化——通过 MybatisAutoConfiguration 与条件装配,开发者只需引入 mybatis-spring-boot-starter 并配置数据源,其余全部由 Boot 接管。本文将正面拆解这一自动配置引擎,揭示它如何根据类路径和已有 Bean 智能决策,以及如何在多数据源等复杂场景下进行优雅的隔离与定制。
总结性引言
在 Spring Boot 体系中,引入 mybatis-spring-boot-starter 后,开发者的工作几乎只剩下编写 Mapper 接口和 SQL。Boot 不仅自动创建了 SqlSessionFactory、SqlSessionTemplate 和事务管理器,还通过 @AutoConfigurationPackage 配合 AutoConfiguredMapperScannerRegistrar 自动发现主启动类所在包下的所有 @Mapper 接口,彻底消灭了手动配置 @MapperScan 的需要。这一切的“魔法”都收敛在 MybatisAutoConfiguration 的条件装配逻辑中。本文将逐行拆解这一自动配置类,从 @ConditionalOnClass 的环境感知,到 @EnableConfigurationProperties 的属性绑定,再到 @ConditionalOnMissingBean 的优雅扩展点,为读者呈现 Spring Boot 如何将 MyBatis 的集成变成一件几乎无感却高度可控的事情。
核心要点
- MybatisAutoConfiguration 条件装配:根据类路径和数据源 Bean 的存在与否自动激活。
- 基础设施自动创建:
SqlSessionFactoryBean→SqlSessionFactory→SqlSessionTemplate的自动化链路。 - 自动 Mapper 扫描:
AutoConfiguredMapperScannerRegistrar利用BasePackages自动检测@Mapper接口。 - 多数据源隔离:
@Primary与命名 Bean 在多个DataSource、SqlSessionFactory间的选择策略。 - 高度可定制:
ConfigurationCustomizer与MybatisProperties提供的配置入口。
文章组织架构图
flowchart TD
1[1. Spring Boot 整合 MyBatis 总览<br/>从 Starter 到自动配置] --> 2[2. MybatisAutoConfiguration 源码拆解]
2 --> 3[3. SqlSessionFactory 与 SqlSessionTemplate<br/>的自动创建]
3 --> 4[4. MapperScan 的自动扫描<br/>与 Mapper 注解处理]
4 --> 5[5. 配置属性与定制<br/>MybatisProperties 与 ConfigurationCustomizer]
5 --> 6[6. 多数据源场景下的<br/>自动化配置隔离]
6 --> 7[7. 与 Spring 事务的自动集成]
7 --> 8[8. 生产事故排查专题]
8 --> 9[9. 面试高频专题]
1 -.- 2 -.- 3 -.- 4 -.- 5 -.- 6 -.- 7 -.- 8 -.- 9
架构图说明
- 总览说明:全文9个模块从Boot整合总览开始,逐步深入自动配置、基础设施创建、自动扫描、配置定制、多数据源和事务,最后通过事故和面试完成闭环。
- 逐模块说明:模块1-2建立自动配置的全局认知;模块3-5揭示各核心Bean的创建原理;模块6-7聚焦高级场景与协作;模块8-9落地排查与应试。
- 关键结论:Spring Boot 通过条件装配和自动配置将 MyBatis 的整合门槛降到了最低。理解
MybatisAutoConfiguration的决策逻辑是定制和排查 Boot 环境下 MyBatis 问题的根本基础。
1. Spring Boot 整合 MyBatis 总览:从 Starter 到自动配置
1.1 mybatis-spring-boot-starter 依赖拓扑
引入 mybatis-spring-boot-starter 在 pom.xml 中仅做一件事:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
该 starter 自身是一个空项目,通过 pom.xml 传递以下核心依赖:
- mybatis-spring-boot-autoconfigure:包含
MybatisAutoConfiguration及属性绑定类。 - mybatis-spring:提供
SqlSessionTemplate、MapperFactoryBean、MapperScannerConfigurer等 Spring 整合核心。 - mybatis:MyBatis 内核。
- spring-boot-starter-jdbc(依赖传递):提供
DataSource自动配置、DataSourceTransactionManager、JdbcTemplate等。
这种组织方式完美实践了 Spring Boot 的“starter 只做依赖聚合,autoconfigure 负责自动配置”设计理念。
1.2 自动配置入口:AutoConfiguration.imports
在 mybatis-spring-boot-autoconfigure 的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中(Spring Boot 2.7.x 新位置),声明了:
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
Spring Boot 启动时,@EnableAutoConfiguration 通过 AutoConfigurationImportSelector 加载该文件并批量处理配置类。MybatisAutoConfiguration 被纳入自动配置候选,随后接受条件注解筛选,最终决定是否注册相关 Bean。
1.3 与传统整合方式的对比
在非 Boot 环境中,整合 MyBatis 需要显式配置:
- XML 方式:定义
SqlSessionFactoryBean、MapperScannerConfigurer等 Bean。 - 纯 Java Config:使用
@Import(MapperScannerRegistrar.class)或直接@Bean定义。
而 Boot 的“零配置”理念下,上述工作完全消失。开发者只需关注 DataSource 和 Mapper 接口,其余全部由 MybatisAutoConfiguration 按条件自动装配。这种机制背后正是 Spring Boot 条件装配与 @ConfigurationProperties 属性绑定的强大能力,也是本文要深挖的核心。
2. MybatisAutoConfiguration 源码拆解
2.1 类级别条件注解:激活的边界
MybatisAutoConfiguration 位于 org.mybatis.spring.boot.autoconfigure 包下,Spring Boot 2.7.x 版本典型源码如下(已简化)。
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
// ...
}
解读:
@ConditionalOnClass:类路径必须同时存在SqlSessionFactory和SqlSessionFactoryBean。SqlSessionFactory由 MyBatis 内核提供,SqlSessionFactoryBean由mybatis-spring提供。缺少任一 jar 均不会激活。@ConditionalOnBean(DataSource.class):容器中至少有一个DataSourceBean。这自然依赖于DataSourceAutoConfiguration,所以使用@AutoConfigureAfter确保数据处理自动配置先行。@EnableConfigurationProperties(MybatisProperties.class):将application.yml中以mybatis为前缀的属性绑定到MybatisProperties实例,并注册到容器。
这种条件装配策略确保了 Boot “有数据源才配 MyBatis”的智能决策。
2.2 基础设施 Bean 的注册
该配置类通过 @Bean 方法创建三大核心基础设施:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setConfiguration(this.properties.getConfiguration());
// 处理 configLocation、mapperLocations 等属性
this.applyConfiguration(factory);
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
@ConditionalOnMissingBean
public DataSourceTransactionManager platformTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
要点:
@ConditionalOnMissingBean允许用户通过自定义同名 Bean 覆盖自动配置的任何组件。SqlSessionFactoryBean的getObject()返回SqlSessionFactory实例,利用了 Spring 的FactoryBean机制(详见核心容器系列第 16 篇)。SqlSessionTemplate直接包装第一个(或唯一的)SqlSessionFactory,保证线程安全。- 事务管理器同样由数据源驱动,与 Spring 事务抽象无缝结合。
2.3 自动扫描的触发:内部类与 Registrar
Spring Boot 2.x 早期版本直接在 MybatisAutoConfiguration 中通过 @Import(AutoConfiguredMapperScannerRegistrar.class) 触发自动扫描。在 2.7.x 中,该逻辑仍然存在,以内部类或顶层 AutoConfiguredMapperScannerRegistrar 的形式注册,并由 AutoConfigurationPackage 提供基础包路径。这部分将在第 4 节深入。
序列图:MybatisAutoConfiguration 创建核心 Bean 序列图
sequenceDiagram
participant SC as Spring容器启动
participant EAC as EnableAutoConfiguration
participant MAC as MybatisAutoConfiguration
participant SFB as SqlSessionFactoryBean
participant ST as SqlSessionTemplate
participant DTM as DataSourceTransactionManager
SC->>EAC: 加载 AutoConfiguration.imports
EAC->>MAC: 条件评估(@ConditionalOnClass/OnBean)
alt 条件满足
MAC->>MAC: 绑定 MybatisProperties (application.yml)
MAC->>DTM: 创建 DataSourceTransactionManager (条件缺失)
MAC->>SFB: 创建 SqlSessionFactoryBean 并注入 DataSource
SFB-->>MAC: 返回 SqlSessionFactory
MAC->>ST: 创建 SqlSessionTemplate(SqlSessionFactory)
ST-->>MAC: 返回模板实例
MAC->>MAC: 导入 AutoConfiguredMapperScannerRegistrar
else 条件不满足
MAC-->>SC: 跳过自动配置
end
- 图表主旨概括:展示 Spring Boot 启动时,
MybatisAutoConfiguration如何根据条件逐步创建SqlSessionFactory、SqlSessionTemplate和事务管理器。 - 逐元素分解:首先生效条件注解,然后绑定配置属性,依次检查并创建
DataSourceTransactionManager(由数据源驱动),再通过SqlSessionFactoryBean构建核心工厂,接着创建线程安全的SqlSessionTemplate,最后触发 Mapper 扫描器的自动注册。 - 设计原理映射:
@ConditionalOnMissingBean实现策略模式,允许用户扩展;FactoryBean负责复杂构建;属性绑定遵循“约定优于配置”。 - 工程联系与关键结论:自动配置的顺序逻辑完全依赖条件注解和
@AutoConfigureAfter,任何对数据源的变更或自定义 Bean 都会影响整个链路,这正是排查问题的起点。
3. SqlSessionFactory 与 SqlSessionTemplate 的自动创建
3.1 SqlSessionFactoryBean 的深度绑定
在 sqlSessionFactory 方法中,SqlSessionFactoryBean 被赋予两项核心配置:
- DataSource:直接注入容器中的
DataSource,若存在多个且没有@Primary标注,Boot 会启动失败。 - MybatisProperties 的
getConfiguration():该方法返回一个org.apache.ibatis.session.Configuration对象,核心逻辑如下(出自MybatisProperties):
public Configuration getConfiguration() {
if (this.configuration == null) {
this.configuration = new Configuration();
// 将 configuration 子属性映射到 MyBatis Configuration
PropertyMapper mapper = PropertyMapper.get();
mapper.from(this::getLogImpl).whenNonNull().to(this.configuration::setLogImpl);
mapper.from(this::isCacheEnabled).to(this.configuration::setCacheEnabled);
// ... 其他属性映射
}
return this.configuration;
}
属性映射利用 Spring Boot 的 PropertyMapper 工具,将 mybatis.configuration.* 下的设置安全赋值。例如 mybatis.configuration.cache-enabled=true 将设置 Configuration.setCacheEnabled(true)。此外,config-location(指向外部 XML)和 mapper-locations(映射文件路径)则在 applyConfiguration 方法中处理,支持 Resource 模式匹配。
3.2 SqlSessionTemplate 的线程安全与事务同步
SqlSessionTemplate 是 Spring 管理的 SqlSession 代理,内部通过 SqlSessionInterceptor(动态代理)确保每次调用都从 TransactionSynchronizationManager 获取事务绑定的 SqlSession,若不存在则新建并绑定。自动配置将其注册为 @ConditionalOnMissingBean,即若用户自定义了 SqlSessionTemplate,该自动步骤跳过。这极大保留了灵活性。
3.3 自定义覆盖策略演示
场景:用户想使用 MyBatis 的 VFS 自定义实现或添加插件。
@Configuration
public class MyBatisCustomConfig {
@Bean
public ConfigurationCustomizer myBatisCustomizer() {
return configuration -> {
configuration.setMapUnderscoreToCamelCase(true);
configuration.setDefaultFetchSize(100);
};
}
// 若直接自定义 SqlSessionFactory,可使用如下方式:
// @Bean
// public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// factory.setDataSource(dataSource);
// factory.setTypeAliasesPackage("com.example.domain");
// return factory.getObject();
// }
}
由于 @ConditionalOnMissingBean 的存在,用户只需在同名 Bean 中定义即可自动覆盖,无需排除整个 MybatisAutoConfiguration。
4. @MapperScan 的自动扫描与 @Mapper 注解处理
4.1 传统 @MapperScan 与 Spring 扩展点
前文(核心容器系列第 16 篇)已详述 MapperScannerRegistrar 通过 @Import 导入,在 registerBeanDefinitions 中注册 MapperScannerConfigurer 实现了指定包路径下的 Mapper 动态代理注册。Spring Boot 在此基础上进行了增强:当用户未显式使用 @MapperScan 时,框架自动进行扫描,进一步降低配置负担。
4.2 AutoConfiguredMapperScannerRegistrar 的触发
MybatisAutoConfiguration 内部通过 @Import(AutoConfiguredMapperScannerRegistrar.class) 自动注册扫描器。源码片段(org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar)如下:
static class AutoConfiguredMapperScannerRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 获取 AutoConfigurationPackage 注册的包路径
List<String> packages = AutoConfigurationPackages.get(registry);
if (Collections.isEmpty(packages)) {
return;
}
// 构建 ClassPathMapperScanner
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(packages.toArray(new String[0]));
}
}
解读:
AutoConfigurationPackages.get(registry):这是 Spring Boot 内部工具,用于获取由@AutoConfigurationPackage保存的基础包。该注解本质是@Import(AutoConfigurationPackages.Registrar.class),会在启动类所在包及子包自动注册。ClassPathMapperScanner:继承自ClassPathBeanDefinitionScanner,专门用于扫描标记@Mapper注解的接口,并将其 BeanDefinition 替换为MapperFactoryBean(详细过程见核心容器系列第 16 篇)。setAnnotationClass(Mapper.class):明确只扫描 MyBatis 的@Mapper注解。
4.3 自动扫描与显式 @MapperScan 的协作
当用户显式使用了 @MapperScan(例如指定特定包),此时容器中会存在 MapperScannerConfigurer 或 MapperFactoryBean 的定义,AutoConfiguredMapperScannerRegistrar 会通过 @ConditionalOnMissingBean 相关条件跳过自动扫描。具体实现是通过在 MybatisAutoConfiguration 中增加条件:
@Configuration
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
@Import(AutoConfiguredMapperScannerRegistrar.class)
public static class MapperScannerRegistrarNotFoundConfiguration {
}
因此,若用户自定义了 @MapperScan 或直接注册了 MapperScannerConfigurer Bean,自动扫描即失效,避免了重复扫描或冲突。
序列图:自动扫描 @Mapper 接口
sequenceDiagram
participant SC as Spring容器启动
participant MAC as MybatisAutoConfiguration
participant AMSR as AutoConfiguredMapperScannerRegistrar
participant ACP as AutoConfigurationPackages
participant CPS as ClassPathMapperScanner
participant MPR as MapperFactoryBean
SC->>MAC: 条件满足,加载内部配置类
MAC->>AMSR: @Import 注册 Registrar
SC->>AMSR: 调用 registerBeanDefinitions()
AMSR->>ACP: 获取基础包路径 (BasePackages)
ACP-->>AMSR: 返回 "com.example"
AMSR->>CPS: 创建 ClassPathMapperScanner
AMSR->>CPS: setAnnotationClass(Mapper.class)
AMSR->>CPS: doScan("com.example")
CPS->>CPS: 扫描classpath下所有@Mapper接口
loop 每个 Mapper 接口
CPS->>CPS: 生成 MapperFactoryBean 定义
CPS->>MPR: 注册到容器
end
- 图表主旨概括:展示 Boot 自动扫描机制如何利用
AutoConfigurationPackage和ImportBeanDefinitionRegistrar将 Mapper 接口自动纳入 Spring 容器。 - 逐元素分解:自动配置类通过
@Import引入AutoConfiguredMapperScannerRegistrar,该 Registrar 从AutoConfigurationPackages取得默认包路径,构建ClassPathMapperScanner并设置@Mapper为扫描标记,最终执行扫描并将每个接口转换为MapperFactoryBean。 - 设计原理映射:这是
ImportBeanDefinitionRegistrar扩展点的典型应用,配合 Spring Boot 的AutoConfigurationPackage实现“约定包结构”下的零配置。 - 工程联系与关键结论:只要 Mapper 接口位于主启动类所在包或其子包,并标记
@Mapper,即使没有@MapperScan,Boot 也会自动注册。这一特性极易被忽略导致‘为什么我的 Mapper 能注入?’的疑惑。
5. 配置属性与定制:MybatisProperties 与 ConfigurationCustomizer
5.1 MybatisProperties 属性全览与源码流向
MybatisProperties 使用 @ConfigurationProperties(prefix = "mybatis") 绑定以下核心属性:
configLocation:指定外部 MyBatis XML 配置文件位置,优先级高于其它属性。mapperLocations:映射文件路径,支持classpath*:mapper/**/*.xml模式。typeAliasesPackage:别名扫描包。typeHandlersPackage:类型处理器扫描包。configuration内部属性:logImpl、cacheEnabled、lazyLoadingEnabled、mapUnderscoreToCamelCase等,映射到org.apache.ibatis.session.Configuration。configurationProperties:传递给 MyBatis 配置的额外 properties。
源码片段展示映射逻辑:
public void applyConfiguration(SqlSessionFactoryBean factory) {
if (this.configLocation != null) {
factory.setConfigLocation(this.configLocation);
}
factory.setConfiguration(this.getConfiguration());
if (this.mapperLocations != null) {
factory.setMapperLocations(this.mapperLocations);
}
// 其他设置...
}
在 getConfiguration() 中,Spring Boot 通过 PropertyMapper 将属性逐项拷贝至 Configuration 实例,避免了硬编码。这种将外部配置流向框架 Configuration 的路径是整个自动配置的灵魂。
5.2 ConfigurationCustomizer 扩展回调
ConfigurationCustomizer 是一个函数式接口:
@FunctionalInterface
public interface ConfigurationCustomizer {
void customize(Configuration configuration);
}
用户可在容器中注册一个或多个 ConfigurationCustomizer Bean,它们会在 SqlSessionFactoryBean 创建最终 SqlSessionFactory 之前被回调,以实现对 MyBatis 配置的精细干预。MybatisAutoConfiguration 内部注入所有 ConfigurationCustomizer 列表,并在 applyConfiguration 中调用:
List<ConfigurationCustomizer> customizers = this.configurationCustomizers;
for (ConfigurationCustomizer customizer : customizers) {
customizer.customize(factory.getObject().getConfiguration());
}
示例:添加插件、设置默认执行器等。
序列图:属性绑定到 MyBatis Configuration
sequenceDiagram
participant YML as application.yml
participant MP as MybatisProperties
participant MAC as MybatisAutoConfiguration
participant SFB as SqlSessionFactoryBean
participant CFG as MyBatis Configuration
YML->>MP: 绑定 mybatis 前缀属性
MAC->>MP: 调用 getConfiguration()
MP->>CFG: 创建 Configuration 实例
MP->>MP: PropertyMapper 复制属性
MP-->>MAC: 返回 Configuration
MAC->>SFB: setConfiguration(cfg)
SFB->>SFB: 构建时应用自定义器
SFB->>CFG: 最终 Configuration 生效
SFB-->>MAC: 返回 SqlSessionFactory
- 图表主旨概括:说明
application.yml中的 MyBatis 配置如何一步步传递到org.apache.ibatis.session.Configuration实例。 - 逐元素分解:属性绑定发生在
@EnableConfigurationProperties之后,自动配置调用MybatisProperties.getConfiguration()创建并填充 MyBatisConfiguration,然后传递给SqlSessionFactoryBean;SqlSessionFactoryBean合并自定义配置,最终构建SqlSessionFactory。 - 设计原理映射:Spring Boot 的属性映射机制(
@ConfigurationProperties)和“回调定制”模式(ConfigurationCustomizer)体现了开放封闭原则。 - 工程联系与关键结论:所有 XML 配置均等价于 YAML 的
mybatis.configuration.*属性,但configLocation优先级最高;利用ConfigurationCustomizer可注入任何未暴露为属性的高级设置。
6. 多数据源场景下的自动化配置隔离
6.1 场景与挑战
在实际项目中,经常需要配置多个数据源(例如主库、从库,或不同业务库),并期望不同包下的 Mapper 使用不同数据库连接。Boot 的默认自动配置只针对单个数据源,故多数据源场景通常需要部分或全部排除默认配置。
挑战:
- 容器中存在多个
DataSourceBean 时,MybatisAutoConfiguration会因为@ConditionalOnBean(DataSource.class)仍然激活,但其sqlSessionFactory方法注入DataSource会因歧义而启动失败(除非指定@Primary)。 - 需要创建多个
SqlSessionFactory、SqlSessionTemplate和独立事务管理器。 - 必须将相应 Mapper 绑定到特定的
SqlSessionFactory。
6.2 手动配置隔离策略
典型的做法是在一个或多个 @Configuration 类中排除 MybatisAutoConfiguration(在 @SpringBootApplication(exclude = MybatisAutoConfiguration.class) 中),并手动构建所需 Bean。同时,利用 @MapperScan 的 sqlSessionFactoryRef 或 sqlSessionTemplateRef 精确指定。
实例代码(主/从库):
@Configuration
@MapperScan(basePackages = "com.example.mapper.primary",
sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource,
MybatisProperties properties) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setConfiguration(properties.getConfiguration());
// 可额外设置 mapperLocations 等
return factory.getObject();
}
@Primary
@Bean(name = "primarySqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sf) {
return new SqlSessionTemplate(sf);
}
@Primary
@Bean(name = "primaryTransactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("primaryDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
从库配置类似,省略 @Primary,并指定不同的包路径和 Bean 名称。通过 @MapperScan 的 sqlSessionFactoryRef 指向各自工厂,实现彻底隔离。事务管理也需使用对应的 TransactionManager,@Transactional 需指定 transactionManager 属性。
6.3 @Primary 的作用与陷阱
@Primary 确保在有多个同类型 Bean 时,默认注入(例如无 @Qualifier 的 DataSource)会选择该 Bean。若未正确设置 @Primary,默认的自动配置或其它自动连线(如 DataSourceTransactionManager 创建)会因无法确定唯一 Bean 而报错。当主库标记 @Primary 后,任何未显式限定符的注入都将得到主库数据源,因此从库相关的所有 Bean 必须通过 @Qualifier 明确指定。
绑定原理图:多数据源 Mapper 绑定
classDiagram
class DataSourcePrimary
class DataSourceSecondary
class SqlSessionFactoryPrimary
class SqlSessionFactorySecondary
class SqlSessionTemplatePrimary
class SqlSessionTemplateSecondary
class MapperScanPrimary {
-sqlSessionFactoryRef = "primarySqlSessionFactory"
}
class MapperScanSecondary {
-sqlSessionFactoryRef = "secondarySqlSessionFactory"
}
DataSourcePrimary <.. SqlSessionFactoryPrimary : 注入
DataSourceSecondary <.. SqlSessionFactorySecondary : 注入
SqlSessionFactoryPrimary <.. SqlSessionTemplatePrimary
SqlSessionFactorySecondary <.. SqlSessionTemplateSecondary
MapperScanPrimary ..> SqlSessionFactoryPrimary : 绑定
MapperScanSecondary ..> SqlSessionFactorySecondary : 绑定
note for MapperScanPrimary "包: com.example.mapper.primary"
note for MapperScanSecondary "包: com.example.mapper.secondary"
- 图表主旨概括:展示多数据源场景下,如何通过指定
sqlSessionFactoryRef将不同包下的 Mapper 接口绑定到专属工厂。 - 逐元素分解:两个独立的
DataSource分别创建各自的SqlSessionFactory和模板,@MapperScan注解通过引用工厂名称,使得扫描到的接口均使用对应的连接。 - 设计原理映射:基于 Spring 限定符和命名 Bean 的策略,结合 MyBatis-Spring 的
MapperFactoryBean对sqlSessionFactory引用的支持,实现了完全的上下文隔离。 - 工程联系与关键结论:多数据源下必须显式配置并排除默认自动配置;依靠
@Primary解决大部分模糊匹配,但务必在@Transactional声明中指定对应的事务管理器,否则数据操作会落入错误的数据源。
7. 与 Spring 事务的自动集成
7.1 DataSourceTransactionManager 的自动创建回放
MybatisAutoConfiguration 中:
@Bean
@ConditionalOnMissingBean
public DataSourceTransactionManager platformTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
只要容器中没有其他 PlatformTransactionManager Bean,Boot 便基于数据源创建 DataSourceTransactionManager。该事务管理器支持声明式事务 @Transactional。
7.2 SqlSessionTemplate 如何感知事务
SqlSessionTemplate 的每个数据库操作都经过 SqlSessionInterceptor 代理,其核心逻辑为:
- 从
TransactionSynchronizationManager.getResource(holderKey)获取当前事务绑定的SqlSession。 - 若存在,直接使用并增加引用计数。
- 若不存在,从
SqlSessionFactory打开新SqlSession,并通过TransactionSynchronizationManager.bindResource绑定,注册事务同步器以便在事务提交或回滚时关闭会话。
该机制完全依赖于 Spring 的事务同步管理器,确保在同一事务中多次调用 Mapper 共用同一个数据库连接,无需额外配置。Boot 自动装配的 SqlSessionTemplate 已内置此行为,因此开发者仅需使用 @Transactional 即可获得支持。
7.3 注意区分事务管理器
在多数据源下,必须为每个数据源创建独立的 DataSourceTransactionManager,并在 @Transactional 中明确指定 transactionManager = "xxxTransactionManager",否则默认取 @Primary 的事务管理器,极易导致跨库操作事务失效或数据错乱。
8. 生产事故排查专题
8.1 事故一:升级 Spring Boot 版本后 Mapper 突然无法注入
现象:项目从 Spring Boot 2.5 升级到 2.7 后,原本正常运行的 Mapper 接口注入报 NoSuchBeanDefinitionException,启动失败。
排查思路:
- 检查
mybatis-spring-boot-starter版本是否兼容,已升级至 2.3.x。 - 查看
MybatisAutoConfiguration是否激活,发现日志无相关注册信息。 - 分析条件注解,发现新版本中增加了
@ConditionalOnBean(DataSource.class)(一直存在,但之前版本可能条件判定有差异)或改变了扫描条件。进一步发现项目中使用了DataSourceBuilder创建数据源但未暴露为 Bean,或者数据源的自动配置类执行顺序问题导致DataSource未被提前注册。 - 实际上,多数情况是因为升级后
DataSourceAutoConfiguration的某些条件改变导致 DataSource Bean 未创建。经过检查,发现spring.datasource.url等配置被放入某些 profile 未激活,导致 DataSource 缺失。
根因:MybatisAutoConfiguration 的条件装配严格依赖 DataSource Bean 的存在。Spring Boot 2.7.x 对某些内部条件进行了调整,加上项目配置分离,使得 DataSource 未成功创建,从而连累 MyBatis 自动配置关闭。
解决:补全 spring.datasource 必要属性,或在配置类中主动定义 DataSource Bean。
最佳实践:升级 Boot 版本后,务必完整核对自动配置报告(--debug 启动)中 MyBatis 相关条件的正负匹配情况。理解 @ConditionalOnBean 的本质:只有在容器中确实存在对应 Bean 时才生效。
8.2 事故二:多数据源下写操作误写入从库,导致数据污染
现象:某次版本上线后,发现部分用户数据丢失。排查日志发现更新操作 SQL 执行在从库上(只读),导致业务逻辑未生效,后续操作覆盖了正确数据。
排查思路:
- 应用配置了主从两个数据源,主库
@Primary,从库未标记。 - Mapper 扫描配置:主库 Mapper 包路径正确指向主库
SqlSessionFactory,从库映射只读查询包。 - 但业务代码中一个新功能调用了从库包中的 Mapper 进行写操作(代码错误),同时该 Service 方法上有
@Transactional,但未指定事务管理器,默认使用了@Primary主库事务管理器。 - 然而,问题的严重性在于:由于该 Mapper 实际绑定的
SqlSessionTemplate是从库工厂,Spring 事务管理虽然开启在主库事务管理器,但SqlSession通过SqlSessionTemplate获取时使用了从库连接,导致写操作实际发送到从库。
根因:多数据源下,@Transactional 未指定对应的事务管理器,而 Mapper 的 SqlSessionFactory 与事务管理器操作的数据源不一致,Spring 不会自动对齐。Spring 事务仅管理指定事务管理器对应的连接,而 Mapper 却使用了自己的会话,导致事务与数据源脱节。
解决:重构代码,将写操作移动至主库 Mapper 包,或严格指定 @Transactional(transactionManager = "secondaryTransactionManager") 并确保从库具备写权限(但实际不应写)。根本措施:从库数据源应配置为只读,或通过防火墙限制避免此类误用。
最佳实践:多数据源环境必须建立明确的包和代码边界,配合架构约束检查。同时,统一在 @Transactional 中显式指定事务管理器,防止默认行为带来的潜在风险。
序列图:事故排查序列图(多数据源误写)
sequenceDiagram
participant SVC as 业务Service
participant TX as 事务拦截器
participant ST1 as 主库SqlSessionTemplate
participant ST2 as 从库SqlSessionTemplate
participant DBS as 数据库从库(只读)
SVC->>TX: 调用方法(标注@Transactional)
TX->>TX: 获取主库事务管理器(默认@Primary)
TX->>ST1: 获取主库连接(未使用,因为调用从库Mapper)
SVC->>ST2: 调用从库Mapper.update()
ST2->>DBS: 通过从库连接执行UPDATE
DBS-->>ST2: 执行成功(意外写入)
ST2-->>SVC: 返回
TX->>TX: 提交主库事务(空事务)
Note over SVC,TX: 写操作绕过主库事务,污染从库
- 图表主旨概括:直观展示因事务管理器与 Mapper 绑定的 SqlSessionTemplate 数据源不一致导致的误写入从库事故链路。
- 逐元素分解:事务拦截器基于
@Primary选择主库事务管理器,但业务代码调用了从库 Mapper。从库SqlSessionTemplate独立于事务管理,直接在其数据源上执行 SQL,造成数据污染。 - 设计原理映射:Spring 声明式事务依靠
TransactionManager控制连接,但 MyBatis 的SqlSession来源由SqlSessionTemplate决定,两者之间缺乏自动的统一性检查。 - 工程联系与关键结论:多数据源下务必保持事务管理器与
SqlSessionFactory的一致性,并在@Transactional中显式指定,否则就是“定时炸弹”。
9. 面试高频专题
9.1 Spring Boot 是如何自动配置 MyBatis 的?
标准回答
Spring Boot 通过 mybatis-spring-boot-autoconfigure 模块中的 MybatisAutoConfiguration 实现自动配置。激活条件为类路径存在 SqlSessionFactory 和 SqlSessionFactoryBean,并且 Spring 容器中至少存在一个 DataSource Bean。配置类通过 @EnableConfigurationProperties(MybatisProperties.class) 绑定 mybatis.* 前缀的配置。内部通过三个 @Bean 方法分别创建 SqlSessionFactory(使用 SqlSessionFactoryBean)、SqlSessionTemplate 和 DataSourceTransactionManager,全部标注 @ConditionalOnMissingBean 允许用户覆盖。同时,当容器中不存在 MapperFactoryBean 或 MapperScannerConfigurer 时,会通过 @Import(AutoConfiguredMapperScannerRegistrar.class) 自动扫描主启动类所在包及其子包下标记 @Mapper 的接口,完成 Mapper 代理注册。
追问 1:MybatisAutoConfiguration 和 DataSourceAutoConfiguration 的加载顺序如何保证?
答:MybatisAutoConfiguration 上标注了 @AutoConfigureAfter(DataSourceAutoConfiguration.class),Spring Boot 在排序自动配置类时会根据该注解确保 DataSource 相关配置优先执行,从而满足 @ConditionalOnBean(DataSource.class)。
追问 2:如果用户自定义了 SqlSessionFactory,自动扫描的 AutoConfiguredMapperScannerRegistrar 还会生效吗?
答:会生效,因为自动扫描的触发条件(@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}))与 SqlSessionFactory 的自定义独立。只要用户没有显式使用 @MapperScan 或手动注册扫描配置器,自动扫描依然会执行。
追问 3:自动配置中的条件注解评估发生在什么阶段?与 spring.factories 机制的关系是什么?
答:Spring Boot 启动时,AutoConfigurationImportSelector 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件获取候选配置类,然后通过 Filter 和 Condition 接口在 Bean 定义注册前进行评估。条件不满足的配置类直接被跳过,不会创建任何 Bean 定义。这一机制使得不满足环境的自动配置完全不会占用资源。
追问 4:如果在 application.yml 中不配置任何 mybatis 属性,自动配置创建的 SqlSessionFactory 使用的是 MyBatis 的默认配置吗?
答:是的,MybatisProperties.getConfiguration() 将创建一个全新的 org.apache.ibatis.session.Configuration 对象,默认值与 MyBatis 内核一致(如缓存开启、JDBC 日志等)。若需定制,可通过 mybatis.configuration.* 覆盖。
加分回答
可提及 Spring Boot 2.7 将 spring.factories 中的 AutoConfiguration 条目迁移到了专用的 AutoConfiguration.imports 文件,并支持 AutoConfiguration.valueAttribute 等方式。此外,可以结合 MybatisAutoConfiguration 的完整源码,说明 PropertyMapper 如何将 MybatisProperties 的属性安全地复制到 MyBatis Configuration 实例,甚至能处理 null 源值不覆盖的问题。
9.2 MybatisAutoConfiguration 使用了哪些条件注解?各有什么作用?
标准回答
类级别:
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}):校验 MyBatis 核心库和 mybatis-spring 整合库都在类路径下。@ConditionalOnBean(DataSource.class):确保容器中已经注册了至少一个DataSourceBean。@AutoConfigureAfter(DataSourceAutoConfiguration.class):指定加载顺序,保证数据源先配置。
方法级别:
@ConditionalOnMissingBean:在每个@Bean方法上使用,保证只有当容器中不存在同类型的 Bean 时,才创建默认的SqlSessionFactory、SqlSessionTemplate、DataSourceTransactionManager,实现用户覆盖的扩展点。
追问 1:如果项目中同时引入了 HikariCP 和 Druid,DataSource Bean 会被自动配置成哪个?
答:这取决于 DataSourceAutoConfiguration 的条件匹配顺序,默认使用 HikariCP(如果可用)。即使多个数据源依赖存在,最终只会注册一个 DataSource Bean。MybatisAutoConfiguration 只检测该 Bean 存在性,不关心具体实现。
追问 2:@ConditionalOnClass 中的类名可以随意写吗?如果这个类不存在会发生什么?
答:必须真实存在于类路径。Spring Boot 在解析时会尝试用类加载器加载这些类,如果找不到则条件为 false,对应的配置类完全忽略,不会报错。这保证了缺少 jar 包时只是功能缺失,而不会启动失败。
追问 3:@ConditionalOnMissingBean 能指定 Bean 名称吗?
答:可以,它提供了 name 属性,如 @ConditionalOnMissingBean(name = "customSqlSessionFactory")。如果不指定,默认按类型匹配。MyBatis 自动配置不加名称,直接按类型覆盖。
加分回答
可以深入讨论 Spring Boot 条件注解的实现原理,比如 SpringBootCondition 和 ConditionOutcome,以及如何在 META-INF/spring-autoconfigure-metadata.properties 中预计算条件,以加速启动。同时可以指出 @ConditionalOnBean 的强依赖可能导致 Bean 循环依赖,因此在复杂项目中需要谨慎设计。
9.3 如果在 Boot 项目中不配置 @MapperScan,MyBatis 能自动扫描到 Mapper 吗?
标准回答
可以。MybatisAutoConfiguration 内部定义了 MapperScannerRegistrarNotFoundConfiguration,在满足 @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) 时,会通过 @Import(AutoConfiguredMapperScannerRegistrar.class) 自动注册扫描器。该扫描器从 AutoConfigurationPackages.get(registry) 获取基础包(即主启动类所在包),然后扫描该包及子包下所有标注了 @Mapper 注解的接口,并注册为 MapperFactoryBean。
追问 1:如果 Mapper 接口位于其他独立模块的包中,但仍在 Spring Boot 扫描范围内,会被自动扫描吗?
答:是的,只要主启动类的 @SpringBootApplication(内含 @AutoConfigurationPackage)指定的包范围能覆盖到那些接口,就会被自动扫描。如果其他模块的包不在启动类包的子包下,则需要显式使用 @MapperScan 指定。
追问 2:自动扫描只认 @Mapper 注解吗?如果使用自定义注解怎么办?
答:默认 ClassPathMapperScanner 设置的标记注解就是 org.apache.ibatis.annotations.Mapper。如果要扫描自定义注解(如 @MyMapper),必须显式使用 @MapperScan(annotationClass = MyMapper.class),此时自动扫描会因为 MapperScannerConfigurer 的存在而失效。
追问 3:如果容器中已经通过其他方式(如 XML 中定义 MapperFactoryBean)注册了 Mapper,自动扫描还会重复注册吗?
答:不会,自动扫描的条件是 @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})。只要存在任何一个 MapperFactoryBean 类型的 Bean,该条件就不满足,整个自动扫描逻辑被跳过。
加分回答
深入 AutoConfigurationPackages 的源码,它是 Spring Boot 提供的一个工具,底层使用 BeanDefinitionRegistry 存储基础包列表。AutoConfiguredMapperScannerRegistrar 通过 AutoConfigurationPackages.get(registry) 获得这些包,这完全依赖于 @AutoConfigurationPackage 注解的处理。若用户有意不使用主启动类(如测试中),包集合可能为空,自动扫描会静默跳过。理解这一点对排查“为什么我的 Mapper 没自动注册”非常关键。
9.4 如何在使用 Spring Boot 时定制 MyBatis 的全局配置?
标准回答
有三种主要方式:
- 在
application.yml中使用mybatis.configuration.*配置项,如mybatis.configuration.map-underscore-to-camel-case: true,它会通过MybatisProperties映射到 MyBatis 的Configuration对象。 - 实现
ConfigurationCustomizer接口并注册为 Spring Bean,在 MyBatis 创建SqlSessionFactory前会调用所有ConfigurationCustomizer对Configuration进行定制,适合无法通过属性暴露的复杂设置(如注册插件、TypeHandler)。 - 直接自定义
SqlSessionFactoryBean,因为自动配置方法上有@ConditionalOnMissingBean,用户提供的SqlSessionFactory会完全替代默认的。
追问 1:如果同时使用了 mybatis.configuration 属性和 ConfigurationCustomizer,谁的优先级高?
答:MybatisProperties 先创建并填充 Configuration,然后 ConfigurationCustomizer 再依次定制,后者可以覆盖前者设置的值。同理,若存在 config-location 指定的 XML 配置,其加载顺序也是 XML → MybatisProperties 属性 → ConfigurationCustomizer。
追问 2:ConfigurationCustomizer 可以被多次调用吗?
答:可以,Spring 容器中可以注册多个 ConfigurationCustomizer Bean,自动配置会通过注入 List<ConfigurationCustomizer> 并遍历调用,执行顺序由 Spring 的 @Order 控制。
追问 3:如何为不同环境(如 dev 和 prod)设置不同的缓存策略?
答:可以在 application-{profile}.yml 中分别设置 mybatis.configuration.cache-enabled 的值;或者编写一个 ConfigurationCustomizer 并注入 Environment 对象,动态读取当前 activeProfiles 来决定是否启用缓存。
加分回答
可提及 Spring Boot 的 EnvironmentPostProcessor 扩展点,在配置源加载之前就动态添加属性,实现更早阶段的配置干预。同时,结合 MybatisProperties 的 configurationProperties 可以传递额外的 java.util.Properties,供 MyBatis XML 配置文件中的占位符使用,实现高度灵活的配置。
9.5 Boot 下多数据源时,如何为不同的 Mapper 配置不同的 SqlSessionFactory?
标准回答
首要步骤是排除 MybatisAutoConfiguration,避免其默认创建歧义的 SqlSessionFactory 和 SqlSessionTemplate。然后分别定义多个数据源、SqlSessionFactory、SqlSessionTemplate 和 DataSourceTransactionManager,并严格使用 @Primary 标注默认的主数据源相关 Bean。接着利用 @MapperScan 注解的 sqlSessionFactoryRef 或 sqlSessionTemplateRef 属性,为不同包路径下的 Mapper 指定不同的工厂或模板。
示例架构:
@Configuration
@MapperScan(basePackages = "com.example.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig { ... }
@Configuration
@MapperScan(basePackages = "com.example.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory")
public class SecondaryDataSourceConfig { ... }
追问 1:在多数据源下,@Transactional 需要注意什么?
答:必须显式指定事务管理器。例如 @Transactional(transactionManager = "secondaryTransactionManager"),否则默认使用 @Primary 的事务管理器,可能导致跨数据源操作时事务不匹配,甚至数据写入错误数据库。
追问 2:能不能复用 DataSourceTransactionManager?或者每个数据源必须单独一个?
答:每个数据源必须独立的事务管理器,因为事务管理器内部持有特定数据源,负责从该数据源获取连接并进行事务控制。混用会导致连接混乱。
追问 3:如果 Mapper 之间需要跨库操作怎么办?
答:跨库事务需要使用分布式事务方案(如 JTA),单个 DataSourceTransactionManager 无法处理。Spring Boot 可通过引入 Atomikos 或 Bitronix 并配置 JTA 事务管理器来实现。
加分回答
可深入讨论 MapperFactoryBean 如何通过名称引用 sqlSessionFactory,其内部通过 BeanFactory 根据名称获取对应的工厂实例。同时,使用 @Primary 解决歧义的原理是 Spring 的依赖注入消歧策略,若没有 @Primary 且未指定 @Qualifier,Spring 会抛出 NoUniqueBeanDefinitionException。多数据源场景下,通常建议为每个数据源创建完全隔开的配置模块,甚至使用自定义注解来标记 Mapper 属于哪个数据源。
9.6 SqlSessionTemplate 在 Boot 中是如何被自动创建的?它的线程安全原理是什么?
标准回答
MybatisAutoConfiguration.sqlSessionTemplate() 方法注入唯一的 SqlSessionFactory,并直接 new SqlSessionTemplate(sqlSessionFactory) 返回。SqlSessionTemplate 的线程安全性由内部设计保证:它是一个单例 Bean,但实际数据库操作通过 JDK 动态代理创建的 sqlSessionProxy 进行。每次调用 Mapper 方法时,SqlSessionInterceptor 会根据 TransactionSynchronizationManager 获取当前事务绑定的 SqlSession,如果没有则临时创建,调用后根据是否在事务中决定立即关闭或延迟到事务结束。这样,每个线程实际使用的是独立的 SqlSession 实例,避免了并发问题,同时确保了同一事务内复用会话。
追问 1:如果不存在事务,SqlSessionTemplate 如何管理 SqlSession 生命周期?
答:在非事务环境下,每次执行 SQL 都会新建一个 SqlSession,执行完毕后立即 close(),返回到连接池。这保证了连接及时释放,但每次调用都重复获取连接。
追问 2:SqlSessionTemplate 可以被多个 SqlSessionFactory 共享吗?
答:不能,一个 SqlSessionTemplate 对象在构造时必须绑定一个明确的 SqlSessionFactory。如果需要对多个数据源,需要为每个工厂建立对应的 SqlSessionTemplate。
追问 3:为什么 SqlSessionTemplate 被称为“模板”,而不是直接使用 DefaultSqlSession?
答:模板模式屏蔽了 SqlSession 的获取、关闭、事务同步等复杂细节,使得 DAO 层可以直接注入 SqlSessionTemplate 或通过 Mapper 代理透明使用。它符合 Spring 一贯的模板风格(如 JdbcTemplate)。
加分回答
甚至可以展示 SqlSessionTemplate 构造函数的核心代码:
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
以及 SqlSessionInterceptor 中如何通过 SqlSessionUtils.getSqlSession() 与 Spring 事务抽象协作,这体现了代理模式和模板模式的完美结合。
9.7 @ConditionalOnMissingBean 在 MyBatis 自动配置中起到什么作用?
标准回答
它是“约定优于配置,但可灵活覆盖”的核心机制。在 MybatisAutoConfiguration 中,关键的 @Bean 方法(sqlSessionFactory、sqlSessionTemplate、platformTransactionManager)都标注了该注解,意思是只有当容器中不存在对应类型的 Bean 时,才自动创建。这允许开发者在自己的 @Configuration 类中定义相同类型的 Bean,完全接管该组件的创建,而无需排除整个自动配置类。这是一种策略模式的应用:默认策略是 Boot 自动提供,用户可以通过注册自己的实现来替换。
追问 1:如果用户自定义了 SqlSessionFactory,但忘记自定义 SqlSessionTemplate,会发生什么?
答:自动配置还会尝试创建默认的 SqlSessionTemplate,但由于此时容器中可能已经有多个 SqlSessionFactory 或自定义的那个没有标识为 @Primary,注入时会发生歧义异常。因此,只要自定义了 SqlSessionFactory,通常也需要配套自定义 SqlSessionTemplate。
追问 2:@ConditionalOnMissingBean 如何与 @Primary 交互?
答:@ConditionalOnMissingBean 检查的是容器中是否存在对应类型的 Bean,不考虑 @Primary。如果用户定义了两个 SqlSessionFactory,即使其中一个标注了 @Primary,条件仍然判断为“存在”,自动配置的 sqlSessionFactory 不会创建。但其他依赖注入点(如 sqlSessionTemplate 方法参数)会依据 @Primary 进行选择。
追问 3:有没有办法部分覆盖自动配置,比如只改变 SqlSessionFactory 的某一部分而保留其他默认 Bean?
答:有,可以使用 ConfigurationCustomizer 或者暴露自定义 MybatisProperties 的属性,甚至通过 BeanPostProcessor 在初始化完成后修改,但这些属于不推翻自动配置的细粒度定制。若要从根本上替换,则仍需使用 @ConditionalOnMissingBean 的覆盖方式。
加分回答
讨论 Spring Boot 的其他条件注解组合,例如 @ConditionalOnProperty 可以与 @ConditionalOnMissingBean 联合使用,实现基于配置 switch 的动态组件替换。MyBatis 自动配置没有这样做,但用户可以自行扩展。另外,了解 @ConditionalOnMissingBean 的作用域仅限于当前应用上下文,对父子容器无效。
9.8 MybatisProperties 是如何将 YAML 配置映射到 MyBatis 的 Configuration 对象的?
标准回答
MybatisProperties 是一个 @ConfigurationProperties(prefix = "mybatis") 的 POJO,Spring Boot 启动时将 application.yml 中以 mybatis 开头的属性值绑定到它的字段上。需要使用时,MybatisAutoConfiguration 调用 MybatisProperties.getConfiguration() 方法,内部会延迟创建 org.apache.ibatis.session.Configuration 实例,并使用 Spring Boot 的 PropertyMapper 工具将内部 configuration 子属性安全地映射到该实例。例如,mybatis.configuration.log-impl 会映射到 configuration.setLogImpl(...)。最终,这个 Configuration 对象传递给 SqlSessionFactoryBean。
追问 1:如果配置了 mybatis.config-location 指向 XML,和 mybatis.configuration 同时存在,哪个生效?
答:XML 文件先被 SqlSessionFactoryBean.setConfigLocation() 加载,随后 setConfiguration() 方法传入 MybatisProperties 创建的 Configuration 对象。MyBatis 内部处理时,后设置的属性会覆盖 XML 中的同名设置,所以 mybatis.configuration 优先级更高。若必须保持 XML,则应避免重复设置。
追问 2:MybatisProperties 可以支持未知属性吗?
答:通过 @ConfigurationProperties(ignoreUnknownFields = true)(Spring Boot 默认行为)会忽略未知属性,不会报错。但 Spring Boot 2.x 会在启动时报告位置属性。
追问 3:如果需要将 MyBatis 的配置属性写入一个自定义的 PropertySource,能不能在 @ConfigurationProperties 之前完成?
答:可以,通过 EnvironmentPostProcessor 或 ApplicationContextInitializer 在环境准备时添加 PropertySource,提供的属性可以被 MybatisProperties 识别。
加分回答
可以深入 PropertyMapper 源码,PropertyMapper.from(source).whenNonNull().to(target) 这种流式调用可以避免 source 为 null 时覆盖 target 的默认值,是一种优雅的防御性编程。此外,MybatisProperties 的 getConfiguration() 还传递 configurationProperties 字段中的额外属性到 MyBatis 的 Configuration.getVariables() 中,这些变量可在 XML 映射文件中通过 ${} 占位符引用,增强了动态性。
9.9 如果我想让 MyBatis 同时支持 MyBatis 和 MyBatis Plus,Boot 下该如何处理冲突?
标准回答
MyBatis Plus 的 mybatis-plus-boot-starter 也包含了 MybatisPlusAutoConfiguration,它同样会创建 SqlSessionFactory 和扫描 Mapper,会和原生 MybatisAutoConfiguration 冲突。解决办法通常是排除原生 MyBatis 的自动配置,使用 MyBatis Plus 提供的。具体做法:
- 在启动类上使用
@SpringBootApplication(exclude = MybatisAutoConfiguration.class)。 - 如果使用了
@MapperScan,应替换为com.baomidou.mybatisplus.annotation.MapperScan(兼容 MyBatis 原@Mapper)。 ConfigurationCustomizer仍然可以正常使用,因为 MyBatis Plus 的自动配置也会注入它们。
追问 1:如果不想完全排除,能否让两个自动配置共存?
答:理论上可以手动定义两个 SqlSessionFactory 分别对应原生和 Plus,但实际中 MyBatis Plus 的内核扩展了对映射的处理,混合使用极易导致类型处理、分页等问题,不推荐生产应用。
追问 2:MyBatis Plus 的分页插件在 Boot 下如何自动装配?
答:MyBatis Plus 提供了 PaginationInnerInterceptor,通常通过在 ConfigurationCustomizer 中手动添加,或者定义 MybatisPlusInterceptor Bean 自动添加到 SqlSessionFactory。
追问 3:如果引入了一个第三方库也依赖 mybatis-spring-boot-starter 但我们需要用 Plus,有什么最佳实践?
答:可以在全局剔除原生 starter 的依赖,或者确保 Plus starter 在依赖树中靠前,Maven 的 exclusions 可以排除传递依赖。然后集中使用 MyBatis Plus 的统一配置。
加分回答
分析 MybatisPlusAutoConfiguration 的源码发现,它实际上扩展了 MybatisAutoConfiguration,重用了许多逻辑。它也通过 AutoConfiguration.imports 注册。通过查看自动配置报告可以清晰看到哪个自动配置生效。如果两者都没被排除,通常会因为 SqlSessionFactory 冲突导致启动失败。
9.10 Boot 环境下,手动开启二级缓存需要注意什么?自动配置是否会干扰?
标准回答
MyBatis 的二级缓存是全局开关与局部声明配合生效。自动配置默认创建的 SqlSessionFactory 使用 MybatisProperties.getConfiguration(),其中 cacheEnabled 默认为 true,所以全局开关是开着的。但要真正使用,还需要在 Mapper XML 中添加 <cache/> 标签或在接口上使用 @CacheNamespace。自动配置不会干扰二级缓存,但需要注意:
- 在多数据源下,各
SqlSessionFactory拥有独立的Configuration,因此缓存空间隔离,可能造成缓存不一致。 SqlSessionTemplate管理SqlSession的生命周期,二级缓存的生命周期与SqlSessionFactory相同,是进程级别的,事务提交后才刷新到缓存,注意缓存刷新操作。- 如果自定义了
SqlSessionFactory,必须显式设置configuration.setCacheEnabled(true)并注入自定义缓存实现(如 Redis)。
追问 1:mybatis.configuration.cache-enabled=false 能完全禁用二级缓存吗?
答:可以,全局开关关闭后,所有的 XML 中的 <cache> 也会失效,不会再维护缓存。
追问 2:二级缓存和 Spring 事务结合时,缓存更新时机是怎样的?
答:MyBatis 在 SqlSession 关闭时将缓存数据从一级移到二级。使用 SqlSessionTemplate,当没有事务时,每次调用结束后 SqlSession 关闭,缓存立即被更新;在事务中,SqlSession 会被暂存,直到事务提交后关闭,此时才会刷新到二级缓存,保证了事务内的隔离性。
追问 3:如果使用自定义的 SqlSessionTemplate,缓存行为会不会变?
答:只要底层是通过 SqlSessionFactory 获得的 SqlSession,缓存行为不变。自定义模板通常只是改变了获取策略,二级缓存依然由 Configuration 和映射语句控制。
加分回答
深入讨论 MyBatis 二级缓存的 TransactionalCacheManager 和 TransactionalCache,它们负责暂存缓存并在提交/回滚时清理。Spring 事务同步器会在事务完成后回调,最终触发缓存刷新。如果自定义了缓存实现(如 Hazelcast),需要确保线程安全和序列化支持。
9.11 AutoConfiguredMapperScannerRegistrar 是如何决定扫描范围的?
标准回答
该 Registrar 通过 AutoConfigurationPackages.get(registry) 获取 Spring Boot 自动配置包(即 @AutoConfigurationPackage 注册的包)。@AutoConfigurationPackage 本身通过 @Import(AutoConfigurationPackages.Registrar.class) 在启动时把主启动类所在包存储到 BeanDefinitionRegistry 中。AutoConfiguredMapperScannerRegistrar 在 registerBeanDefinitions 中拿到这些包路径,然后创建 ClassPathMapperScanner,调用 doScan(packages) 进行类路径扫描。
追问 1:如果项目中有多个 @AutoConfigurationPackage 怎么办?
答:Spring Boot 通常只有一个主启动类,因此只有一个 @AutoConfigurationPackage。如果有多个,AutoConfigurationPackages.get() 会返回所有注册的包,可能会导致重复扫描,但不常见。
追问 2:测试环境下包路径不同,自动扫描会生效吗?
答:测试时,@SpringBootTest 会寻找 @SpringBootConfiguration,通常还是主启动类,所以基本一致。如果使用 @ContextConfiguration 指定特定配置类,可能不会自动注册包,可以通过 @TestPropertySource 或显式 @MapperScan 解决。
追问 3:能否在配置文件中通过属性修改自动扫描的包?
答:自动扫描不支持外部属性配置,但可以通过自定义 @MapperScan 并利用 basePackageClasses 或通过 @MapperScan 的占位符 "${mybatis.base-package}" 实现动态配置,后者是显式 @MapperScan 的功能。
加分回答
深入 ClassPathMapperScanner 的 doScan 方法,它不仅扫描 @Mapper 接口,还可以通过 registerFilters() 添加对自定义注解的支持。在自动扫描时,它忽略了 @Component 等 Spring 注解,只寻找 @Mapper。这与显式 @MapperScan 的扫瞄逻辑略有差异:显式 @MapperScan 允许通过 annotationClass 属性指定标记注解。
9.12 (系统设计题)设计一个公司内部的 Starter,它要求能够根据不同环境(dev/test/prod)自动切换 MyBatis 的缓存开启状态和日志输出级别,同时支持多租户插件自动装配。请利用 MyBatis 自动配置的扩展点(如 ConfigurationCustomizer),给出核心的配置与实现思路。
标准回答
核心设计思路:
- 创建一个自定义 Starter,依赖
mybatis-spring-boot-starter,并包含自定义的自动配置类MultiEnvMybatisAutoConfiguration。 - 定义
MultiEnvProperties,使用@ConfigurationProperties(prefix = "mybatis.enhanced")绑定配置:dev、test、prod对应不同的缓存和日志设置,以及多租户插件开关。 - 在自动配置类中,使用
@AutoConfigureBefore(MybatisAutoConfiguration.class),确保在官方配置之前注册必要的ConfigurationCustomizer和EnvironmentPostProcessor(用于提前设定mybatis.configuration属性)。 - 环境适配实现:通过
EnvironmentPostProcessor读取当前激活的 profile,将对应的mybatis.configuration.cache-enabled和mybatis.configuration.log-impl以MapPropertySource形式添加到Environment中(最高优先级),这样MybatisProperties会绑定到正确的值。 - 多租户插件:实现一个
TenantInterceptor implements Interceptor,在 SQL 执行前根据线程局部变量或上下文(如 Spring Security)拼接租户条件。自动配置类定义ConfigurationCustomizerBean,将插件添加到Configuration的拦截器链中。通过@ConditionalOnProperty("mybatis.enhanced.multi-tenant.enabled")控制是否启用。 - 最终用户在项目中引入该 Starter 后,只需在
application.yml配置环境,即可自动获得对应的缓存和日志策略,多租户插件自动列入。
追问 1:如果用户也想自定义日志实现,会不会与 Starter 的 EnvironmentPostProcessor 冲突?
答:EnvironmentPostProcessor 添加的 PropertySource 通常设置为高优先级,会覆盖 application.yml 的配置,因此用户若想覆盖,必须使用更高优先级的属性源(如系统属性)。也可以设计一个开关属性,例如 mybatis.enhanced.log-override=false 来跳过环境自动配置。
追问 2:多租户插件如何与 MyBatis 的二级缓存协同工作?
答:需要对缓存 key 加入租户 ID 以保证隔离,可以重写缓存 key 的生成方式,或者采用租户隔离的缓存实现(如基于租户划分缓存命名空间)。插件中可以在查询前修改 boundSql,同时也应关注缓存的 key 计算入口。
追问 3:这个 Starter 如何保证在 MybatisAutoConfiguration 之前运行?
答:使用 @AutoConfigureBefore(MybatisAutoConfiguration.class) 可确保自定义自动配置类在 MybatisAutoConfiguration 之前被评估。这样 ConfigurationCustomizer Bean 提前注册,或者添加的 PropertySource 在 MyBatis 属性绑定前就位。
追问 4:如何测试这种 Starter 的自动配置?
答:使用 Spring Boot 的 ApplicationContextRunner 对自动配置进行切片测试,模拟不同的 profile 和用户自定义配置,断言 Configuration 中的属性值和插件存在与否。
加分回答
可以进一步设计 @EnableMultiTenant 注解,通过 @Import 导入一个 ImportBeanDefinitionRegistrar 动态注册多租户拦截器,并提供灵活的租户解析策略接口。这套设计展示了 Spring Boot 的条件装配、EnvironmentPostProcessor、ConfigurationCustomizer 和自动配置顺序控制等关键扩展点的综合运用,是公司级基础框架的典型方案。
文末速查表
| 核心组件 | 自动配置类 | 条件 | 核心 Bean | 定制方式 |
|---|---|---|---|---|
| SqlSessionFactory | MybatisAutoConfiguration | @ConditionalOnClass, @ConditionalOnBean(DataSource) | SqlSessionFactory | @Bean覆盖,ConfigurationCustomizer |
| SqlSessionTemplate | MybatisAutoConfiguration | @ConditionalOnMissingBean | SqlSessionTemplate | @Bean覆盖 |
| MapperScanner | AutoConfiguredMapperScannerRegistrar | 无MapperFactoryBean/MapperScannerConfigurer时 | 扫描注册MapperFactoryBean | 使用@MapperScan覆盖 |
| TransactionManager | MybatisAutoConfiguration | @ConditionalOnMissingBean | DataSourceTransactionManager | @Bean覆盖 |
| 属性绑定 | MybatisProperties | @EnableConfigurationProperties | MybatisProperties | 在YAML中配置mybatis前缀 |
| 多数据源定制 | 手动排除自动配置 | N/A | 多组SqlSessionFactory等 | 排除自动配置,手动创建Bean |
总结:Spring Boot 与 MyBatis 的整合是条件装配、自动扫描和属性绑定的一场精密协作。掌握
MybatisAutoConfiguration的内部逻辑,就能在单数据源下享受“零配置”的便利,并在复杂多数据源场景下从容驾驭隔离与调优。