兄弟们,如果你正在用 MybatisPlus,某个新项目接手时BaseMapper里的方法全都给我报Invalid bound statement (not found),别慌,这大概率不是你的 SQL 写错了,更不是数据库连接断了。这个错我看着眼熟,因为我自己在早年间的项目里被它折磨过整整一个下午,后来把原理捋清楚之后才发现,这玩意儿其实就几种固定套路,挨个排查基本十到十五分钟能定位。今天就把我的经验完整拆开讲,顺便把我踩过的一些“非典型”的坑也给你摊开看。
先说清楚这篇文章适合谁看:刚接触 MybatisPlus 的新手、在 Spring Boot 项目里遇到过 Mapper 方法绑定失败的老哥,以及正在接手一个“别人写的、突然启动就报错”的历史项目的朋友。我不会上来就让你删 XML 文件碰运气,而是带你按一条有序的路子走一遍,保证你看完能自己动手解决。
1. 别急着改代码,这个报错到底在抱怨什么
1.1 一次 SQL 绑定失败的底层事实
MyBatis 的核心工作方式,是把“接口方法”和“要执行的 SQL 语句”做一个绑定关系。往细了说,MyBatis 启动时会扫描你写的 Mapper 接口,解析接口里的每个方法,再去找到对应的一段 SQL 描述,这个 SQL 描述可能来自 XML 文件,也可能来自注解,或者是 MybatisPlus 自动注入的那一整套 BaseMapper 内置 SQL。绑定成功之后,框架把这些信息装进一个叫MappedStatement的东西里,运行时你调用selectById(1L),框架就直接从这个MappedStatement里取 SQL 执行。
如果运行时框架发现:你这个接口方法是存在的,但对应的方法找不到任何 SQL 描述,就会抛出Invalid bound statement (not found)。翻译成人话就是:方法签名我看到了,但和它配套的 SQL 死活找不到。
这里有一个很多人会忽略的细节:MyBatis 的报错是启动时未必报,往往是你在 Service 里第一次调用这个方法时才炸出来。原因是接口扫描和绑定过程发生在 Spring 容器启动阶段,但真正触发绑定校验是在执行调用时。所以你经常会看到项目能正常启动、Controller 也能注入,直到用户点了一个功能,日志里才蹦出一整行红字。这也是很多人第一反应会往“运行时数据有问题”上猜的原因,实际上从头到尾就是配置或绑定层面的问题。
1.2 “BaseMapper 方法全部失效”和“自定义方法失效”不是一回事
很多人一看到标题里的“BaseMapper 中的方法全部 Invalid bound statement”,就下意识觉得是 MybatisPlus 这个框架出了 Bug。但在我实际排查的经验里,这个现象可以分成完全不同的两类。
第一类:你自定义的 mapper 方法(比如List<User> selectByStatus(Integer status))能正常用,唯独BaseMapper自带的selectById、selectList、insert这些方法一调用就抱错。
第二类:所有方法都报错,包括自定义的和继承 BaseMapper 的。
如果是第一类,你要优先怀疑的是“用户的 Maapper 接口有没有正确继承 BaseMapper”以及“MybatisPlus 的 SQL 注入器有没有生效”。如果继承关系写错,比如搞了个泛型错位,或者写成了extends BaseMapper但泛型没有指明实体类型,MybatisPlus 无法知道该注入哪张表对应的 CRUD SQL,就会导致 BaseMapper 方法绑定失败。
如果是第二类,多半是接口压根没被正确注册成 MyBatis 的 Mapper,或者 Spring 容器里管理它的 SqlSessionTemplate 不是 MybatisPlus 的那套。这种时候不是某个方法的问题,而是整个 Mapper 的代理对象就没有被正确构建。
你自己先对号入座一下,能省很多时间。
2. 从四个维度做根因排查
2.1 一看扫描范围:Mapper 接口有没有被 Spring 管理
第一个要确认的是 Spring 到底有没有把你的 Mapper 接口扫进来。
在 Spring Boot 项目里,常见的扫描方式有三种:启动类上的@MapperScan("com.xxx.module.mapper")、接口上的@Mapper注解、以及配置类里手动注册MapperScannerConfigurer。这三种方式都行,但容易出问题的是扫描路径写错了或者接口没放在预期的包下。
一个典型的场景是:你把一个 Mapper 接口放在com.company.project.mapper包,但@MapperScan写的是com.company.project.dao,接口没被扫到,运行时就找不到绑定关系。更隐蔽的是多模块工程里,某个公共模块里定义了 Mapper 接口,Consumer 模块并没有额外添加对这个包的扫描,结果就是接口没注册。
怎么验证?第一看启动日志。Spring Boot 启动时,如果接口被识别为 Mapper,会打出一行类似ClassPathMapperScanner的日志,里面会列出扫描到的接口。没看到就要查路径。第二看 Spring 容器里有没有这个 Bean。可以在启动类里临时放一个
@Bean public CommandLineRunner check(ApplicationContext ctx) { return args -> { String[] names = ctx.getBeanNamesForType(UserMapper.class); System.out.println("UserMapper bean: " + Arrays.toString(names)); }; }如果打印出来是个空数组,说明你的 Mapper 接口连 Bean 都不是,那后面的绑定问题无从谈起。先把这个解决掉。
2.2 二看 XML 文件:位置、命名、namespace 一个都不能错
如果你的接口已经被 Spring 管理,那下一步检查的就是 XML 文件。
MyBatis 的 XML 映射文件必须满足三个硬性条件才能和接口成功绑定:
- 文件放在 classpath 下,并且路径被
mapper-locations配置覆盖到; - XML 里的
namespace必须和 Mapper 接口的全限定名完全一致; - XML 里的每个语句
id必须和接口方法名完全一致。
只要这三条里有一条不对,轻则某个方法失效,重则全部失效。最容易被忽略的是namespace,因为它一旦写错,MyBatis 在绑定阶段压根不会把这个 XML 里的语句关联到你接口的方法上,但它也不会给你明确的提示。很多时候你的 XML里的 SQL 写得完美,就是 namespace 少了一个包名,整个 Mapper 直接哑火。
在 Spring Boot 中,location 默认是classpath*:/mapper/**/*.xml,但有些老项目会简写成
mybatis: mapper-locations: classpath:mapper/*.xml这个写法会遇到一个大坑:classpath:单星号和classpath*:的差别。如果你的工程有多个 jar 包依赖,比如某些公共模块里也放了 Mapper XML,用classpath:只会去主工程的 classpath 找,公共依赖 jar 里的 XML 很可能被漏掉。这种情况就适合改成
mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml另外 XML 文件名其实没有硬性要求和接口同名,UserMapper.java对应的 XML 理论上可以叫MyUser.xml,只要路径被扫到、namespace 和接口全限定名一致就能绑定。但我个人强烈建议保持同名且同目录,方便维护,也避免队友排查时绕远路。
2.3 三看依赖全家桶:mybatis-plus 和 mybatis 是否打架
这个问题特别容易出现在那种“从网上找了一段历史配置,自己拼起来跑”的项目里。
Spring Boot 使用 MybatisPlus 时,正确的依赖是三件套:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.x</version> </dependency>如果你同时引入了原生的mybatis-spring-boot-starter,或者干脆手动塞进了旧版的mybatisjar,很可能会导致 Spring Boot 自动配置走的是原生 MyBatis 的MybatisAutoConfiguration,而不是 MybatisPlus 的MybatisPlusAutoConfiguration。
为什么这会造成 BaseMapper 方法报错?因为BaseMapper里那些 CRUD 方法本身是没有 XML 的,它们靠 MybatisPlus 提供的一个ISqlInjector(默认实现是DefaultSqlInjector)在启动时动态把 SQL 注入进MappedStatement。要是容器里的SqlSessionFactory是原生 MyBatis 而不是 MybatisPlus 增强过的,那 BaseMapper 的方法绑定就不会发生,你调用时自然就找不到绑定。这种场景下,自定义方法如果配了 XML,反而可能正常,看起来特别诡异。
解决的思路很粗暴:删掉重复引入的 mybatis jar,项目只保留mybatis-plus-boot-starter,然后在依赖依赖树里检查有没有历史传递依赖又带回来,命令是
mvn dependency:tree -Dincludes=org.mybatis对于使用早期版本的老项目,地方要注意mybatis-plus-boot-starter和mybatis-spring-boot-starter同时存在时你得手动 exclusions 掉后者,不然自动配置顺序会出妖蛾子。
2.4 四看构建产物:target/classes 里到底有没有 XML
这个检查我一般放在最后做,但往往能一击命中。
有很多次,源码路径下明明有 XML 文件,IntelliJ IDEA里也能看到,但程序跑起来就是找不到。原因在于 Maven 默认的 resource 配置只打包src/main/resources下的内容,如果你的 XML 跟 Mapper 接口一样放在src/main/java目录下(为了和接口放一起),Maven 构建时大概率不会把.xml文件拷到target/classes。
怎么确认?先去 target 目录翻一翻:
find target/classes -name "UserMapper.xml"输出为空,说明你的 XML 根本没进 classpath。这时需要在pom.xml的<build>里补一段:
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources>补完再重新 clean package,再启动项目,错误一般就消失了。这个原因同时解释了为什么很多老项目“本地好好的,一打包就崩”,因为本地 IDE 运行时默认把你源码目录也算进资源里了,但 Maven 构建可不管你这些。
3. 一次真实场景复盘:从报错到恢复只用了十分钟
3.1 用日志和代码定位“有没有扫描到”
前阵子帮人看一个模拟项目 X,现象就是标题里说的:项目能启动,其他 Mapper 能用,唯独新增的一个AuditLogMapper,调用insert时抛Invalid bound statement (not found)。拿到这样一个模糊的问题,我不会先怀疑 XML,而是先把刚才四个维度的顺序整体走一遍。
第一步先看 Spring 是否扫描到接口。如果你的 Mapper 是新加在某个包里的,确认启动类上的@MapperScan范围。当时可以看到启动类只扫了com.example.demo.mapper,而新加的接口在com.example.demo.repository.audit下面。把路径范围扩大后,Spring 倒是能把它注册成 Bean 了,但调用 insert 仍然报错。
第二步看 XML 文件。原工程的 resources 目录下,AuditLogMapper.xml文件的 namespace 写的是com.example.demo.repository.audit.AuditLogMapper,但接口类改名后放在了com.example.demo.mapper.AuditLogMapper。这明显是复制粘贴时忘了改 namespace。把 namespace 改回来,再重新 clean 一次,问题解决。
整个过程看起来极其简单,但如果你不了解 namespace 和接口全限定名的绑定关系,就只能在看到报错之后盲改配置,大概率越改越乱。排查的顺序重要性就在这:先看接口有没有被扫到,再看 XML 绑定是否正确,最后再动配置和依赖。
3.2 修复三步走:改配置、清缓存、验证查询
把这个问题的操作步骤整理成可以直接“抄作业”的版本:
第一步,改配置。找到项目里的application.yml,把 mapper-locations 改成足够覆盖的写法:
mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity这里我特意把前缀写成mybatis-plus而不是mybatis,因为 Spring Boot 项目如果用 MybatisPlus 的 starter,配置前缀理应绑定到mybatis-plus。有些老项目两种前缀混着写,底下能生效一半,但很容易让人产生误解,统一成mybatis-plus更整洁。
第二步,清缓存并重新构建。这一步很关键,IDEA 的 Build Output 或 Maven 的 target 目录经常藏着旧的 class 文件。执行
mvn clean compile -DskipTests如果项目完全基于 Gradle,那对应命令是gradle clean compileJava。cleaning 会强制把 target 下旧文件干掉,新的 XML 才能进 classpath。这一步不是玄学,是确确实实能规避掉很多“改完配置不起作用”的错觉。
第三步,验证查询。用一个临时接口方法跑一次简单调用,比如直接注入 Mapper 调selectById(1L),看看控制台有没有打出 SQL,返回是否正常。至此,如果你遇到的是常规绑定问题,基本已经治好了。
4. 高频原因对照表与避坑清单
做个小总结,把我认为最好用的排查表放出来。你拿到一个问题,先对照症状,再找对应解决方案,比从零开始瞎试要快得多。
| 症状 | 可能根因 | 快速定位方法 | 解决方案 |
|---|---|---|---|
| BaseMapper 自带方法全部报错,自定义方法正常 | MybatisPlus 自动注入未生效 | 检查依赖树里是否有原生 mybatis | 只保留mybatis-plus-boot-starter,排除冲突依赖 |
| Mapper 接口全部失效,Bean 都注入不了 | @MapperScan路径未覆盖或@Mapper缺失 | 启动日志看是否打印扫描路径 | 修正@MapperScan路径或给接口加@Mapper |
| 个别自定义方法报错 | XML 的 id 或 namespace 写错 | 用grep namespace对接口全限定名 | 修改 namespace / id 保持一致 |
| 本地运行正常,Maven 打包后报错 | XML 未随构建拷贝到 classpath | 查看target/classes下有没有 XML | 在 pom.xml<resources>里包含**/*.xml |
| 多模块项目部分 Mapper 报错 | 依赖 jar 里的 XML 没被扫描 | 确认 mapper-locations 是否带classpath*: | 改成classpath*:/mapper/**/*.xml |
这张表如果配合上一节的定位步骤,基本能覆盖九成以上的场景。
再补充几条我在代码规范上坚持了很久的习惯:
- Mapper 接口和 XML 同名同包,放在
src/main/java下或src/main/resources/mapper下都行,但别一会放这一会放那,统一标准; - 每个 XML 里的 namespace 不要手敲,直接从接口全限定名复制过去,避免大小写、多一个少一个字符的问题;
- 使用 Git 时不忽略 XML 文件,这东西虽然不会变来变去,但缺了一个就是线上事故。
5. 那些“非典型”但概率不低的坑
5.1 多模块工程下 XML 被 Maven 悄悄过滤掉
有一个朋友的项目,接口在common模块里定义,实现和 XML 也在common模块,但运行时却在主启动模块里调用。接口本身能被扫到,因为主模块的@MapperScan直接扫的是com.example.common.mapper这个包,而 common 模块里的src/main/java目录下放着FooMapper.xml。本地 IDEA 跑没事,因为 IDEA 会把源码目录里的 XML 一并放入 classpath;但用mvn clean package后,common 模块的 jar 里根本没有这个 XML,导致线上崩。
这类问题最坑的地方在于报错信息和其他原因完全一样,你看代码看不出任何毛病。我的排查建议是直接打开最终的jar包,看里面的 BOOT-INF/lib 下那个 common jar 是否包含了 XML:
jar tf common-module.jar | grep xml如果没有,按上一章说的<resources>配置处理。这类问题我称它为“构建产物幻觉”,现象在本地完美,在部署环境下完美呈现最原始的错误。
5.2 手贱覆盖 BaseMapper 自带方法
另一个值得单独说的坑,是有人在 Mapper 接口里“重写”了 BaseMapper 的方法。
比如你在UserMapper接口里写了:
public interface UserMapper extends BaseMapper<User> { int insert(User user); }你本意是让这个 insert 走一段你自己写的 XML 逻辑,但问题来了,MyBatis 的MappedStatement是按 Mapper 接口全限定名 + 方法名建立的。一旦接口里出现同名方法,XML 里的id和接口方法同名,很可能发生冲突。更常见的是你写了重写方法但忘了配 XML,于是调用时 BaseMapper 的默认注入逻辑又因为同名冲突没有生效,直接就Invalid bound statement。
解决方案很简单:不要覆盖 BaseMapper 的自带方法。如果真的需要自定义插入行为,换一个方法名,比如myInsert,并在 XML 里定义myInsert的 SQL。这样既避免冲突,也确保 BaseMapper 的默认注入不受干扰。说实话,覆盖内置方法的意义本来就不大,MyBatisPlus 已经给你留好了@TableField、@TableLogic这些扩展点,没必要动最底层的一层。
5.3 自定义注入器与 Spring Boot 重装时的静默故障
还有一种情况适合资深一点的朋友排查:项目里手动配置了ISqlInjector,想扩展一个自定义方法。这时候如果你的SqlSessionFactory是通过MybatisSqlSessionFactoryBean自定义构建的,但漏掉了把自定义注入器塞进去,会导致整个注入流程报废。
常见配置写作:
@Bean public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean(DataSource dataSource) { MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setGlobalConfig(new GlobalConfig()); return factoryBean; }这里setGlobalConfig如果只 new 里一个空的GlobalConfig,没有设置DbConfig和ISqlInjector,那么 MybatisPlus 的很多内置机制根本不会启动。于是 BaseMapper 的方法全部绑定不上。正确的做法是使用 MybatisPlus 提供的默认GlobalConfig构建方式,或者显式注入一个DefaultSqlInjector:
@Bean public GlobalConfig globalConfig() { GlobalConfig config = new GlobalConfig(); config.setDbConfig(new GlobalConfig.DbConfig()); // 注入器可按需自定义 return config; }如果你不是对 MybatisPlus 的源码有十足把握,我建议直接用 starter 自动配置,不要手写SqlSessionFactoryBean。自动配置能覆盖绝大多数的常规需求,一旦你开始手动接管,就要做好所有细节都由自己兜底的心理准备。
6. 最后再分享一个我个人的排查习惯
看了这么多,其实最核心的观点就一个:遇到Invalid bound statement (not found),不要慌,也不要无脑搜“万能解决办法”,先分清是自定义方法失效还是 BaseMapper 内置方法失效,再按我前面给的四个维度一条条过:扫描路径、XML 绑定、依赖冲突、构建产物。
我个人的习惯是专门在本地建一个最小复现工程,里面只放一个 Mapper 和一张表,用来快速测试 MybatisPlus 基础功能。每次项目里出现跟 MyBatis 绑定相关的妖蛾子,我都会先把这个问题放在最小工程里跑一遍,看能不能稳定复现。如果能,说明是我的环境或配置问题;如果不能,那就要检查是不是业务工程里那几十个 Mapper 之间有什么奇怪的覆盖关系。
最后送你一条小技巧:如果你用的是 IDEA,可以把 Mapper 接口和 XML 同时打开,IDEA 会在接口的方法名左侧显示一个图标,点一下就可以跳到对应的 XML SQL。如果接口方法上没有任何跳转图标,十有八九就是绑定没成功,趁早顺着这个线索排查,别等运行时报错才回头看。这个方法并不能直接告诉你原因,但它能帮你启动阶段就看出是不是绑定缺失,省掉很多无效操作。