Spring Boot 应用间动态协同调用:启动、通信与优雅终止指南

本文介绍在非 serverless 环境下,如何让一个 spring boot 主应用按需启动并安全通信另一个 spring boot 微服务(如独立 jar),涵盖进程级启动、健康探测、同步

调用及资源回收等关键实践。

在微服务架构中,有时需要“按需唤醒”轻量级辅助服务(例如临时数据导出器、异步报告生成器或合规性校验器),而非长期运行所有组件。虽然 Kubernetes 的 Horizontal Pod Autoscaler(HPA)或云平台的 Serverless 方案(如 AWS Lambda + Spring Cloud Function)是理想选择,但若受限于本地部署、离线环境或容器编排不可用,可通过 JVM 进程级协同实现类似效果。

✅ 核心思路:主控 + 子进程 + 健康感知通信

主 Spring Boot 应用(Primary)作为协调者,通过 Runtime.exec() 启动独立打包的微服务 JAR(Secondary),再主动探测其就绪状态,待确认后发起业务调用;Secondary 完成任务后自行退出,避免资源泄漏。

1. 启动子服务(Secondary)

在 Primary 中封装启动逻辑,建议使用 ProcessBuilder(比 Runtime.exec() 更安全、可配置):

public Process startSecondaryService() throws IOException {
    String jarPath = "/path/to/secondary-service.jar";
    ProcessBuilder pb = new ProcessBuilder(
        "java", "-jar", jarPath,
        "--server.port=8081",  // 显式指定端口,避免冲突
        "--spring.profiles.active=standalone"
    );
    pb.redirectErrorStream(true); // 合并 stderr 到 stdout,便于日志采集
    return pb.start();
}
⚠️ 注意事项: 确保 secondary-service.jar 已预置在 Primary 可访问路径(如 src/main/resources/bin/ 或挂载卷); Secondary 的 application.yml 应支持 standalone profile,关闭无关自动配置(如 Eureka Client、Config Server); 避免端口冲突:Primary 默认 8080,Secondary 建议固定为 8081 并在代码中校验端口可用性。

2. 探测服务就绪状态(Health Check)

Secondary 启动后需暴露 /actuator/health(启用 spring-boot-starter-actuator)。Primary 通过轮询实现等待:

public boolean waitForSecondaryUp(int maxRetries, long delayMs) {
    String healthUrl = "http://localhost:8081/actuator/health";
    for (int i = 0; i < maxRetries; i++) {
        try {
            ResponseEntity resp = restTemplate.getForEntity(healthUrl, String.class);
            if (resp.getStatusCode().is2xxSuccessful() && 
                resp.getBody().contains("\"status\":\"UP\"")) {
                log.info("Secondary service is UP.");
                return true;
            }
        } catch (Exception ignored) {}
        try { Thread.sleep(delayMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
    log.error("Secondary service failed to become ready within {} retries.", maxRetries);
    return false;
}

3. 发起业务调用与优雅终止

就绪后,Primary 调用 Secondary 的 REST 接口执行任务。Secondary 完成后应主动退出(推荐方式):

// 在 Secondary 的 Controller 中(例如 /api/process)
@PostMapping("/process")
public ResponseEntity doWork(@RequestBody WorkRequest req) {
    // 执行业务逻辑...
    log.info("Work completed. Shutting down...");
    // 触发 JVM 退出(注意:仅适用于单进程场景)
    System.exit(0);
    return ResponseEntity.ok("DONE");
}

✅ 替代方案(更健壮):Secondary 向 Primary 发送回调(如 POST /secondary/complete),由 Primary 调用 process.destroy() 终止子进程,并捕获 destroyForcibly() 防止僵死。

4. 异常处理与资源清理

  • 使用 try-with-resources 或 @PreDestroy 确保 Process 对象被销毁;
  • 捕获 IOException(JAR 不存在)、IllegalThreadStateException(进程已退出)等;
  • 将子进程日志重定向到文件或 SLF4J(通过 pb.redirectOutput(new File("secondary.log")));
  • 禁止在生产环境滥用:该模式缺乏隔离性、监控和弹性(如 OOM 无法自动重启),仅推荐用于开发测试、CI 工具链或边缘嵌入式场景。

综上,这种 JVM 进程协同是一种务实的轻量级解耦方案,虽不如 Kubernetes 原生调度严谨,但在特定约束下可快速落地。关键在于明确职责边界——Primary 负责生命周期管理,Secondary 专注单一任务并自我终结。