Spring Boot 3.x重构支付系统:从循环依赖到DAG的破局之路🚀

当支付系统遇上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

重构策略

  1. selectAllPayIfConfigListByAppId()中的mchAppService.getById()mchInfoService.getById()上移到Controller
  2. Service方法改为接收MchInfo参数
  3. 依赖方向恢复为单向: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