你要是被项目里那句“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 以上环境中,你可以在同一个类上写多个 @MapperScan,Spring 会处理多个扫描器,这个在后续多数据源场景会用到。
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 接口上逐个加 @Mapper:
@Mapper 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-locations=classpath:mapper/*.xml如果你的 Mapper 接口分散在多个模块,XML 也分散在各个 jar 包里,可以考虑用:
mybatis.mapper-locations=classpath*: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> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/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 namespace="com.example.project.mapper.UserMapper">,这个 namespace 必须和接口全限定名完全一致;二是里面的<select id="selectById">,id 必须和接口方法名一致。这两处错一个,程序跑起来就会报 Invalid bound statement。
5. 依赖、版本和多模块项目里的隐藏问题
5.1 到底加没加对依赖
很多扫描不到的案例,根源在于依赖没加对。Spring Boot 集成 MyBatis 最标准的依赖是:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-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 found | Mapper 接口未注册成 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 使用不同的 SqlSessionTemplate:
@Configuration 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 的时候,不妨先笑一笑,然后打开包结构,多半问题就在那里等着你。