你要是被项目里那句“Field userMapper in ... required a bean of type ...Mapper that could not be found”折磨过肯定懂我说的场景代码写得没问题注解也加了启动就是失败。这个“Spring Boot MyBatis 扫描不到 Mapper”的问题几乎是每个刚接触这套组合的开发者都会撞上的墙我自己也曾经在一个多模块项目里被它卡了一下午。后来排查得多了才发现这类错误翻来覆去就那么几个原因扫描入口没配、包路径写错、依赖没引对、XML 没打包进去。这篇文章我不绕弯子直接把实践中最有效的几种解决思路和排查顺序写出来你照着走一遍基本十分钟内能定位。1. 先搞清楚“扫描不到”到底断在哪个环节1.1 两类报错代表着不同的断点“扫描不到 Mapper”其实是一类笼统的说法实际报错分两种很多新手会把它们混在一起结果排查方向从一开始就错了。第一种是启动阶段直接失败报错长这样Field userMapper in com.example.project.controller.UserController required a bean of type com.example.project.mapper.UserMapper that could not be found.这句话很直白Spring 在创建 UserController 的时候需要注入一个 UserMapper 类型的对象但容器里没有。注意它说的是“bean 找不到”不是“SQL 找不到”。这说明 Mapper 接口本身根本没被注册成 Spring 的 Bean。第二种是项目能正常启动但一调用 Mapper 方法就报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.project.mapper.UserMapper.selectById这个错误和第一种完全不同。它表示 Mapper 接口已经被扫描注册了但执行方法时找不到对应的 SQL 语句。也就是说问题出在 XML 绑定环节而不是接口注册环节。这两种报错一字之差排查路径完全不同。所以遇到问题第一步不是改配置而是先分清启动时挂的还是运行时挂的。1.2 Spring 和 MyBatis 的扫描到底是怎么配合的我一直觉得理解这层机制比背解决方案更重要。很多人困惑的点是我明明在接口上写了 Mapper为什么 Spring 不认识关键在于Spring 默认的组件扫描根本不认识 Mapper。Spring 的 ComponentScan、SpringBootApplication 自带扫描只认 Component、Service、Repository、Controller 这些标准注解。Mapper 是 MyBatis 框架自己的注解它不会触发 Spring 的组件扫描逻辑。那 Mapper 是谁来处理的是 MyBatis 的 MapperScannerConfigurer。这个扫描器会把带有 Mapper 注解的接口扫描出来为每个接口生成一个代理对象然后注册到 Spring 容器里。而这个扫描器通常是由 MapperScan 注解引入的或者在 mybatis-spring-boot-starter 的自动配置里被注册。打个比方Mapper 只是给接口贴了一张“我是 Mapper”的标签但如果没有一个专门的人去把这些贴了标签的接口登记到花名册上Spring 这个“人事系统”就查不到它们。所以排查时第一反应应该是我的项目里有没有让扫描器生效扫描范围覆盖到接口了吗1.3 包路径和启动类位置的关系即使你正确引入了 mybatis-spring-boot-starter自动配置里的扫描器也不是全项目乱扫。MyBatis 的自动配置扫描器会以启动类所在包为基准向下扫描所有子包。举个例子你的启动类在com.example.project.demo而 Mapper 接口放在com.example.project.mapper那这个接口并不在启动类的子包范围内默认扫描就会漏掉它。反过来如果接口放在com.example.project.demo.mapper那大概率能扫到。我见过很多项目启动类放在比较深的包里Mapper 接口却放在和它平级甚至更靠外的包结果自动配置的扫描自然覆盖不到。这也是为什么大家普遍推荐用 MapperScan 显式指定接口位置而不是依赖默认扫描——显式配置不会受包结构限制。2. 方案一启动类上的 MapperScan 用对就成功了一半2.1 最稳妥的写法在启动类上直接加注解是解决“扫描不到 Mapper”最直接的操作SpringBootApplication MapperScan(com.example.project.mapper) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }这里的核心是把包路径写成 Mapper 接口真正所在的包而不是它的父包。我见过有人图省事喜欢把扫描路径写成com.example这样的大包确实能扫到但会造成这个扫描器去扫描大量无关接口和类碰到同名 Bean 或者需要排除某些接口时麻烦就来了。我自己的习惯是只指定到具体的 Mapper 包这一层。项目规则清晰别人接手看代码时也能一眼看出哪些包会被扫到。如果 Mapper 接口本身有子包MyBatis 的扫描器默认也会递归扫描子包所以不用担心漏掉内部类。2.2 多个包路径怎么处理当项目里有多个 Mapper 包时比如不同模块各自维护自己的 Mapper或者多数据源情况下 Mapper 分散在不同位置MapperScan 支持传入一个字符串数组MapperScan({com.example.module1.mapper, com.example.module2.mapper})也可以写成多个参数的形式MapperScan(basePackages {com.example.module1.mapper, com.example.module2.mapper})如果两个包之间存在共同父包理论上写一次父包也能覆盖但我建议还是分开写。为什么因为只写父包会把不该纳进来的接口也扫进来一旦其中一个模块调整 Mapper 目录就非常容易出问题。显式列出每个 Mapper 包虽然代码稍微长一点但每个包都能单独维护也方便排查。还有一个小细节很多人不知道MapperScan 是可以重复标注的。在 Java 8 以上环境中你可以在同一个类上写多个 MapperScanSpring 会处理多个扫描器这个在后续多数据源场景会用到。2.3 MapperScan 放在配置类上同样可行MapperScan 不一定要写在启动类上。它的底层是 ImportBeanDefinitionRegistrar 机制只要所在类能被 Spring 加载扫描就会生效。所以你也可以单独建一个配置类Configuration MapperScan(com.example.project.mapper) public class MybatisConfig { }这种写法适合想保持启动类干净的项目把所有数据库相关配置统一放到一个配置类里比如数据源、事务管理器、MyBatis 配置放一起逻辑内聚。不过要注意如果启动类和配置类上都加了 MapperScan且扫描范围有重叠可能出现重复注册轻则浪费启动时间重则报 BeanDefinitionOverrideException 之类的冲突。后面我会单独讲这个坑。3. 方案二每个 Mapper 接口上使用 Mapper 的手动方式3.1 给每个接口都标上 Mapper如果不喜欢用 MapperScan 统一定位另一种方式是在每个 Mapper 接口上逐个加 MapperMapper public interface UserMapper { User selectById(Integer id); }当项目正常引入 mybatis-spring-boot-starter 时自动配置会启动一个默认的扫描器扫描启动类所在包及其子包把带 Mapper 注解的接口注册成 Bean。这种方式在小项目里非常直观每新增一个 Mapper 接口只要记得补上 Mapper 就不会漏。我接触过一些团队他们更倾向这种写法因为注解直接写在接口上和代码放在一起IDE 跳转方便代码评审时也容易看到哪些类被纳入了 MyBatis 管理。缺点是每加一个接口都不能忘记注解忘了就启动报错而且报错信息有时候比较隐晦新人容易懵。3.2 为什么只加 Mapper 有时还是搜不到只加 Mapper 依然扫描不到原因基本逃不出三种第一项目里根本没有 mybatis-spring-boot-starter 依赖自动配置没生效Mapper 就只是一个普通注解没有任何扫描器会去解析它。容器里自然不会有对应的代理 Bean。第二接口放在了启动类子包范围之外。前面说过自动配置触发的扫描器是从启动类所在包向下扫的你的接口如果在启动类的兄弟包甚至外层包默认扫描完全看不到。第三项目里可能已经存在一个自定义的 MyBatis 配置类它自定义了 SqlSessionFactory 或扫描器导致自动配置被覆盖。此时 Mapper 注解的扫描入口可能不存在或者扫描路径被改到了别的地方。这三种情况都需要回到“扫描入口”这个核心问题上去查而不是在接口上反复加注解碰运气。3.3 到底选 MapperScan 还是 Mapper我个人在实际项目中会这样判断如果 Mapper 接口数量少、包路径简单、项目是单体应用用 Mapper 逐个标注完全没问题代码直观心智负担低。如果接口数量多或者项目是多模块、多数据源最好统一用 MapperScan 管理至少它能让你在一个地方看清楚哪些包被纳入了扫描。其实两者也可以共存。MapperScan 扫描范围内没有 Mapper 注解的接口也能被注册反过来自动配置的扫描器也会去寻找带 Mapper 注解的接口。一个接口最终只会被注册一次不会因为两种方式同时作用于它而产生两个 Bean。真正要担心的是两个 MapperScan 同时扫描同一个接口那才有冲突的可能。4. 方案三XML 没绑定也会造成“扫描不到”的假象4.1 Invalid bound statement 与接口注册是两回事很多人在排查“找不到 Mapper”时把精力全放在接口注册上却忽略了 XML 绑定这一关。实际情况里有一类问题启动时完全正常代码也能注入 Mapper 对象但一执行方法就报 Invalid bound statement。这个报错出现时你甚至可以在 Spring 容器里看到 UserMapper 这个 Bean 是存在的。所以它不是“接口没被扫描”而是“接口的方法没有对应的 SQL 映射”。简单说MyBatis 给接口生成了代理对象但代理对象找不到具体执行的 SQL自然就罢工了。我在帮别人排查时第一步永远是确认错误出现的时机。如果错误发生在项目启动阶段优先查接口注册如果发生在接口调用阶段优先查 XML 绑定。方向搞清了问题基本解决一半。4.2 mapper-locations 配置怎么写XML 绑定最常见的坑是mybatis.mapper-locations配置路径和实际 XML 位置对不上。假设你的 XML 文件放在src/main/resources/mapper/目录下那在配置文件里应该写mybatis.mapper-locationsclasspath:mapper/*.xml如果你的 Mapper 接口分散在多个模块XML 也分散在各个 jar 包里可以考虑用mybatis.mapper-locationsclasspath*:mapper/**/*.xml这里需要理解classpath和classpath*的区别。classpath:mapper/*.xml只在当前 classpath 的根路径下找mapper目录如果 XML 在依赖的 jar 包内这种方式可能扫不到。classpath*:mapper/**/*.xml会扫描所有 jar 包里的同名路径资源适合多模块项目。但这个方法也会把依赖里所有匹配的 XML 都加载进来如果某些 jar 里恰好有相同路径的无关 XML可能有意外影响。所以只有当确认当前工程确实存在多模块资源时再用它别一上来就无脑加星号。4.3 XML 的命名和位置决定了是否会掉落还有一种特殊场景XML 文件没有放在src/main/resources下而是直接和 Mapper 接口一起放在src/main/java的同一个包里。这种做法在 IDE 里能正常读取但用 Maven 打包时src/main/java目录下的文件默认只处理.java源码不会把.xml复制到最终产物。结果就是运行时找不到 XML报的错还是“扫不到 SQL”。解决方式是在 pom.xml 里显式声明把 XML 文件也作为资源打进产物build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build这段配置一看就懂把src/main/java下的 XML 也复制到 classpath同时保留src/main/resources下的资源文件。加了这段之后那些和接口同包的 XML 就能被正常打包。不过这也是一种偏门用法正规做法还是建议把 XML 统一放到src/main/resources/mapper下避免折腾构建配置。4.4 怎么确认 jar 里有没有 XML排查到这一步时别只顾着改配置。我一般在修改前先直接验证当前构建产物里到底有没有 XML这样能少走弯路。操作很简单项目执行mvn clean package之后到 target 目录找到生成的 jar 包用压缩工具打开找到BOOT-INF/classes或classes目录看看你预期的 XML 路径是否存在。如果不存在就是资源没打进去如果存在就继续检查 XML 内容。打开 XML 之后重点核对两样东西一是mapper namespacecom.example.project.mapper.UserMapper这个 namespace 必须和接口全限定名完全一致二是里面的select idselectByIdid 必须和接口方法名一致。这两处错一个程序跑起来就会报 Invalid bound statement。5. 依赖、版本和多模块项目里的隐藏问题5.1 到底加没加对依赖很多扫描不到的案例根源在于依赖没加对。Spring Boot 集成 MyBatis 最标准的依赖是dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version对应Spring Boot版本的版本号/version /dependency注意这个坐标和我之前见过的一些项目里写的org.mybatis:mybatis完全不同。只引入 MyBatis 核心库Spring Boot 根本不会自动配置 MyBatis更不会去扫描 Mapper。还有一种情况是项目里手动引入了mybatis-spring但缺少mybatis-spring-boot-autoconfigure自动配置同样不会生效。你可以这样检查在 IDE 右侧打开 Maven 依赖树搜索mybatis-spring-boot-autoconfigure。如果这个 jar 存在说明自动配置是有机会触发的如果根本没有那先别急着查什么扫描路径把依赖补齐再说。5.2 版本不匹配带来的怪问题版本问题往往是“看起来都对了但就是不行”的元凶。Spring Boot 2.x 项目通常使用 mybatis-spring-boot-starter 2.x 版本Spring Boot 3.x 项目则必须使用 mybatis-spring-boot-starter 3.x。如果你把 2.x 的 starter 硬塞进 Spring Boot 3.x 项目启动阶段可能因为某些类不存在或者包路径变更而报错有时候表现就是注解失效、扫描异常。更麻烦的是公司里的老项目可能用的是自定义的 MyBatis 版本或者某个传递依赖把mybatis核心库的版本换了。这种时候我建议先跑一遍mvn dependency:tree看看实际生效的 mybatis 相关 jar 版本到底是什么再和 starter 的要求对照。版本差得太多改配置是解决不了问题的。5.3 多模块项目里的“包路径看不见”多模块是另一个高频雷区。假设你的 Mapper 接口放在一个公共模块里业务启动模块只负责启动 Spring Boot两者是依赖关系。这种情况下只要在启动模块的启动类或配置类上写对 MapperScan指向公共模块的 Mapper 包路径扫描是能正常跨模块工作的。因为 Mapper 接口编译后会打进公共模块的 jar再被启动模块依赖classpath 里能看得到这些类扫描器也就能处理。但如果公共模块本身也引入了 MyBatis 的自动配置或者有自己的 SqlSessionFactory就有可能和启动模块里的配置打架。两个扫描器同时工作重复注册同一个 Mapper 接口表现出的问题可能就是启动异常或者运行时 Bean 类型不匹配。这类问题排查起来比较费劲我一般会先看报错堆栈里涉及哪一个 SqlSessionFactory 或扫描器再去对应模块找配置。6. 快速排查清单十分钟内定位问题6.1 按报错快速判断我把常见现象、原因、修复方向整理成一张表排查时可以直接对照报错现象最可能原因修复方向启动报 Field xxxMapper ... could not be foundMapper 接口未注册成 Bean检查 MapperScan 或 Mapper 注解确认包路径运行报 Invalid bound statement接口已注册但 SQL 未绑定检查 mapper-locations 与 XML 路径启动时 SqlSessionFactory 创建失败依赖缺失或版本冲突检查 mybatis starter 依赖和版本BeanDefinitionOverrideException多个 MapperScan 扫描重叠统一扫描入口避免重复扫描打包后 XML 找不到资源未复制到 classpath配置 pom.xml 的 resources 节点这张表是我平时排查问题的第一参考。看着报错信息找对应的行至少能圈定一个大方向。6.2 动手排查步骤如果不想对着表格猜可以按下面的顺序一步步走确认 Mapper 接口上有没有加 Mapper启动类或配置类上有没有 MapperScan。把 MapperScan 的包名复制出来和 Mapper 接口的 package 声明逐字对比排查拼写错误。在 pom.xml 中确认 mybatis-spring-boot-starter 依赖存在且 Spring Boot 版本匹配。启动项目时开启 debug 日志重点看 MyBatis 相关日志有没有输出。执行mvn clean package后解压 jar确认 XML 是否在 classpath 中。若 XML 存在逐项核对 namespace 和 statement id 与接口、方法名是否一致。这套流程不需要特殊的工具只要会看日志、会解压 jar十分钟内基本能走到正确的结论。6.3 打印容器里到底注册了哪些 Mapper Bean有时候项目能启动但你不确定某个 Mapper 到底有没有被注册可以用一个小技巧写一个 ApplicationRunner启动完成后打印容器中所有名字带 mapper 的 Bean。Component public class BeanPrinter implements ApplicationRunner { private final ApplicationContext context; public BeanPrinter(ApplicationContext context) { this.context context; } Override public void run(ApplicationArguments args) throws Exception { String[] names context.getBeanDefinitionNames(); for (String name : names) { if (name.toLowerCase().contains(mapper)) { System.out.println(registered bean: name); } } } }如果启动成功但某个 Mapper 没打印出来说明它压根没注册查扫描路径和注解。如果打印出来了但调用方法还是报 BindingException那问题就在 XML 绑定和接口注册无关。这个小工具我至今留着排查时非常省事。7. 多个扫描器叠加和多数据源场景要单独处理7.1 一个项目里只有一套 MyBatis 配置时没必要反复写我在帮人看代码时经常发现同一个项目里既有启动类上的 MapperScan某个配置类上又写了一个 MapperScan扫描范围还基本重叠。这么做并不能增强扫描反而可能引发重复注册。Spring 对同名 BeanDefinition 的默认行为在不同版本下处理方式不太一样有些版本直接抛出 BeanDefinitionOverrideException有些版本允许覆盖但无论哪种都会给排查带来干扰。更关键的是重复代码会让人误以为项目配置很复杂掩盖了真正的问题。我的建议很简单一个数据源一套 MyBatis 配置一个 MapperScan位置选启动类或专门的配置类二选一就行。不要多写。7.2 多数据源场景更要小心多数据源时通常会有多个 SqlSessionTemplate 和多个扫描器。此时 MapperScan 需要显式指定 sqlSessionTemplateRef让不同包的 Mapper 使用不同的 SqlSessionTemplateConfiguration public class MybatisConfig { Bean public SqlSessionTemplate primarySqlSessionTemplate() { // 主数据源相关配置 return new SqlSessionTemplate(primarySqlSessionFactory()); } MapperScan(basePackages com.example.mapper.primary, sqlSessionTemplateRef primarySqlSessionTemplate) Configuration public static class PrimaryConfig { } MapperScan(basePackages com.example.mapper.secondary, sqlSessionTemplateRef secondarySqlSessionTemplate) Configuration public static class SecondaryConfig { } }如果忽略了 sqlSessionTemplateRef或者引用了错误的 Bean 名称报错千奇百怪有时候提示找不到扫描器有时候提示 Bean 类型不匹配。多数据源项目排查这种问题我的经验是先确认每个 Mapper 包是否明确归属到某一个 SqlSessionTemplate再去看扫描范围有没有重叠。一旦出现交叉后续处理会非常麻烦。8. 我个人的排查习惯和小技巧说点实战里总结的私人经验吧。我现在遇到“扫描不到 Mapper”这类问题已经不怎么慌张了因为套路相对固定。第一次出手之前我会先把三件事写清楚启动类在哪个包、MapperScan 指向哪个包、Mapper 接口在哪个包。把这三个包路径列出来之后大部分扫描问题其实一眼就能看出答案。还有一个小技巧可以分享凡是和 Mapper 相关的“找不到”先别急着改配置停下来判断报错的时机。启动阶段找不到问题大概率出在接口注册链路运行阶段找不到问题大概率出在 SQL 绑定链路。方向定准了剩下的就是对照本文前几章逐项排查。最后说一句框架的报错信息虽然看着吓人但大多数时候它已经把原因写得很直白了只是我们容易被一长串堆栈带偏。下次再看到 could not be found 的时候不妨先笑一笑然后打开包结构多半问题就在那里等着你。