☰
Spring Boot 3迁移MyBatis-Plus:SqlSessionFactory缺失根因与修复
2026/10/15 5:40:22 网站建设 项目流程

前段时间把一个老服务从Spring Boot 2.7往3.2迁移,顺手把MyBatis也换成MyBatis-Plus,结果启动直接抛了一串异常,核心信息就是Error creating bean with name 'sqlSessionFactory'。但更让人头疼的是,控制台给的提示并不统一——有时是报Bean创建失败,有时是报No qualifying bean of type 'org.apache.ibatis.session.SqlSessionFactory'。这两种日志虽然长得不一样,但都指向同一个根因:Spring容器里根本没有一个可用的SqlSessionFactory实例。而MyBatis-Plus框架的一切能力,比如分页插件、逻辑删除、乐观锁,全都建立在SqlSessionFactory之上,这一步起不来,后面全是白搭。

这篇文章我把整个排查过程、自动配置原理、修复方案,以及修完之后容易踩的连带问题整理出来。不管你是刚把MyBatis-Plus引入项目就报错,还是升级Spring Boot版本后突然挂掉,都可以按这个思路快速定位。内容不绕弯子,直接讲清楚"为什么找不到""怎么排查""怎么修",最后再补几个后续必做检查。

1. 先还原现场:两种看起来像"sqlSessionFactory缺失"的报错

1.1 BeanCreationException与NoSuchBeanDefinitionException的区别

很多人在群里发日志求帮助时,只贴了最后几行异常,但其实第一步要分清你遇到的是哪一种。我自己在多个项目里见过这两种写法,它们的位置不同,说明的问题阶段也不同。

第一种是BeanCreationException:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.ibatis.session.SqlSessionFactory]

这种报错的语气是:容器已经找到了创建sqlSessionFactory的方式(自动配置类存在,也准备执行它),但执行sqlSessionFactory(...)方法时抛了异常。常见原因包括:数据源没配、DataSource类型不对、某个MyBatis-Plus内部组件初始化失败、甚至纯粹是类加载冲突。

第二种是NoSuchBeanDefinitionException:

Parameter 0 of method sqlSessionFactory in com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration required a single bean, but 0 were found: - No qualifying bean of type 'javax.sql.DataSource'

或者更直接:

No qualifying bean of type 'org.apache.ibatis.session.SqlSessionFactory' available

这种报错说明自动配置类压根没有执行,Spring在某个地方需要注入SqlSessionFactory,但扫描了整个容器也没找到一个Bean。常见原因就是自动配置没有触发,或者MyBatis-Plus相关的自动配置类被排除了。

分清这两种报错的意义在于:第一种问题多半出在DataSource、参数装配这些执行细节上;第二种问题多半出在"自动配置没被加载"这个更前置的环节上。排查思路完全不同。

1.2 从启动日志里读出的关键线索

只盯着最后的异常堆栈不够,我把启动日志分成三块来看。

第一块是Spring Boot启动横幅之后的前几十行,里面会有自动配置的判定日志。如果开启了debug: true,能看到类似这样的内容:

MybatisPlusAutoConfiguration matched: - OnClassCondition: found class com.baomidou.mybatisplus.core.MybatisConfiguration - OnSingleCandidateCondition: found single candidate bean for type [javax.sql.DataSource]

如果这里显示did not match,那问题就清楚了——自动配置条件不满足,容器自然没有sqlSessionFactory。

第二块是@MapperScan扫描相关的警告。正常情况下它会把Mapper接口注册成Bean,但如果你Mapper扫描包路径写错了,或者根本没有配置,那启动虽然能过,运行时一调Mapper就崩。这类问题我会在第五节展开。

第三块是BeanFactoryPostProcessor和BeanPostProcessor的注册日志。MyBatis-Plus在很多版本里会注册自己的MybatisPlusBeanPostProcessor,如果在启动日志中能看到它,说明自动配置已经参与进来了。

看日志不是让人去背源码,而是掌握一个原则:自动配置做没做、数据源有没有、扫描路径对不对,这三件事的先后顺序决定了报错形式。

2. MyBatis-Plus的自动配置链路:为什么Spring Boot有责任找到它

2.1 触发加载的三个缺一不可的条件

MyBatis-Plus在Spring Boot体系里并不是通过魔法生效的,它走的是Spring Boot的自动配置机制。自动配置类MybatisPlusAutoConfiguration本身带了一堆条件注解,只有当这些条件全部满足时,它才会创建SqlSessionFactory。我总结成三个核心条件:

第一,classpath里必须存在关键的类。比如SqlSessionFactory、SqlSessionFactoryBean这些类,它们分别来自mybatis和mybatis-spring这两个基础库。MyBatis-Plus的starter会传递依赖引入它们,但如果有人在构建配置里不小心把它们移除了,或者引入了互相冲突的版本,条件就直接不满足。

第二,容器里必须恰好有一个DataSource,而且是可以被自动配置消费的。@ConditionalOnSingleCandidate(DataSource.class)这个条件卡得非常严格,"单个候选"意味着如果容器里有两个DataSource实例,且没有通过@Primary标记谁是主数据源,自动配置会直接放弃。

第三,@MapperScan或者@Mapper注解得能正确扫描到Mapper接口。这个条件并不影响SqlSessionFactory的创建,但会影响到后续的使用,如果漏了,启动不会报sqlSessionFactory缺失,只是运行时找不到Mapper,很多人会在这个地方二次踩坑。

2.2 Spring Boot 2与Spring Boot 3在自动配置注册上的差异

这里有一个特别容易踩坑的分水岭:Spring Boot 2和Spring Boot 3的自动配置注册机制不一样。

Spring Boot 2.7之前,自动配置类是通过META-INF/spring.factories文件里的EnableAutoConfiguration键注册的。Spring Boot 2.7引入了一个新的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件作为替代,但旧路径仍然兼容。问题出在Spring Boot 3上——它直接移除了对spring.factories里自动配置的支持,只认.imports文件。

这就导致一个非常典型的错误:如果你的项目用的是Spring Boot 3,但无意间加了针对Spring Boot 2的mybatis-plus-boot-starter,那么MyBatis-Plus的自动配置类根本不会被扫描到。因为旧starter的包里可能还在用spring.factories注册,即使包含SqlSessionFactory相关的类,Spring Boot 3也不认账。最终表现就是:运行了半天,报NoSuchBeanDefinitionException,说找不到sqlSessionFactory。

所以,排查任何sqlSessionFactory缺失问题,我建议第一步先确认Spring Boot版本和MyBatis-Plus的starter版本是否匹配,这个比检查配置文件和代码都重要。

2.3 一个总被忽视的细节:starter的实际包名与版本矩阵

MyBatis-Plus官方实际上提供过好几个starter,我见过很多人在网上抄依赖时张冠李戴:

场景Maven坐标适用Spring Boot版本
传统Spring Boot 2.xmybatis-plus-boot-starter2.0 ~ 2.7
升级期Spring Boot 2.7+推荐用mybatis-plus-boot-starter并升级到3.5.x以上2.7
Spring Boot 3.xmybatis-plus-spring-boot3-starter3.0+

注意事务:从3.5.3开始,官方还引入了mybatis-plus-jsqlparser这个独立模块,把分页插件需要的JSqlParser拆出去了。如果你用的是较新版本但没有引入这个模块,某些场景下会在运行时抛NoClassDefFoundError。这个坑特别隐蔽,因为它不影响启动,只在执行分页查询时才炸。

我后来习惯直接用一个规则来判断:先看pom.xml里的Spring Boot版本,再去MyBatis-Plus的版本发布页面对照说明了。不要光看starter名字里有没有boot3,还要看具体版本号是否在支持范围内。

3. 逐个击破:这几类高频诱因,按优先级排查

3.1 依赖冲突和版本错配(占比最高)

我在实际排查中,大概七成的情况是依赖问题。不是缺类,就是版本对不上。

最常见的组合错误是:Spring Boot 3项目里引入了mybatis-plus-boot-starter,导致自动配置不生效。第二个常见错误是项目里同时引入了原生MyBatis的starter和MyBatis-Plus的starter。两者会争夺SqlSessionFactory和SqlSessionTemplate这些Bean的定义权,最终结果不可控。甚至会出现一种诡异情况:两个自动配置类各自跑了一遍,先注册的Bean被后注册的覆盖掉,应用启动成功,但MyBatis-Plus的专属功能全部失效,比如分页插件不生效、逻辑删除不生效。

依赖冲突除了肉眼检查pom.xml之外,最有效的工具是Maven的依赖树分析:

mvn dependency:tree -Dverbose -Dincludes=org.mybatis,org.mybatis.spring,com.baomidou

这套命令会把跟MyBatis相关的依赖全部列出来,包括被谁传递引入、版本号是多少。我在这里已经不止一次发现,某个模块传递引入了低版本的MyBatis,把MyBatis-Plus自带版本的类给覆盖了。如果你用的是Gradle,可以用dependencies任务或dependencyInsight来查。

3.2 DataSource没有成为单一候选或缺少主数据源

自动配置类上面的@ConditionalOnSingleCandidate(DataSource.class)真的卡掉了不少人。我有一次接手一个同事的项目,他为了做读写分离,在配置里同时定义了两个DataSource,但只给其中一个加了@Primary。于是启动时报了sqlSessionFactory缺失,他一直在查MyBatis-Plus的配置,完全没想到问题出在数据源上。

如果你在项目里搜一下,能看到多个返回DataSource的@Bean方法,或者@MapperScan里用了SqlSessionTemplate,那就要特别小心。最稳妥的做法是明确指定主数据源:

@Primary @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); }

而且这个@Primary一定要加在作为主库的那个Bean上,不能加在从库上,否则自动配置虽然不会报错了,但业务读写逻辑会对调,这种错误在线上特别难发现。

还有个细节:如果数据源是通过@Bean方法创建的,方法的返回值类型最好声明成具体类型,比如HikariDataSource,而不要一律返回DataSource接口。因为Spring的类型匹配在某些情况下会受泛型和代理影响,声明成具体类型能减少很多"候选中找不到对应类型"的奇怪问题。

3.3 Starter被手动排除或自动配置没被注册

有时是项目里的某个依赖,为了规避冲突,在引入其他组件时主动排除了MyBatis-Plus相关的东西。比如很多缓存组件、分布式事务组件,它们会在排除配置里把mybatis-plus或mybatis写进去。你根本不知道,直到启动报错才去翻。

另外要检查@SpringBootApplication注解上是否有排除项:

@SpringBootApplication(exclude = { MybatisPlusAutoConfiguration.class })

这种代码一般不是故意写的,而是之前排查问题时临时加上的,后来忘了删。一旦排除了自动配置类,sqlSessionFactory就得靠你自己手动创建,否则容器里必然没有。

我的排查方法是直接全局搜索两个关键词:MybatisPlusAutoConfiguration和SqlSessionFactory,看它们在代码里、配置文件里、排除配置里出现了多少次。出现次数越多,越要仔细看上下文。

3.4 自定义SqlSessionFactoryBean时的潜在盲区

还有一部分项目,为了定制MyBatis的行为,老代码里手动定义了SqlSessionFactoryBean。比如有人这样写:

@Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean.getObject(); }

这里有两个盲区。第一,如果SqlSessionFactoryBean没有设置typeAliasesPackage和mapperLocations,就算Bean创建成功,后续Mapper的XML加载也可能出问题。第二,这个方式和MyBatis-Plus的自动配置是不兼容的,因为你手动定义Bean之后,MyBatis-Plus的MybatisPlusAutoConfiguration里面的@ConditionalOnMissingBean注解会判定"已经有Bean了,不需要我创建",于是整个MyBatis-Plus的增强链路都不会初始化,包括分页插件、逻辑删除等全局配置全部失效,但启动又不会直接报错。

这种情况的修复思路不是"修Bean定义",而是:尽量删掉手动定义的SqlSessionFactoryBean,让MyBatis-Plus的自动配置接管一切。如果实在有特殊场景必须手动定义,也建议直接用MybatisSqlSessionFactoryBean(MyBatis-Plus提供的Bean类)来创建,而不是用原生MyBatis的SqlSessionFactoryBean。

4. 修复方案的选择逻辑与操作细节

4.1 优先复位自动配置:改依赖而非手动造Bean

排查了一圈之后,真正的修复动作往往是改依赖,而不是在代码里手动写一个Bean去"补"缺失的对象。

我见过一拨人,包括早期的我自己,习惯用"缺什么补什么"的思路:报sqlSessionFactory缺失,那我就在配置类里手动生成一个。短期内确实能通过启动,但问题是:你手动生成的SqlSessionFactoryBean和MyBatis-Plus的全局配置是两条线,后续会遇到"插入的时候主键不返回""分页查询完全不限制条数"这类更隐蔽的问题。

所以正确的思路是反过来的:先保证MyBatis-Plus的自动配置能按原方案正常执行,把环境因素清干净,而不是绕开自动配置。

操作上最简单的两步:

  1. 确认Spring Boot版本对应的starter坐标正确。
  2. 删除项目里用@Bean方式手动定义的SqlSessionFactory和SqlSessionTemplate(如果有)。

这两步做完,让MyBatis-Plus的MybatisPlusAutoConfiguration自己把Bean建好。绝大多数情况下,它的默认行为就是项目想要的行为。

4.2 依赖仲裁实操:用exclusion清掉原生MyBatis

如果项目里同时存在原生MyBatis的starter和MyBatis-Plus的starter,我的建议是,优先保留MyBatis-Plus这一个来源,在引入其他模块时排除掉原生MyBatis相关的传递依赖。

Maven的写法:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> <exclusions> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> </exclusion> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> </exclusion> </exclusions> </dependency>

不过要说明的是,这种排除写法并不是必须的。MyBatis-Plus的starter本身就会传递引入一套它自己测试过的MyBatis版本,如果你在项目里又显式声明了不同版本,就要靠exclusion来消除冲突。我的习惯是:先执行依赖树命令,看清楚冲突事实之后再做排除,不要盲目看到org.mybatis就排掉,否则可能误伤其他组件。

4.3 多数据源场景下明确@Primary的正确位置

如果你已经在用多个数据源,而且不太方便改成单数据源,那就要确保主数据源上明确标注了@Primary。这不仅是解决启动问题,也是给后续的事务管理、自动配置打底。

除了@Primary之外,还有一个常见的坑是自动配置试图用DataSourceProperties去绑定配置项,如果你的主数据源是通过自定义类创建的,可能没有配套的配置绑定,启动时虽然不会报缺失,但某些参数会取默认值。比如连接池大小、连接超时时间。我在过一次@ConditionalOnSingleCandidate报错的排查,最终发现数据源不止两个,而是三个,其中一个还是由某个第三方框架在内部自动注册的。这种东西光看自己的代码根本发现不了,必须查ApplicationContext中所有DataSource实例。

如果你用的是spring-boot-starter-jdbc或spring-boot-starter-data-jpa包,也可能间接注册了一些DataSource相关组件,导致MyBatis-Plus判断候选不唯一。

4.4 修改后的验证:如何在日志里确认自动配置已经生效

修复完成后,我先做两个验证。第一个是启动验证,第二个是功能验证。

启动验证时,我会在application.yml里临时开启动态配置日志:

debug: true

或者更精准一点,只开启自动配置条件的日志:

logging.level.org.springframework.boot.autoconfigure=DEBUG

启动后搜索MybatisPlusAutoConfiguration,确认下面有这样几行:

MybatisPlusAutoConfiguration matched: - @ConditionalOnClass found required classes - @ConditionalOnSingleCandidate found a primary bean from type [DataSource]

这说明自动配置的判定逻辑已经走通了。

功能验证时,写一个最简单的Mapper接口,执行一次单表查询,确认能查到数据,然后测试分页插件是否生效。如果分页插件没生效,查一下MybatisPlusInterceptor有没有被注册进容器。

5. 修好之后也别急着走:三个高频连带问题

5.1 页面能启动但SQL报错:mapper扫描包与XML路径检查

启动成功只是第一步,接下来大概率会遇到"应用起来了,但一调用Mapper接口就报Invalid bound statement (not found)"。

这个问题和sqlSessionFactory缺失是"兄弟问题"——厂里没有Bean,业务跑不起来;厂里有Bean但找不到SQL映射,业务也跑不起来。

排查时看两处:

第一处,Mapper接口的扫描路径。@MapperScan("com.example.module.mapper")这里的包路径必须包含所有Mapper接口,否则它们不会注册为Bean。很多人的包结构是com.example.project.common.mapper、com.example.project.order.mapper这种分散式的,只扫描一个包就会漏掉另一个包。要么把路径改成共同的父包,要么写多个@MapperScan。

第二处,XML文件的位置和mapperLocations配置。MyBatis-Plus默认会去classpath的/mapper/**/*.xml位置找XML文件,如果你的XML放到其他目录,要配合配置:

mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml

注意这个classpath*:的写法,多模块项目尤其需要,它表示在所有jar包的classpath里搜索。只用classpath:的话,只能匹配当前模块。

5.2 配置了分页插件却无效:拦截器注册顺序问题

修好sqlSessionFactory之后,很多人会紧接着发现分页插件不生效。比如执行selectPage,返回的Record总数是0,或者干脆查出了全表数据。

常见原因有两个。

第一个,MybatisPlusInterceptor没有被Spring容器管理。你在某个配置类里写了一个@Bean方法,但配置类没有被Spring扫描到,或者方法没有被@Bean标记,导致容器里没有这个拦截器。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

第二个,拦截器本身存在,但生效顺序不对。MyBatis-Plus的分页插件依赖于PaginationInnerInterceptor的执行顺序,如果同时注册了多个InnerInterceptor,顺序写错了,后面的插件可能被前面的拦截器短路掉。这个顺序在代码里是addInnerInterceptor的次数决定的,先添加的先执行。多个插件同时存在时,一般把分页放在偏后的位置。

还有些人在启动后修改了插件列表,或者用了多个@Bean方法返回同一个类型,这会导致Spring容器随机选择一个,最终行为不可预测。我建议全项目里只保留一个MybatisPlusInterceptor的Bean定义。

5.3 版本升级带来的隐性问题:jsqlparser与模块分割

如果你的SqlSessionFactory缺失恰好是升级MyBatis-Plus版本引发的,那还有一个隐藏问题值得提前排查:新版MyBatis-Plus把JSqlParser拆成了独立模块。从3.5.3开始,PaginationInnerInterceptor依赖的JSqlParser不再默认传递引入。

表现就是应用能正常启动,连数据库也能正常访问,但第一次执行分页查询或者某些复杂的条件构造器时,抛NoClassDefFoundError: com/github/jsqlparser/parser/CCJSqlParserUtil。

解决方案很简单,检查MyBatis-Plus版本,如果用的是3.5.3及以上,并且确实用到分页插件,手动补一个依赖:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.7</version> </dependency>

这里版本号不用完全对齐主包的版本,但最好用同一个发布线的最新小版本,避免出现接口不兼容。

6. 再分享一个小习惯:每次改完依赖,我都会顺手做一次干净构建

排查sqlSessionFactory这类问题,最怕的是本地IDE里用了旧的增量编译,改了依赖但类没重新加载,启动还是报同样的错。我被这个坑浪费过不少时间,后来形成了固定习惯。

凡是涉及依赖坐标修改、版本号调整的操作,我不用IDE的重启动,而是直接在命令行执行:

mvn clean compile

Gradle项目就是:

gradle clean compileJava

清理之后再启动应用。如果命令行构建能通过,启动也没报错,那基本可以排除"本地环境有脏缓存"的因素。之后再去IDE里跑。

还有一个习惯是,遇到报错后不要马上改代码,先花三分钟看完整启动日志。日志里自动配置的判定输出,其实已经告诉你问题的七八成原因了,剩下的才是动手修改。很多排查方向都是被种草的"报错文本"带偏的,比如看到sqlSessionFactory就以为要手动创建Bean,实际上真正的原因只是依赖没配对。

最后说一句实在话:把SqlSessionFactory交回给MyBatis-Plus自动配置,然后用@MapperScan让Mapper接口各归其位,这才是Spring Boot项目里最省心的状态。除非有极其特殊的诉求,否则不要轻易手动接管它的Bean创建流程。毕竟你手工搭建的那套东西,后续维护成本远比想象中高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询