[程序员] 支付中台系统,基于 jeepay 重构了 2 天,目标是做企业部署发行版遇到的问题分享

Spring Boot 3.x 循环依赖实战:从 allow-circular-references 到纯 DAG ,一个支付系统的重构之旅 项目背景:Jeepay 计全支付,Spring Boot 3.3.7 + MyBatis-Plus + Vue 3 ,多模块 Maven 项目。 前端集成引发...
[程序员] 支付中台系统,基于 jeepay 重构了 2 天,目标是做企业部署发行版遇到的问题分享
[程序员] 支付中台系统,基于 jeepay 重构了 2 天,目标是做企业部署发行版遇到的问题分享

Spring Boot 3.x 循环依赖实战:从 allow-circular-references 到纯 DAG ,一个支付系统的重构之旅

项目背景:Jeepay 计全支付,Spring Boot 3.3.7 + MyBatis-Plus + Vue 3 ,多模块 Maven 项目。

  1. 前端集成引发的连锁反应

原本前后端分离部署( Nginx + 独立前端容器),想把前端 dist 直接打进 JAR 简化部署。几个坑:

  • Vue SPA 路由 fallback:Spring Boot 3.x 默认用 PathPatternParser ,addViewControllers 不支持 {spring:\w+} 正则。最终用 @Order(HIGHEST_PRECEDENCE) 的 Filter 拦截无后缀路径 forward 到 index.html 。
  • Spring Security 6 ignoring() 不生效:日志明确警告 You are asking Spring Security to ignore... This is not recommended 。改为 authorizeHttpRequests().permitAll() 才生效。
  • 字体文件 401:Security 的静态资源豁免模式漏了 .woff/.woff2/.ttf/.eot 。
  • 前端构建 API baseURL 双层 /api/api/:VITE_API_BASE_URL=/api 拼接 url: '/api/xxx' → /api/api/xxx 。改为空值。
  1. 发行包瘦身:260MB → 120MB

三个模块的 fat JAR 共 288MB ,其中 130 个公共依赖(72MB) 重复存储 3 次。方案:

  • 去掉未用依赖:jaxb-api(零 import)、mysql-connector-j(纯配置模块不需要)、activemq(仅编译,运行时用 RabbitMQ)
  • 共享 lib:maven-dependency-plugin 收集所有传递依赖到 lib/(自动去重 182 个),antrun 解压 fat JAR 提取 BOOT-INF/classes/ 打包为 flat thin JAR(1.7-5MB)
  • 启动方式:从 java -jar fat.jar 改为 java -cp "lib/*:apps/app.jar" MainClass
  1. 循环依赖根治

去掉 allow-circular-references: true 后直接启动,Spring Boot 3.x 严格检测报出完整依赖链:

┌─────┐ │ SysConfigService 自引用 (删 @Autowired self ,直接用 this) ↑ ↓ │ IsvInfoService 自引用 (同上)

修复策略:

  • 自引用:删字段,改用 this.xxx()
  • 三角循环( PayInterfaceConfigService ↔ MchAppService/MchInfoService ):PayInterfaceConfigService.selectAllPayIfConfigListByAppId() 内部反向调用了 mchAppService.getById() 和 mchInfoService.getById()——这些只是 MyBatis-Plus 的简单 CRUD 委托。将这两个查询上移到 Controller 层,Service 方法改为接收 MchInfo 参数,依赖方向恢复。
  • 两两循环:MchInfoService 注入 IsvInfoService 只是为了 getById(),IsvInfoService 注入 MchInfoService 只是为了 count()。直接替换为对应的 Mapper 注入,因为 MyBatis-Plus 的 ServiceImpl.getById() 底层就是 BaseMapper.selectById()。

关键认知:

  1. 依赖只能单向流动。找到"谁在反向调用"就是断环点。

  2. 大多数循环依赖是 CRUD 委托导致的。ServiceImpl 包装 BaseMapper ,循环往往是因为 A 需要 B 的 getById(),B 需要 A 的 count()——直接用 Mapper 替代 Service 注入,不改任何业务逻辑。

  3. 不要用 @Lazy 、不要用 allow-circular-references ,这些都是掩耳盗铃。Spring Boot 3.x 默认禁止循环是为了逼你写出正确的分层。

  4. 最终效果

┌───────────────────────────┬────────┬───────────────┐ │ 指标 │ 修复前 │ 修复后 │ ├───────────────────────────┼────────┼───────────────┤ │ 发行包大小 │ 260 MB │ 120 MB (-54%) │ ├───────────────────────────┼────────┼───────────────┤ │ 循环依赖 │ 5 个 │ 0 │ ├───────────────────────────┼────────┼───────────────┤ │ allow-circular-references │ true │ 已移除 │ ├───────────────────────────┼────────┼───────────────┤ │ @Lazy │ 2 处 │ 0 │ ├───────────────────────────┼────────┼───────────────┤ │ 启动时间 │ ~8s │ ~4s │ └───────────────────────────┴────────┴───────────────┘

来源: v2ex查看原文