当支付系统遇上Spring Boot 3.x严格模式:一场关于架构纯净度的战争
"循环依赖就像系统里的幽灵,平时看不见,但关键时刻总能让你抓狂。"在重构Jeepay支付系统的第48小时,看着控制台输出的5条完整依赖链,我意识到这场与历史代码的较量才刚刚开始。这个服务Spring Boot 2.x时代积累的"技术债",在升级到3.3.7版本后彻底爆发------默认禁止循环依赖的特性像一把手术刀,精准地切开了系统架构的病灶。
一、重构背景:当支付中台遇上企业级部署
作为一款支持支付宝/微信/银联等全渠道的支付中台,Jeepay的架构设计本应如瑞士军刀般精巧。但现实是:
- 技术栈:Spring Boot 3.3.7 + MyBatis-Plus + Vue 3
- 部署模式:原前后端分离(Nginx + 独立容器)
- 核心问题 :企业客户要求"开箱即用"的发行版,但现有架构存在:
- 260MB的臃肿安装包
- 隐藏的5处循环依赖
- 启动时间长达8秒
"我们要的不是能运行的代码,而是能长期维护的工程。"客户CTO的这句话,成为这次重构的终极目标。
二、前端集成:看似简单的需求背后的深渊
1. SPA路由的致命陷阱
将Vue前端打包进JAR的决策,让团队掉进了第一个坑:
java
// 错误示范:Spring Boot 3.x默认PathPatternParser不支持正则路由
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/{spring:\\w+}").setViewName("forward:/index.html");
}
}
解决方案 :用Filter实现无后缀路径转发
java
@Order(HIGHEST_PRECEDENCE)
public class SpaFallbackFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
if (req.getRequestURI().lastIndexOf('.') < 0) {
req.getRequestDispatcher("/index.html").forward(request, response);
} else {
chain.doFilter(request, response);
}
}
}
2. Security配置的暗礁
Spring Security 6的ignoring()方法警告让我们意识到:安全配置没有"差不多":
java
// 错误方式(会触发警告)
http.ignoring().antMatchers("/static/**");
// 正确方式
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/static/**").permitAll()
);
静态资源豁免清单:
arduino
.css, .js, .png, .jpg, .gif, .ico, .svg
.woff, .woff2, .ttf, .eot // 字体文件容易被忽略
3. API基地址的双重陷阱
Vite的VITE_API_BASE_URL配置与前端代码拼接导致双层/api的问题,暴露了环境变量管理的脆弱性:
javascript
// 错误配置
// .env.production
VITE_API_BASE_URL=/api
// 前端代码
const url = `${import.meta.env.VITE_API_BASE_URL}/api/xxx`
// 结果:/api/api/xxx
// 解决方案:留空或使用完整URL
VITE_API_BASE_URL=
三、包体积优化:从260MB到120MB的瘦身秘籍
1. 依赖树分析:Maven的"俄罗斯套娃"
使用mvn dependency:tree发现:
- 3个模块重复存储130个依赖(72MB)
- 无用依赖:
jaxb-api(XML处理,实际未使用)mysql-connector-j(配置模块不需要JDBC驱动)activemq(编译时依赖,运行时用RabbitMQ)
2. 共享库方案:告别重复存储
Maven配置精髓:
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
启动命令变革:
bash
# 旧方式(fat jar)
java -jar app.jar
# 新方式(thin jar + 共享库)
java -cp "lib/*:apps/app.jar" com.jeepay.MainClass
四、循环依赖根治:从允许到禁止的架构进化
1. 自引用陷阱:SysConfigService的觉醒
java
// 错误示范:自引用导致循环
@Service
public class SysConfigService {
@Autowired
private SysConfigService self; // 删除这行!
public String getConfig() {
return self.getConfigFromDB(); // 直接用this.xxx()
}
}
关键认知 :自引用是设计问题的信号,通常意味着:
- 服务职责不清晰
- 需要拆分服务
- 或改用方法调用替代字段注入
2. 三角循环:PayInterfaceConfigService的逆袭
PayInterfaceConfigService ↔ MchAppService ↔ MchInfoService
重构策略:
- 将
selectAllPayIfConfigListByAppId()中的mchAppService.getById()和mchInfoService.getById()上移到Controller - Service方法改为接收
MchInfo参数 - 依赖方向恢复为单向:Controller → Service → Mapper
3. 两两循环:Mapper注入的降维打击
java
// 错误模式(循环依赖)
@Service
public class MchInfoService {
@Autowired
private IsvInfoService isvInfoService;
public Long count() {
return isvInfoService.count(); // 反向调用
}
}
// 正确模式(用Mapper替代)
@Service
public class MchInfoService {
@Autowired
private MchInfoMapper mchInfoMapper;
@Autowired
private IsvInfoMapper isvInfoMapper; // 直接注入Mapper
public Long count() {
return isvInfoMapper.selectCount(null); // 底层就是MyBatis原生的SQL
}
}
金句 :"当你在Service层看到大量getById/count调用时,这就是Mapper注入的信号"
五、重构成果:数字背后的架构健康度
| 指标 | 修复前 | 修复后 | 优化幅度 |
|---|---|---|---|
| 发行包大小 | 260 MB | 120 MB | -54% |
| 循环依赖数量 | 5个 | 0个 | 100%消除 |
| allow-circular-references | true | 移除 | 强制合规 |
| @Lazy使用 | 2处 | 0处 | 100%消除 |
| 启动时间 | ~8s | ~4s | -50% |
隐藏收益:
- 代码可维护性显著提升
- 新人上手成本降低
- 符合企业级部署的严苛要求
六、未来展望:DAG架构的终极形态
这次重构让我们深刻认识到**:循环依赖是技术债的显性化指标**。Spring Boot