资讯动态

搞懂MyBatis与Spring Boot整合:两大核心类自动装配原理与排错

发布时间:2026/10/15 19:16:54 来源:尧图企业网站定制
搞懂 MyBatis 和 Spring Boot 的整合原理其实只要抓住两个类MybatisAutoConfiguration和AutoConfiguredMapperScannerRegistrar。我们常用 Spring Boot 的人都知道加一个mybatis-spring-boot-starter就能自动装配 SqlSessionFactory、Mapper 扫描这些能力但真正遇到问题的时候比如 Mapper 没被扫描、多数据源下自动配置失效、加了 MapperScan 还是报 no bean如果不把底层这两个类弄明白排查基本靠猜。我之前接手过一个老项目原来用的是手写 XML 配置和 Spring 手动注册 SqlSessionFactory 的方式后来要迁到 Spring Boot 自动装配过程里踩了不少坑。今天就把我梳理过的原理、源码分析和排查经验一次性写出来。1. 首先理解 Spring Boot 是怎么“自动”把 MyBatis 装配起来的1.1 自动配置的真正入口在 starter 的 jar 包里mybatis-spring-boot-starter表面上只负责引入依赖真正干活的其实是mybatis-spring-boot-autoconfigure这个模块。它内部通过 Spring Boot 的自动配置加载机制把MybatisAutoConfiguration这个配置类注册到容器候选列表里。Spring Boot 的自动配置入口老版本靠META-INF/spring.factories文件里的EnableAutoConfiguration配置项在新版本里则是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。这个机制可以简单理解成Spring Boot 启动时内部会专门找所有AutoConfiguration.imports文件把所有xxxAutoConfiguration类收集起来接着做条件判断满足条件的才真正保留。所以MybatisAutoConfiguration不是被某个Configuration显式引入的而是这个“自动配置收集器”找到的。它会被AutoConfigurationImportSelector统一加载执行顺序在常规用户配置类之后这样用户如果想覆盖默认配置有优先权。1.2 自动配置类上的条件注解决定了它什么时候生效MybatisAutoConfiguration是一个配置类但并不是无脑生效的它头上挂了一堆条件注解。核心几个是ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})类路径里必须有 MyBatis 和 mybatis-spring 的核心类否则自动配置直接跳过。ConditionalOnSingleCandidate(DataSource.class)容器里只能有一个 DataSource 候选如果配了多数据源这个条件不满足自动配置就失效。ConditionalOnMissingBean容器里没有用户自定义的同类型 Bean 时才自动创建。这个设计思路很典型Spring Boot 默认帮你做事情但你一旦自己定义了关键 Bean它的自动配置就主动退让避免冲突。这也是为什么很多老开发者习惯手动写一个SqlSessionFactory类型的Bean方法后自动配置就不再重复创建了。1.3 配置属性绑定到了哪里MybatisAutoConfiguration上还有EnableConfigurationProperties(MybatisProperties.class)把application.yml里以mybatis开头的配置项绑定到MybatisProperties对象。常见的有mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.example.domain configuration: map-underscore-to-camel-case: true cache-enabled: false这些配置最终会在创建SqlSessionFactory的时候被读进去只要理解了属性绑定逻辑后面看它怎么构建工厂就容易多了。2. MybatisAutoConfiguration 内部拆解它到底创建了哪些核心 Bean2.1 宏观结构一个配置类内部还藏了一个 RegistrarMybatisAutoConfiguration本身是一个Configuration类里面定义了多个Bean。但它的内部还嵌套了一个AutoConfiguredMapperScannerRegistrar这个内部类实现了ImportBeanDefinitionRegistrar接口。我先说结论MybatisAutoConfiguration负责把“数据库访问基础设施”搭好比如 SqlSessionFactory、SqlSessionTemplateAutoConfiguredMapperScannerRegistrar负责把 Mapper 接口变成能被注入的 Bean。两者职责不同但是相互配合。2.2 SQL 会话工厂是如何创建的关键Bean方法一般是这样的逻辑Bean ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 设置 mapperLocations // 设置 typeAliases // 设置 configurationProperties return factory.getObject(); }这里有个容易被忽略的点真正实例化 SqlSessionFactory 的不是一个简单的 new而是通过SqlSessionFactoryBean这个实现了FactoryBeanSqlSessionFactory的类。为什么要绕这么一层因为 SqlSessionFactory 本身的配置项特别多像Configuration、mapperLocations、typeAliasesPackage、plugins如果全用构造器拼调用方代码会非常冗长且不可维护。SqlSessionFactoryBean更像一个“工厂和配置的合体”Spring 的 IoC 容器会先创建它再调用其getObject()方法得到最终的 SqlSessionFactory同时它还能校验 XMl 映射文件是否存在。看完源码你就知道MybatisAutoConfiguration做了不少额外处理比如如果MybatisProperties里指定了configuration就用它作为 MyBatis 的 Configuration。如果指定了mapperLocations就把这些 XML 资源注册到 Configuration 里。会把容器里已有的InterceptorMyBatis 拦截器注入到SqlSessionFactoryBean的 plugins 里这就是为什么你即使不在配置里写拦截器只要声明了一个Bean InterceptorMyBatis 也会自动使用它。2.3 SqlSessionTemplate 的职责和为什么有它SqlSessionTemplate是 MyBatis 的核心入口之一它实现了SqlSession接口但你很少直接看到它因为 Mapper 代理在底层调用的是它。MybatisAutoConfiguration也会创建一个SqlSessionTemplate类型的 Bean依赖前面生成的SqlSessionFactory。可以把 SqlSessionTemplate 理解成“MyBatis 和 Spring 之间的适配器”平时我们自己写 MyBatis 代码要手动开SqlSession、提交、关闭而SqlSessionTemplate负责把这些和 Spring 的事务管理结合起来。当方法上标注了Transactional真正让 SqlSession 提交或回滚的是 Spring 事务而不是某一个 Mapper 方法内部临时开的 Session。所以如果你在自己的项目里手动覆盖了SqlSessionFactory一定要同步提供一个SqlSessionTemplate否则依赖SqlSessionTemplate注入的地方会直接报“没有可用 Bean”。这点在后面的排错部分我会再提到。3. AutoConfiguredMapperScannerRegistrar 逐层剖析Mapper 到底是怎么被发现的3.1 没有 MapperScan它是怎么找到 Mapper 的很多人会有个疑问我从没写MapperScan只在接口上加了Mapper注解为什么 Spring Boot 就能识别并注入这些 Mapper答案就是AutoConfiguredMapperScannerRegistrar。这个内部类同样是ImportBeanDefinitionRegistrar它会在容器启动阶段向BeanDefinitionRegistry注册一个MapperScannerConfigurer的 BeanDefinition。MapperScannerConfigurer是一个BeanDefinitionRegistryPostProcessor在容器实例化 Bean 之前进行扫描和注册。它默认做的核心事情是从AutoConfigurationPackages中拿到自动配置的根包名。注册MapperScannerConfigurer的时候把basePackage设为这个根包。把annotationClass设为Mapper.class。交给ClassPathMapperScanner扫包把所有接口扫描出来并注册为 MapperFactoryBean。这里最关键的是第 1 步。Spring Boot 在自动配置时会把主启动类所在的包记录到AutoConfigurationPackages里相当于一个“自动配置包”。MybatisAutoConfiguration内部正是基于这个包作为默认扫描范围所以 Mapper 接口放在启动类所在包或者子包里才能被默认扫到。这也能解释一个高频报错如果 Mapper 接口放在和启动类完全无关的包路径下即使加了Mapper注解它也不会被自动扫描因为默认扫描的根包不对。3.2 registerBeanDefinitions 里的实际逻辑AutoConfiguredMapperScannerRegistrar的核心逻辑简化后是这个样子Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 没有自动配置包就直接返回避免报错 if (!AutoConfigurationPackages.has(registry)) { return; } // 拿到自动配置包的包名列表 ListString packages AutoConfigurationPackages.get(registry); // 创建 MapperScannerConfigurer 的 Bean 定义 BeanDefinitionBuilder builder BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); builder.addPropertyValue(basePackage, StringUtils.collectionToCommaDelimitedString(packages)); builder.addPropertyValue(annotationClass, Mapper.class); // 注册到容器 registry.registerBeanDefinition( AutoConfiguredMapperScannerRegistrar.class.getName(), builder.getBeanDefinition()); }注意它不是直接调ClassPathMapperScanner.scan()而是注册一个MapperScannerConfigurer这符合 Spring Boot 的延迟初始化思路把扫描行为交给后置处理器等到对应的阶段再执行。这个设计使得用户可以仍然通过自定义MapperScannerConfigurer覆盖默认行为。3.3 和 MapperScan 的差异如果用户写了MapperScan(basePackages com.example.mapper)它会引入另外一套机制效果是注册一个MapperScannerConfigurer并把扫描包设置为注解里指定的包。这两套机制最大的差异是MapperScan让你显式指定扫描包灵活但容易因为包路径写错而扫不到。AutoConfiguredMapperScannerRegistrar走默认包路径只扫Mapper标注的接口省心但灵活性不足。它们之间还有一个优先级关系AutoConfiguredMapperScannerRegistrar上带有ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})相关的条件也就是说容器里一旦有用户通过MapperScan或自定义方式注册了MapperScannerConfigurer自动配置的扫描逻辑就不再重复执行。这避免了重复注册 Mapper Bean。4. 根据原理反推实际排查常见问题与解决办法4.1 报错“没有 Mapper Bean”时不要只看注解我在实际项目里最常遇见的错误是Description: Field userMapper in com.example.service.UserService required a bean of type com.example.mapper.UserMapper that could not be found.很多人第一反应是检查Mapper有没有加、MapperScan有没有写。但漏了一个点AutoConfiguredMapperScannerRegistrar的默认扫包路径是启动类所在包。如果项目结构是下面这样com.example.app.MainApplication com.example.data.mapper.UserMapper那么即使UserMapper加了Mapper默认扫描也扫不到因为com.example.data不在com.example.app的包树下。排查步骤我一般这样走看启动类所在位置和 Mapper 接口包路径的关系。看有没有多个MapperScan互相覆盖。看是否存在多个MapperScannerConfigurer导致后注册的覆盖前面注册的。打开debugtrue看自动配置报告确认MybatisAutoConfiguration是否生效。启动时加debugtrue或--debug控制台会打印 “Positive matches” 和 “Negative matches”直接搜 MybatisAutoConfiguration 就能看到它有没有被排除以及被排除的原因。这一步往往比瞎找注解有用得多。4.2 多数据源时自动配置为什么静默失效这是很多人在从单数据源切到多数据源后才会遇到的坑。MybatisAutoConfiguration上的核心条件之一是ConditionalOnSingleCandidate(DataSource.class)。当你配置了多个DataSource并且它们是同一类型时Spring 容器里不再有“唯一候选”的 DataSourceMybatisAutoConfiguration就会自动跳过。跳过之后SqlSessionFactory和SqlSessionTemplate都不会自动创建如果你也没手动配置项目启动可能不会立刻报错但只要有一个地方注入SqlSessionFactory或者某个 Mapper就会报找不到 Bean。解决办法也很典型自己写一个配置类分别给每个数据源创建独立的SqlSessionFactory、SqlSessionTemplate、DataSourceTransactionManager再通过MapperScan指定不同的包扫描到不同的 SqlSessionTemplate。比如Configuration public class MultiDataSourceConfig { Bean public SqlSessionFactory primarySqlSessionFactory( Qualifier(primaryDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSource); return factory.getObject(); } Bean public SqlSessionTemplate primarySqlSessionTemplate( Qualifier(primarySqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }同时两个 Mapper 扫描要分别指定sqlSessionTemplateRef否则 Mapper 不知道用哪个会话。4.3 手动覆盖 SqlSessionFactory 容易踩的连锁问题还有一类问题是“我自定义了 SqlSessionFactory结果 mybatis 配置全失效了”。原因通常在于MybatisProperties里的属性只会在MybatisAutoConfiguration创建 Bean 时读取一遍。一旦你自己写了Bean SqlSessionFactory自动配置不再生成你的手动 Bean 又没读取原来的配置项比如 mapper-locations自然 XML 映射就加载不到。解决方式有两个方向在自己的SqlSessionFactoryBean里手动读取MybatisProperties把需要的配置项逐个设置进去。更推荐的做法不要在启动类所在包额外声明SqlSessionFactory而是用MybatisAutoConfiguration的默认创建只通过application.yml调整配置。如果确实需要拦截器或者自定义对象工厂可以声明对应的Bean让自动配置感知到。5. 针对这套机制的个人实操体会最后分享两个我自己排查时觉得特别有用的细节。第一个是遇到 Mapper 相关的诡异问题时不要急着反编译或改代码先在启动参数加--debug看自动配置的匹配报告。它会把MybatisAutoConfiguration是被哪个条件挡住的直接打出来指向性非常明确。很多时候问题根本不是 Mapper 扫描而是DataSource不唯一连带着整个 MyBatis 自动配置被跳过了。第二个是如果你在项目里用到了多个 MyBatis 插件比如分页插件、逻辑删除插件最好统一通过Bean方式注册成Interceptor而不是在mybatis.configuration里手工写拦截器类名。原因很简单自动配置创建SqlSessionFactory时会把容器里的Interceptor收集起来统一设置如果你手工写字符串方式一旦类名写错或者类路径调整错误会被延迟到真正执行 SQL 时才发现排错成本远高于启动时报错。这也是我后来更倾向于“让自动配置多干活我少干活”的原因。Spring Boot 这套设计虽然把细节隐藏在自动配置里但只要你抓住MybatisAutoConfiguration和AutoConfiguredMapperScannerRegistrar这两个核心理解它们“搭基础设施 扫 Mapper 接口”的分工很多问题就都能从原理层面找到答案而不是停留在盲试不同的注解组合。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑