SpringBoot项目整合MyBatis-Plus启动报错的根因排查与修复
作者:菩提风
Spring Boot 3 项目里接 MyBatis-Plus,本来是最常规不过的操作,但很多人第一次把依赖拉进来项目就跑不起来,一启动就卡在 Bean named 'ddlApplicationRunner' is expected to be of type 'org.springframework.boot.ApplicationRunner' 这串报错上。搜到的答案要么让你升级版本,要么让你换 starter,但很少有人讲清楚这个 ddlApplicationRunner 到底是谁注册的、为什么会类型不匹配、怎么一步步确认自己的依赖树没有埋雷。这篇文章就把这个报错从现象到根源到修法完整拆开,你照着走,能少走很多弯路。
1. 报错现场还原:先搞清楚 ddlApplicationRunner 是什么路数
1.1 完整报错长什么样
先摆出最常见的报错形态。使用 Spring Boot 3 整合 MyBatis-Plus,项目启动时控制台会输出类似下面这样一段红色错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
Bean named 'ddlApplicationRunner' is expected to be of type 'org.springframework.boot.ApplicationRunner' but was actually of type 'com.baomidou.mybatisplus.extension.task.DdlApplicationRunner'
Action:
Consider defining a bean of type 'org.springframework.boot.ApplicationRunner' in your configuration.
不同依赖组合下,“actually was of type”后面的内容可能有差异,有时候是某个代理类,有时候是另一个自定义 Runner,但核心特征都一样:Spring 在启动阶段扫描或装配 ddlApplicationRunner 这个 Bean 时,发现它的实际类型和预期类型对不上,直接抛了 BeanNotOfRequiredTypeException 。
注意:如果你看到的是 A bean with that name has already been defined... ,那是另一种兄弟报错,根因和类型不匹配其实同源,都是因为类路径里同时存在了多个 MyBatis-Plus 的自动配置或重复依赖。这篇文章后面会一起讲。
1.2 这个 Bean 到底是干什么的
ddlApplicationRunner 不是普通业务代码里定义的 Bean,它是 MyBatis-Plus 自动配置内部注册的一个 ApplicationRunner 。 ApplicationRunner 是 Spring Boot 提供的扩展点,应用启动完、Spring 容器就绪之后,会自动执行所有 ApplicationRunner 实现类的 run 方法。
MyBatis-Plus 设计了一个 DdlApplicationRunner ,专门负责在应用启动后执行 DDL 脚本相关逻辑。所谓 DDL,就是建表、加字段、改索引这一类数据库结构变更语句。MyBatis-Plus 支持把 SQL 脚本放到特定目录,启动时根据配置决定是否执行,这个执行动作就挂在这个 Runner 身上。
所以你可以把它理解成:项目一启动,Spring Boot 喊了一声“所有善后工作开始”,MyBatis-Plus 派出的代表 ddlApplicationRunner 就站出来说“我的活是处理数据库脚本”。它本身是无辜的,问题出在它被注册进容器时,Spring 已经不认它的身份了。
1.3 什么项目最容易踩到这个坑
根据我平时帮人看报错的经验,遇到这个问题的项目基本都集中在下面几类场景:
第一类,从 Spring Boot 2 升级到 Spring Boot 3 的项目。升级过程中如果只是盲目改了 Spring Boot 版本号,MyBatis-Plus 还是沿用旧的依赖坐标,自然就会触发。
第二类,新项目直接在 Spring Boot 3 里复制了 Spring Boot 2 时代的 MyBatis-Plus 依赖写法,用了 mybatis-plus-boot-starter 这个坐标,而不是专门为 Spring Boot 3 准备的 mybatis-plus-spring-boot3-starter 。
第三类,多模块项目或中大型项目,某个公共模块里引了旧版 MyBatis-Plus,业务模块又引了新版,最后打出来的运行环境里同时有两套自动配置类。
这几种场景的共同本质是:类路径里的 MyBatis-Plus 组件和 Spring Boot 3 容器不匹配, ddlApplicationRunner 只是那个最先露出马脚的报错点而已。
2. 根因分析:类型不匹配背后的生态切换
2.1 Spring Boot 3 换了“地基”,很多东西都变了
Spring Boot 3 和 Spring Boot 2 之间不是简单的版本号递增。Spring Boot 3 基于 Spring Framework 6,整个技术底座发生了几个关键变化。
最明显的是包名从 javax.* 换成了 jakarta.* 。所有依赖 Servlet、JPA、Validation 等 Jakarta EE 规范的库,都必须重新编译并改用 jakarta 命名空间。像 MyBatis 这类深度整合 Spring 生态的框架,如果不做适配,直接拿到 Spring Boot 3 里运行时,很多探针类、工具类都会因为找不到类或方法报错。
另一个变化是自动配置机制更严格了。Spring Boot 3 对自动配置的注册、加载、失效判断都做了一轮整理,配置类之间同名 Bean、同路径覆盖这类问题更容易提前暴露。换句话说,以前 Spring Boot 2 时代“糊里糊涂也能跑起来”的场景,在 Spring Boot 3 里往往直接罢工。
生活化一点比喻:Spring Boot 2 的房间是老式插孔,有些旧电器用转接头插上勉强能用;Spring Boot 3 把整个电气系统换成新的国标,旧插头插进去不仅用不了,还会跳闸。 ddlApplicationRunner 这个报错,就是跳闸后电器烧掉的保护器。
2.2 MyBatis-Plus 专门准备了两个 Starter,别用混
这是这个报错最核心、最直接的触发原因。
MyBatis-Plus 在打包发布时,针对 Spring Boot 2 和 Spring Boot 3 分别提供了不同的 Starter:
| 适用环境 | Maven 坐标 | 自动配置类所在包 |
|---|---|---|
| Spring Boot 2 | com.baomidou:mybatis-plus-boot-starter | com.baomidou.mybatisplus.autoconfigure |
| Spring Boot 3 | com.baomidou:mybatis-plus-spring-boot3-starter | com.baomidou.mybatisplus.spring.boot3.autoconfigure |
Spring Boot 3 专用 Starter 里, MybatisPlusAutoConfiguration 的注册逻辑、Bean 定义方式都做了兼容性调整,包括 ddlApplicationRunner 的类型注册方式。如果你在 Spring Boot 3 项目里依然引入的是旧版 mybatis-plus-boot-starter ,很可能会加载到一套不完全兼容的自动配置。
更麻烦的情况是,项目里同时出现了两个 Starter。比如你显式引了 mybatis-plus-spring-boot3-starter ,但另一个模块通过传递依赖把 mybatis-plus-boot-starter 也带进来了。这时候类路径上两套自动配置同时存在,它们都试图注册名为 ddlApplicationRunner 的 Bean,Spring 的 Bean 定义冲突和类型检查就会密集爆发。
2.3 Spring 为什么报“类型不匹配”而不是“找不到 Bean”
很多新手会困惑:既然报错提示我 Consider defining a bean of type 'org.springframework.boot.ApplicationRunner' ,那我是不是自己定义一个 ApplicationRunner 就行了?答案是不行,至少不能盲目这么干。
要理解原因,得先懂 Spring 的 Bean 类型推断机制。Spring 容器在创建 Bean 时,会通过后处理器和工厂方法来推断 Bean 的类型。对于 MybatisPlusAutoConfiguration 里 ddlApplicationRunner() 这样的 @Bean 方法,Spring 首先读取方法的返回类型,如果能拿到具体类型,就直接登记为这个类型;如果方法返回类型是个接口,或者因为代理、泛型等原因暂时拿不到确切类,Spring 会延迟到实例化阶段再判断。
Spring Boot 3 项目里,如果自动配置类来自旧版 MyBatis-Plus,而容器里又因为版本混杂出现了多个同名的 Bean 定义,Spring 在启动后处理阶段查找每个 ApplicationRunner 时,会按名称从容器中取出 ddlApplicationRunner ,再和预期类型做一次 isAssignableFrom 检查。只要实际类型和预期接口之间差了那么一层,异常就抛出来了。
简单说,这不是“没有这个 Bean”,而是“容器里有个叫这个名字的 Bean,但类型对不上”。所以报错 Action 里的“Consider defining a bean”,对多数人而言是一个误导性极强的提示——你如果真手动定义一个 ddlApplicationRunner ,只会把局面搅得更乱,因为底层 MyBatis-Plus 的脚本执行逻辑还在,你的空实现会把它的功能直接架空。
3. 解决方案:从快速救急到彻底根治
3.1 方案一:换用 Spring Boot 3 专用 Starter,这是首选
这是我最推荐的做法,也是长期维护成本最低的解法。下面的配置可以直接抄。
Spring Boot 3 项目的 pom.xml 里,MyBatis-Plus 依赖应当写成这样:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>版本建议用 3.5.5 或更高。MyBatis-Plus 在 3.5.3.1 之后陆续修复了不少 Spring Boot 3 兼容性问题,包括 ddlApplicationRunner 相关的 Bean 注册问题,所以如果你手头是个旧项目,直接升级到较新的稳定版更稳妥。
换完之后,执行一次全量清理和重新编译:
mvn clean mvn spring-boot:run
如果是 Gradle 项目,依赖写法对应改成:
implementation 'com.baomidou:mybatis-plus-spring-boot3-starter:3.5.5'
这里有个容易忽略的细节:很多项目不止引一个 MyBatis-Plus 构件。比如你为了用代码生成器、分页插件或多租户插件,又单独引了 mybatis-plus-extension 、 mybatis-plus-jsqlparser 之类的依赖。升级时务必要让这些附属构件和 Starter 版本保持一致,不要出现 mybatis-plus-spring-boot3-starter 是 3.5.5、 mybatis-plus-extension 还是 3.5.2 这种混搭。版本不一致经常导致比 ddlApplicationRunner 更隐蔽的诡异错误。
3.2 方案二:用依赖树揪出重复的旧版 MyBatis-Plus
如果你的项目很干净,只有一个 MyBatis-Plus 依赖,那大概率不需要看这一节。但如果你是在一个中大型项目里接手这个报错,我强烈建议先跑一下依赖树,别急着改代码。
mvn dependency:tree -Dincludes=com.baomidou
执行后你会看到类似下面的输出:
[INFO] +- com.baomidou:mybatis-plus-spring-boot3-starter:jar:3.5.5:compile
[INFO] | \- com.baomidou:mybatis-plus-extension:jar:3.5.5:compile
[INFO] +- com.baomidou:mybatis-plus-boot-starter:jar:3.5.3.1:compile
看到两套坐标同时存在,问题就明朗了。一定有某个地方通过传递依赖把旧的 mybatis-plus-boot-starter 带进了 classpath。最脏的情况是:你的另一个模块引了一个第三方封装,那个封装内部又引了旧版 MyBatis-Plus。
排查方法很简单,用依赖树先确认来源,然后在你自己的 pom.xml 里用排除法把它拦掉:
<dependency>
<groupId>某个业务包或中间件</groupId>
<artifactId>某个模块</artifactId>
<version>某版本</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>排除完之后再跑一次 mvn dependency:tree -Dincludes=com.baomidou ,确认只剩一个 MyBatis-Plus Starter,然后再启动。
额外提醒一句:如果用了公司内部私 服,还要注意私 服里是否缓存了旧的构件。有些时候 mvn clean 不够,旧的 jar 可能还在本地仓库里,遇到诡异问题可以先删掉本地仓库中 com/baomidou 目录再重新拉取。这个操作不常用,但遇到“日志里明明显示旧包名”的情况很有效。
3.3 方案三:临时覆盖 Bean 定义,仅供紧急自救
如果你的项目非常老,暂时没有条件升级 MyBatis-Plus,又必须在 Spring Boot 3 上跑通,可以临时用自定义配置覆盖 ddlApplicationRunner 的类型。这个方案我不建议长期使用,但作为急救手段它能让你先启动系统、先排查其他问题。
方法是在任意被扫描到的配置类里,手动定义一个同名 Bean:
@Configuration
public class MybatisPlusFixConfig {
@Bean
public ApplicationRunner ddlApplicationRunner() {
return new ApplicationRunner() {
@Override
public void run(ApplicationArguments args) throws Exception {
// 紧急覆盖,不做任何事情
// 注意:MyBatis-Plus 的 DDL 脚本自动执行能力会因此失效
}
};
}
}
加了这段之后,Spring 容器里会有一个名为 ddlApplicationRunner 、类型为 ApplicationRunner 的匿名内部类 Bean。从类型检查来看,它完全匹配,应用可以启动。
但我必须把丑话说在前面:这样覆盖之后,MyBatis-Plus 自带的 DDL 脚本执行机制就废了。如果你的项目确实用了 MyBatis-Plus 的自动建表、自动脚本执行功能,这些功能在启动后都不会再生效。你只是用“让报错消失”换来了“功能缺失”,绝对不是长久之计。等业务允许时,还是要回到方案一,把依赖彻底修正。
3.4 改完依赖之后,验证不能省
很多人在解决报错之后直接进入写代码环节,结果后面又踩到分页插件或 SQL 初始化不生效的坑,再回头看才发现是升级不完整。所以把依赖修正之后,别急着跑业务,先花几分钟做一套验证。
第一步,确认启动日志完整。重新启动应用,观察是否还有 ddlApplicationRunner 相关报错,同时确认 MyBatis-Plus 的正常启动日志出现,包括 MybatisPlusAutoConfiguration 被加载的信息。
第二步,写一个简单的查询接口,或者直接调用一个 Mapper 方法,确认数据源连接、基础增删改查都正常。
第三步,如果项目里配了分页插件或多租户插件,主动调一次分页查询,确保插件链路没有因为升级而失效。
第四步,可以直接在代码里临时检查一下 Bean 类型,加一个 ApplicationRunner 的注入点:
@Component
public class RunnerTypeCheck {
private final ApplicationContext context;
public RunnerTypeCheck(ApplicationContext context) {
this.context = context;
Object runner = context.getBean("ddlApplicationRunner");
System.out.println("ddlApplicationRunner 类型: " + runner.getClass().getName());
}
}
如果输出的是 com.baomidou.mybatisplus.extension.task.DdlApplicationRunner ,说明 MyBatis-Plus 自己的逻辑还在;如果输出的是你覆盖的匿名类,说明你还在急救方案里。
4. 常见问题速查与实战避坑
4.1 问题排查速查表
把这几年遇到的和这个报错相关的典型问题整理一下,方便你对照排查。
| 现象 | 直接原因 | 处理方式 |
|---|---|---|
Bean named 'ddlApplicationRunner' is expected to be of type... | 类路径里存在不兼容的 MyBatis-Plus 自动配置 | 改用 mybatis-plus-spring-boot3-starter ,统一版本 |
A bean with that name has already been defined... | 两个 Starter 或两份不同版本的 MyBatis-Plus 同时存在 | 跑依赖树,排除旧依赖 |
| 启动后 DDL 脚本没有执行 | 手动覆盖了 ddlApplicationRunner | 移除自定义 Runner,恢复官方自动配置 |
| 升级后分页插件失效 | 只改了 Starter,没核对附属构件版本 | 把 mybatis-plus-extension 、 mybatis-plus-jsqlparser 等版本对齐 |
| 升级后 SQL 初始化或数据源报错 | Spring Boot 3 的配置项和旧版不兼容 | 使用 application.yml 新格式,检查 spring.sql.init 相关配置 |
4.2 升级过程中容易连带出现的坑
除了 ddlApplicationRunner 本身,Spring Boot 3 整合 MyBatis-Plus 时还有几个高频连带的坑,这里一并说了。
第一个是 jsqlparser 相关报错。MyBatis-Plus 的分页、多租户等功能依赖 jsqlparser 解析 SQL。升级到 Spring Boot 3 后,如果你的 Starter 版本较低,可能还会在分页查询时遇到 An API incompatibility was encountered while executing ... 之类的异常。这类问题的解法同样是先升级 MyBatis-Plus 到新的稳定版,确保附属依赖正确。
第二个是 application.yml 配置失效。Spring Boot 3 对部分配置项做了调整,MyBatis-Plus 在 Spring Boot 2 时代常用的 mybatis-plus.config-location 、 mybatis-plus.mapper-locations 这类配置,原则上是兼容的,但如果你同时改用了新版 Starter,记得确认配置前缀和文档一致。
第三个是关于自己定义 ApplicationRunner 的冲突。有些团队的内存初始化、数据预加载也依赖 ApplicationRunner ,如果你项目里已经写了自定义 Runner,又不小心引进了同名 Bean,启动顺序和优先级会变得很乱。遇到这种情况,可以在自定义 Runner 上使用 @Order 明确执行顺序,同时避免自定义 Runner 的名字和框架内部 Bean 名冲突。
4.3 几点实操心得
文章最后聊几句我在实际处理中积累的经验。
第一,看到 Bean named ... is expected to be of type ... 这种报错,第一反应不应该是去定义同名 Bean,而应该去看依赖树。Spring Boot 3 在这种类型冲突上报错报得又直白又隐蔽,直白的是它告诉了你 Bean 名,隐蔽的是它没告诉你这个 Bean 是哪个自动配置注册的。你自己定义的 Bean 只会掩盖问题,不会解决问题。
第二,升级并非“版本越高越好”。虽然我推荐用 3.5.5 以上,但如果你的项目有历史包袱,建议先在测试环境做一次完整回归,包括分页、逻辑删除、自动填充、乐观锁、多租户这些 MyBatis-Plus 的常用特性。版本跨度大的时候,有些配置项的默认行为会变,光看启动日志看不出来。
第三,如果是接手别人写到一半的项目,先看 pom.xml 里 MyBatis-Plus 的坐标,再看整个依赖树。很多报错其实就是前人随手复制了一个旧依赖,根本没有真正处理过兼容性问题。把坐标换成 mybatis-plus-spring-boot3-starter 并统一版本,八成坑都能直接消失。
第四,遇到 Spring Boot 3 相关的问题,先分清是“代码问题”还是“依赖问题”。 ddlApplicationRunner 这种 Bean 类型不匹配,几乎可以认定是依赖或版本层面的问题,别在业务代码里找辙,别在配置类里乱加 Bean。把依赖捋干净,问题自然解开。
以上就是SpringBoot项目整合MyBatis-Plus启动报错的根因排查与修复的详细内容,更多关于SpringBoot整合MyBatis-Plus启动报错的资料请关注脚本之家其它相关文章!
