Spring Boot 3整合MyBatis-Plus时,如果启动日志里出现Bean named 'ddlApplicationRunner' is expected to be of type ...这种Bean类型报错,恭喜你,遇到了老项目升级时最经典的一个坑。我刚把项目从Spring Boot 2.7升到3.2那会儿,也在这上面卡了小半天,网上搜到的回答要么让你换starter,要么让你删配置,但很少有人讲清楚这个ddlApplicationRunner到底是谁注册的、为什么Spring Boot 3才会炸、以及换完依赖之后还会不会有连锁问题。这篇就把我的完整排查过程和解决方案整理出来,给正在踩坑的朋友一个能直接落地的参考。
1. 报错的完整形态与触发场景
1.1 先看清错误日志到底说了什么
这类报错通常长这样:
*************************** APPLICATION FAILED TO START *************************** Description: Bean named 'ddlApplicationRunner' is expected to be of type 'org.springframework.boot.ApplicationRunner' but was actually of type 'org.springframework.boot.Runner' Action: Consider injecting the bean as one of its interfaces or forcing the bean's declaring beanFactory to treat them as 'ApplicationRunner'.如果你的日志和我当时看到的基本一致,说明项目里至少有两个版本的Runner类同时存在,或者某个自动配置类注册ddlApplicationRunner这个Bean时,声明的返回类型和实际实例类型对不上。Spring Boot 3的容器会在启动阶段做严格类型校验,发现类型不符合预期就直接抛异常阻止启动,不像Spring Boot 2.x很多场景下睁一只眼闭一只眼,Bean类型不对也只是启动后行为异常,不会爆得这么明显。
1.2 这个错误通常在什么阶段出现
它不是在最开始解析配置文件就报出来的,而是在SpringApplication.run()执行到Runner收集阶段才会触发,此时Spring容器已经完成了大部分Bean的创建和注入,眼看着就要执行CommandLineRunner和ApplicationRunner了,结果发现ddlApplicationRunner这个Bean的声明类型和实际类型对不上,整个启动流程就崩在这里。
我当时项目的情况是客户现场急着要Spring Boot 3.2的适配版本,服务里用到了MyBatis-Plus的分页插件和自动填充功能。从2.7升到3.2之后,第一轮启动直接撞上这个报错,根本没机会验证其他功能。
1.3 为什么这个报错容易让人走弯路
因为ddlApplicationRunner这个名字太有迷惑性,大部分人会第一反应去搜"ddl是什么配置",然后被带到MyBatis-Plus的DDL自动建表功能里去。这条路不是完全无关,但真正的病根往往不在这一层,而是在MyBatis-Plus老版starter对Spring Boot 3的兼容问题。
另一个容易让人绕路的原因是这个报错在不同版本里的文本细节不一样,我看到过有人报expected to be of type 'org.springframework.boot.ApplicationRunner',也有人报反过来的expected to be of type 'org.springframework.boot.Runner'。这两种表述本质是同一类问题:容器里某个Bean的名称是ddlApplicationRunner,但Spring Boot 3要求的Runner接口类型和你当前实际加载到的接口类型不是同一个,或者声明的返回类型与实例类型不一致。
2. 根因拆解:Runner接口的变化与依赖坐标冲突
2.1 ddlApplicationRunner这个Bean是谁注册的
这个Bean来自MyBatis-Plus的DDL模块,准确说是MybatisPlusDdlAutoConfiguration这类自动配置类注册的。它内部会创建DdlApplicationRunner,作用是在应用启动完成后扫描并执行DDL脚本,服务于MyBatis-Plus的自动建表、自动加字段功能。你在application.yml里配置了类似mybatis-plus.global-config.db-config.ddl-auto之类的开关时,这个Runner就会被激活。
问题在于,旧版本MyBatis-Plus的starter在注册这个Bean时,Runner相关的类型引用写的是老版本形态,或者引用的Spring Boot包路径在新版本里已经被调整。Spring Boot 3.0之后整个基础设施做了大改,尤其是Spring Framework 6引入了jakarta.*命名空间,很多为Spring Boot 2.x设计的自动配置类在新容器下注册Runner Bean时,就会出现类型层面的错位。
2.2 Spring Boot 3对Runner机制做了什么调整
Spring Boot应用启动到最后阶段,会从容器里找出所有实现ApplicationRunner和CommandLineRunner接口的Bean,逐个执行它们的run方法。这个机制从Spring Boot 1.x就有,但Spring Boot 3.2之后对Runner的收集和排序逻辑做了一次重构,对Bean声明的类型和它的实际类型之间的一致性检查变得更严格。
打个比方,Spring Boot 2.x像是学校门口签到的保安,你说你是学生他就让你进去,哪怕你其实是老师。Spring Boot 3.2之后换了个较真的保安,不仅要看你说自己是谁,还要掏出证件核对你的实际身份。ddlApplicationRunner这个Bean声明自己是个ApplicationRunner,实际拿出来的类却仍然是旧接口形态,于是就被拦在了门外。
2.3 更深层的原因是依赖坐标用错了
绝大多数触发这个报错的项目,pom里用的还是这个坐标:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>这个starter是为Spring Boot 2.x设计的,虽然也能勉强在Spring Boot 3下被加载,但内部的自动配置类沿用旧逻辑,Runner类型处理就暴露问题了。Spring Boot 3的项目应该使用的是官方专门适配的坐标:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>如果你把mybatis-plus-boot-starter和mybatis-plus-spring-boot3-starter同时引进了pom,那更是雪上加霜,容器里会出现两套MyBatis-Plus自动配置逻辑,Runner Bean可能被重复注册,类型冲突直接升级成BeanDefinitionStoreException,报错信息就更难看了。我当时检查依赖树第一眼瞄到的就是这个双坐标同时存在的问题,直接先干掉了一个。
3. 方案一:升级到官方适配Spring Boot 3的starter坐标
3.1 正确姿势:先清理pom里的旧坐标
如果你的项目还不需要保留特殊旧依赖,最省事的解法就是换坐标加升级版本。先通过Maven的依赖树看一眼当前MyBatis-Plus相关组件都有哪些:
mvn dependency:tree -Dincludes=com.baomidou:*建议把输出结果对一下,重点确认以下几点:
mybatis-plus-boot-starter和mybatis-plus-spring-boot3-starter不能同时出现,只能留后者;mybatis-plus-extension不能被低版本的其它传递依赖拉进来,否则还是会出现旧Runner类;- 如果项目里用到分页插件,还需要检查
jsqlparser的版本是否正常解析到了。
3.2 推荐的最低版本组合
我这里直接给一套经过实测的组合,JDK 17、Spring Boot 3.2.x环境跑通:依赖只保留mybatis-plus-spring-boot3-starter,版本建议选3.5.5以上,我最后用的是3.5.7,完全稳定。如果你愿意追一点新版本,3.5.9之后的版本也可以,但要注意3.5.9把SQL解析相关的依赖做了一次拆分,分页插件单独依赖了jsqlparser,需要额外引入:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.9</version> </dependency>如果你是3.5.9之前的版本,不需要第二个依赖,jsqlparser会由starter自动传递进来。
3.3 替换坐标之后还要检查MapperScan和插件配置
换掉starter之后,很多项目会发现@MapperScan的路径如果没写对,Mapper Bean根本扫不进来,表现就是启动不报错,但一调用Mapper方法就报Invalid bound statement。这不是新问题,但如果项目之前是靠自动配置扫描包的,升级后建议显式检查@MapperScan指向的包路径是否仍然和Mapper接口所在包一致。
分页插件那个配置我顺便多说一句,Spring Boot 3下要重新注入MybatisPlusInterceptor这个Bean,并且在里面加入PaginationInnerInterceptor,注意指定数据库类型:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(-1L); interceptor.addInnerInterceptor(pagination); return interceptor; } }这里setMaxLimit(-1L)是解除单页500条限制用的。网上经常看到有人问MyBatis-Plus分页到了第500条之后就查不出来,多半就是没关这个默认限制。很多项目把接口升到Boot 3后发现分页行为也变了,有一部分原因就在这里。
4. 方案二:不想换坐标的临时兜底方案
4.1 排除掉MyBatis-Plus的自动建表功能
如果项目对MyBatis-Plus的DDL自动建表功能没有依赖,或者本来就只用了单表CRUD和分页,那可以先通过排除自动配置的方式把这个麻烦绕过去。核心是让MybatisPlusDdlAutoConfiguration这个自动配置类不生效,ddlApplicationRunner就不会被注册了。
一种排除方式是启动类上排除:
@SpringBootApplication(exclude = { com.baomidou.mybatisplus.autoconfigure.MybatisPlusDdlAutoConfiguration.class }) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }另一种是配置文件里排除,适合在多个启动类里共用一套配置的情况:
spring: autoconfigure: exclude: - com.baomidou.mybatisplus.autoconfigure.MybatisPlusDdlAutoConfiguration这样处理的代价是MyBatis-Plus自动建表、字段补齐这类能力就没了。如果你公司项目的建表流程本来就走Flyway或者Liquibase,那这个排除没什么损失,甚至更符合生产规范。真正依赖MyBatis-Plus自动建表的小型项目,我不建议长期停留在这个方案上。
4.2 手动注册一个类型正确的同名列覆盖旧Bean
还有一个偏hack的做法,就是通过自定义配置类注册一个类型完全正确的同名Bean,让它把旧自动配置的定义覆盖掉。Spring Boot同一个容器里Bean同名时,按后加载的配置为准,我们可以利用这一点兜底:
@Configuration public class DdlApplicationRunnerOverrideConfig { @Bean public ApplicationRunner ddlApplicationRunner() { return args -> { // 这里可以什么都不做,或者将旧DdlApplicationRunner的核心逻辑手动执行一遍 log.warn("ddlApplicationRunner overridden by custom ApplicationRunner"); }; } }这种做法能让项目先启动起来,表面看问题消失了,实际隐患也埋下了:MyBatis-Plus DDL模块原本的初始化逻辑被完全跳过。如果你还需要@TableName标注的实体类自动建表,这个方案会让你的表永远建不出来。所以我的判断是只适合临时救火,不适合当长期方案。
4.3 两种方案怎么取舍
我自己的倾向很明确:能升级starter就升级starter。临时的绕行方案只能解决眼前启动失败的问题,不能解决Boot 3下的兼容性隐患。尤其项目组如果后续还想上MyBatis-Plus的新功能,旧坐标迟早要换。如果你在客户现场被压得比较急,可以先排除DDL配置让服务跑起来,夜里再找窗口处理依赖升级。
这种级联问题在Spring Boot升级过程中其实特别常见,根因往往不在最表面的报错。所以不管用哪个方案,我建议都先留出半天窗口做回归测试,重点测分页、多租户、逻辑删除、审计填充这几个高度依赖SQL解析的功能。
5. 验证方式与常见的衍生坑
5.1 启动验证不要只看"能起来"
换了starter之后,启动不报错只是第一步。我建议在项目里临时加一个接口或者测试用例,显式输出容器中所有ApplicationRunner和CommandLineRunner的Bean列表,确认ddlApplicationRunner的类型已经正确:
@Autowired ApplicationContext context; @PostConstruct public void printRunners() { for (String name : context.getBeanNamesForType(ApplicationRunner.class)) { System.out.println("ApplicationRunner: " + name + " -> " + context.getBean(name).getClass().getName()); } for (String name : context.getBeanNamesForType(CommandLineRunner.class)) { System.out.println("CommandLineRunner: " + name + " -> " + context.getBean(name).getClass().getName()); } }正常情况下列表里不会有ddlApplicationRunner这个名字,因为新版本starter已经不再用旧的Runner类型,或者干脆把DDL Runner合并到了别的执行链路里。如果这个名字还在且类型是ApplicationRunner,也不算错误,但你要确认它的实际行为可接受。
5.2 分页失效是升级后最隐蔽的坑
报错解决后,很多项目会踩进下一个坑:分页不生效。表现通常是Page对象返回的total是0,或者records集合里塞进了全部数据但总条数不对。原因大部分是在Spring Boot 3的环境下,MybatisPlusInterceptor没有被正确注册为Bean,或者注册了Interceptor但缺少jsqlparser依赖。
还有一种情况需要注意,3.5.9以后官方做了模块拆分,如果项目从3.5.7直接升到3.5.9,没补mybatis-plus-jsqlparser,那么PaginationInnerInterceptor启动时不会直接报错,但运行时无法完成SQL的分页解析,表现出的症状非常像"分页失效"。这种坑不看依赖树是找不到原因的,所以我强烈建议升级后第一时间跑一条分页SQL确认。
5.3 MySQL 1064等SQL相关报错的关联
顺带提一下另一个高频搜索词“mysql1064报错怎么解决”。如果你在分页插件场景下看到1064语法错误,先别急着检查SQL语句本身,确认一下PaginationInnerInterceptor配置的DbType是否和实际数据库一致。我见过有人连接的是MySQL,结果配置里写成了DbType.OTHER,分页SQL拼出来之后语法完全不对,报错信息像极了手写SQL写错了。这个和当前报错虽然不直接相关,但在Spring Boot 3重塑依赖的过程中非常容易一起出现,建议排查顺序是:先确认分页插件注册,再确认DbType,最后才去抠具体SQL。
5.4 依赖树检查的实操细节
用一个命令把MyBatis-Plus家族的全部依赖拉出来看:
mvn dependency:tree -Dincludes=com.baomidou:mybatis-plus*如果输出里同时出现了mybatis-plus-boot-starter和mybatis-plus-spring-boot3-starter两兄弟,请务必删掉前者。也可以查org.springframework.boot:spring-boot-autoconfigure的版本,确认Spring Boot的Runner机制是3.2以前还是以后,这对预判是否还会触发类型强校验有帮助。
6. 我的实战体会与避坑清单
6.1 不同版本组合的横向对照
| 组合 | 启动结果 | 建议 |
|---|---|---|
| Spring Boot 2.7 + mybatis-plus-boot-starter 3.5.1 | 正常 | 老项目可以先不动,别主动升Boot |
| Spring Boot 3.2 + mybatis-plus-boot-starter 3.5.3.1 | 大概率触发ddlApplicationRunner类型报错 | 尽快切换到boot3 starter |
| Spring Boot 3.2 + mybatis-plus-spring-boot3-starter 3.5.7 | 正常 | 当前最稳的搭配 |
| Spring Boot 3.2 + mybatis-plus-spring-boot3-starter 3.5.9 + mybatis-plus-jsqlparser | 正常 | 新项目可优先采用 |
这个对照表是我自己项目里实践过的结论,不是从某个文档抄来的。Spring Boot 3.3和3.4我也在别的分支上试过,只要starter版本够新,ddlApplicationRunner这个类型冲突基本不会再出现。
6.2 排查顺序建议总结
这里我把自己的排查顺序整理了一遍,方便你已经改了一半又重新看这篇文章时快速对照:
- 先看依赖树,确认是不是同时存在两个starter,先做减法;
- 确认项目Spring Boot版本是不是3.2以上,如果是,优先考虑换
mybatis-plus-spring-boot3-starter; - 如果项目里还有老代码手动注册了
Runner类型的Bean,检查有没有和ddlApplicationRunner重名; - 启动验证通过后,马上测分页、逻辑删除、自动填充三个功能;
- 最后再做一轮全量回归,重点关注涉及SQL解析的接口。
6.3 最后分享两个小技巧
第一个是不要一上来就排除SpringAutoConfiguration,这种操作虽然能让服务跑起来,但随着后续组件增多,排除列表会越滚越大,最后变成一团乱麻。第二个是我个人比较推荐的一段配置,放在application.yml里作为新项目的基线:
mybatis-plus: global-config: db-config: ddl-auto: none configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplddl-auto设置成none的意义在于,让DDL Runner即使存在也不执行建表动作,建表的事情交给专门的迁移脚本去管。这样即便下次再出Runner相关的幺蛾子,也仅仅是启动日志里多一个无害Bean,而不会暗地里把生产库的表结构改了。日志级别的StdOutImpl只在联调阶段开,上线前记得换回Slf4jImpl或者直接关掉。
这个报错说到底还是给Spring Boot 3适配过程中交的一笔学费。把依赖坐标理顺、理解Runner机制的校验逻辑、顺手确认一下分页插件和新版SQL解析依赖的关系,基本上就能把这一串问题从根上解决,后面再升级版本也不会慌。