踩坑实录:JRebel 启动报 Mapper 重复 ID 异常,IDEA 普通启动却正常?

前言

日常 Java 后端开发中,JRebel 热部署绝对是提升开发效率的神器 ------ 修改业务代码无需重启应用,几秒就能生效。但最近我遇到了一个非常典型的诡异问题:完全相同的项目代码,用 IDEA 原生启动方式能正常启动、业务运行无异常,可一旦开启 JRebel 热部署启动,应用直接抛出异常终止,进程退出码 1

本文就把完整的问题排查过程、根因分析和最终解决方案全部分享出来,帮同样遇到这个问题的同学快速避坑。

一、问题现象

先交代一下项目基础环境:

  • 技术栈:Spring Boot 2.2.5 + MyBatis-Plus 3.3.2 + 动态数据源 + Nacos
  • 开发工具:IntelliJ IDEA 2024.1.4
  • 热部署工具:JRebel 2026.2.1,且安装了 JRebel MybatisPlus 插件

问题表现非常明确:

  1. 直接通过 IDEA 的Run按钮启动项目,无任何报错,Tomcat 正常监听 6002 端口,接口调用、数据库操作全部正常;
  2. 点击 JRebel 的Run with JRebel启动,控制台直接抛出一大串 Bean 注入异常,应用启动失败,进程直接退出。

二、核心报错信息拆解

很多同学遇到 Spring 启动的依赖注入异常,第一反应是盯着 Controller、Service 的注入问题排查,其实完全找错了方向。Spring 启动异常栈中,最底层的Caused by才是真正的根因,上层的异常全是根因触发的连锁反应。

我们先看最核心的报错信息:

bash 复制代码
# 最底层的根因异常
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.nstc.aims.mapper.AimsCommonMapper.queryAimsFileList. please check file [AimsCommonMapper.xml] and file [AimsCommonMapper.xml]

# 连锁异常链路
Failed to parse mapping resource: 'AimsCommonMapper.xml'
→ Error creating bean with name 'sqlSessionFactory'
→ Error creating bean with name 'accountEmailCountMapper'
→ Error creating bean with name 'accountEmailCountServiceImpl'
→ Error creating bean with name 'ServiceLocator'
→ Error creating bean with name 'BalanceApiController'
→ Application run failed

从报错可以直接得到两个关键信息:

  1. 直接原因:MyBatis 的Mapped Statements集合中,已经存在com.nstc.aims.mapper.AimsCommonMapper.queryAimsFileList这个语句 ID,出现了重复注册;
  2. 影响范围:这个 XML 解析失败,导致 MyBatis 的核心会话工厂sqlSessionFactory创建失败,最终引发整条 Bean 注入链路崩盘,应用启动终止。

但这里就出现了最核心的疑问:为什么同样的代码、同样的 XML 文件,普通启动不报错,只有 JRebel 启动会报错?

三、排查过程:从表象到根因

带着这个核心疑问,我开始了逐步排查:

第一步:排查 XML 文件的低级错误

首先打开报错的AimsCommonMapper.xml文件,全局搜索queryAimsFileList,确认文件内只有一个该 id 的 select 语句,不存在同文件内重复定义的问题。

再全局搜索项目内的AimsCommonMapper.xml,确认项目中只有一份该文件,不存在不同模块、不同资源目录下的重复文件。

第二步:排查 MyBatis 扫描配置

查看项目的 MyBatis-Plus 配置,检查mapper-locations@MapperScan注解:

  • 发现mapper-locations配置的通配符路径,存在一定的重复匹配风险;
  • 同时@MapperScan配置了多个包路径,日志中也出现了大量No MyBatis mapper was found in '[xxx]' packageSkipping MapperFactoryBean with name 'xxx' Bean already defined的警告。

但这里依然有疑问:就算扫描配置有瑕疵,为什么普通启动能正常运行?

第三步:聚焦 JRebel 的特殊行为

这时我注意到 JRebel 启动日志中的关键信息:

bash 复制代码
2026-05-09 13:42:03 JRebel: Directory 'aims-application\target\classes' will be monitored for changes.
2026-05-09 13:42:03 JRebel: Directory 'aims-service-api\target\classes' will be monitored for changes.
2026-05-09 13:42:03 JRebel: Directory 'aims-core-service\target\classes' will be monitored for changes.
2026-05-09 13:42:04 JRebel: Ready config JRebel MybatisPlus plugin(1.0.7)...
2026-05-09 13:42:04 JRebel: Add CBP for mybatis-plus core classes...

到这里,问题的真相已经浮出水面 ------问题的核心,就在于 JRebel 的 MyBatis-Plus 插件,以及它和普通启动完全不同的类与 Mapper 加载机制

四、核心真相:两种启动方式的加载机制差异

我们分别拆解两种启动方式的底层逻辑,就能完全理解这个 "诡异现象" 的本质。

1. IDEA 普通启动:一次性加载,自带 "容错性"

普通启动的加载逻辑非常直接:

  • Spring Boot 启动时,MyBatis-Plus 通过MybatisSqlSessionFactoryBean一次性扫描并解析所有 Mapper XML 文件 ,完成Mapped Statements的注册;
  • 对于重复的扫描路径、重复的语句 ID,MyBatis 原生的处理逻辑是后加载的内容覆盖先加载的内容,并不会直接抛出异常终止启动;
  • 只要最终能注册成功一个可用的语句,应用就能正常启动,甚至调用方法时也不会出现明显异常,只会在日志中留下不显眼的警告。

简单来说:普通启动对 Mapper 重复配置的容忍度很高,哪怕有隐患,只要不影响核心功能,就会 "睁一只眼闭一只眼" 让应用跑起来。

2. JRebel 启动:热部署需求驱动的严格校验

JRebel 启动报错,恰恰是因为它的核心能力 ------热部署

为了实现 Mapper XML 修改后无需重启就能生效,JRebel 的 MyBatis-Plus 插件做了这几件关键的事:

  1. 全范围监控与扫描 :JRebel 会监控项目所有模块的target/classes目录(日志中可见多个模块都被纳入监控),扫描范围比普通启动更广,更容易匹配到重复的 Mapper 资源,触发重复加载;
  2. 严格的唯一性校验 :为了实现单个 Mapper 语句的热替换,JRebel 必须严格维护Mapped Statements集合的全局唯一性。如果允许重复 ID 覆盖,会直接导致热替换失效、内存泄漏、SQL 执行错乱等问题,因此 JRebel 插件会在加载阶段做强校验,只要发现重复 ID,直接抛出异常终止启动,绝不 "容错";
  3. 二次加载风险:Spring 容器初始化时会先加载解析一次 Mapper XML,JRebel 插件为了注册热部署的监控钩子,会对 Mapper XML 进行二次预加载与解析,这也会触发重复 ID 的校验报错。

总结下来,答案非常明确:是的,JRebel 的 MyBatis-Plus 插件会在启动时对 XML 映射文件的合法性、语句 ID 的唯一性做远严于 MyBatis 原生的检查。它不是 "制造了问题",而是把普通启动下被掩盖的配置隐患,直接暴露了出来。

五、完整解决方案

我们分「根治方案」和「JRebel 专属优化」两部分,彻底解决这个问题。

1. 根治方案:彻底消除 Mapper 重复配置隐患

这是最核心的步骤,无论用不用 JRebel,都建议修复,避免线上出现不可预期的 SQL 执行问题。

步骤 1:修正 Mapper XML 的重复定义

再次确认所有 Mapper XML 文件中,不存在相同命名空间下的重复语句 ID,哪怕是注释掉的重复语句,也建议清理干净。

步骤 2:规范 MyBatis-Plus 扫描配置

修正application.yml中的 Mapper 扫描路径,避免宽泛的通配符导致重复匹配:

yaml 复制代码
# 错误示例:可能导致重复扫描的宽泛配置
mybatis-plus:
  mapper-locations: 
    - classpath*:mapper/**/*.xml
    - classpath*:mapper/oracle/aims/*.xml  # 重复路径,会被上面的通配符覆盖

# 正确示例:精准、无重复的扫描配置
mybatis-plus:
  mapper-locations: classpath*:mapper/oracle/aims/*.xml

同时修正启动类上的@MapperScan注解,避免重复扫描包路径,确保每个 Mapper 接口只被扫描一次,消除日志中的Bean already defined警告。

步骤 3:清理编译缓存

在 IDEA 中执行Build → Rebuild Project,或通过 Maven 执行clean compile命令,彻底清理target目录下的残留文件,避免旧的编译文件导致重复加载。

2. JRebel 专属优化方案

步骤 1:清理 JRebel 本地缓存

完全关闭应用和 IDEA 后,删除 JRebel 的缓存目录:

bash 复制代码
C:\Users\你的用户名.jrebel

重启 IDEA 后重新启动应用,避免缓存导致的重复加载。

步骤 2:调整 JRebel 监控范围(可选)

Mapper XML 的热部署实际效果有限,多数场景下修改 XML 后依然需要重启才能完全生效。我们可以在项目的rebel.xml配置文件中,排除对 Mapper XML 的监控,减少不必要的校验:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<rebel>
  <classpath>
    <dir name="target/classes">
      <!-- 排除mapper目录下的XML文件,不纳入热部署监控 -->
      <exclude name="mapper/**/*.xml"/>
    </dir>
  </classpath>
</rebel>

3. 验证修复

完成上述操作后,分别用两种方式启动项目:

  1. 若 JRebel 启动无报错,Tomcat 正常监听端口,说明核心问题已解决;
  2. 调用AimsCommonMapper.queryAimsFileList方法,确认 SQL 执行符合预期,无异常日志。

六、避坑总结与经验分享

  1. **不要迷信 "能启动就是没问题"**应用能正常启动,不代表代码和配置没有隐患。很多线上的诡异问题,都是开发阶段被框架 "容错性" 掩盖的小问题导致的。JRebel 的严格校验,其实是帮我们提前暴露了风险。
  2. MyBatis Mapper 语句 ID 唯一性是硬性规范哪怕 MyBatis 原生允许重复 ID 覆盖,也绝对不要这么做。这会导致开发、测试、生产环境出现不一致的 SQL 执行结果,排查成本极高。
  3. 热部署工具要合理使用JRebel 虽好,但不是所有文件都适合热部署。对于 Mapper XML、配置类、静态常量等内容,修改后建议直接重启应用,避免热部署带来的类加载错乱、数据不一致等诡异问题。
  4. 排查 Spring 启动异常,一定要盯紧最底层的 Caused by90% 以上的 Spring 启动失败问题,根因都藏在异常栈的最底部。不要被上层的 Bean 注入异常迷惑,找到最底层的报错,问题就解决了一半。

结尾

以上就是本次 JRebel 启动踩坑的完整复盘。如果你也遇到过 JRebel 相关的其他奇葩问题,欢迎在评论区留言交流,我们一起避坑~

相关推荐
Yyyyyy~3 分钟前
【java】数组的定义和使用
java
麻辣布丁3 分钟前
java集合篇面试模拟
java·开发语言·面试
niucloud-admin5 分钟前
JAVA V6 多商户商城 开发文档——插件开发规范
java·开发语言
嘟哩DuliDuli18 分钟前
创作 Agent 的任务状态该如何保存
android·java·javascript
wdfk_prog30 分钟前
lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站
java·运维·缓存·docker·容器
海盗123434 分钟前
DSH 接入 OpenCode Go 报 400 MissingSessionID:从 LLM 语义层退到 fetch 传输层的完整排障
开发语言·后端·golang
Token掘金室36 分钟前
JSON模式结构化输出报错
java·服务器·json
秋饼41 分钟前
Spring AI 2.0 接入 DeepSeek V4.1 Flash 生产级实战
java·ai·技术分享·后端开发
IT枫斗者枫哥42 分钟前
Java 分批导出仍然 OOM?用 32 MiB 堆复现三种 CSV 写法
java
一木 之林43 分钟前
第 3 节 在 AI 行业中,C/C++ 编程中的动态库和静态库
c语言·c++·后端