单体Spring Boot项目支持Web与多CLI进程的架构实现咨询
单模块Spring Boot多进程实现方案(符合Spring最佳实践)
针对你的需求,以下是一套基于Spring Boot条件化配置的单模块实现方案,既能支持Web服务启动,也能独立运行多个CLI进程,同时避免多模块带来的复杂度,适配小团队快速交付MVP的场景。
核心思路
通过主类分发逻辑+条件化配置实现不同进程的启动:
- 主类解析命令行参数(如
--mode),决定启动Web环境还是CLI环境 - 用Spring的
@ConditionalOnProperty注解控制不同进程的组件、配置是否加载 - CLI进程通过
CommandLineRunner实现业务逻辑,仅在对应模式下实例化执行
具体实现步骤
1. 主类:命令行参数解析与环境配置
主类作为唯一入口,根据--mode参数设置Web环境类型,并传递参数给Spring上下文,触发条件化配置:
@SpringBootApplication public class MultiModeApplication { public static void main(String[] args) { String mode = extractCommandLineArg(args, "mode"); if (mode == null) { System.err.println("必须指定运行模式:--mode=web | --mode=ingestion | --mode=analysis"); System.exit(1); } SpringApplication app = new SpringApplication(MultiModeApplication.class); // 非Web模式下关闭Web服务器 if (!"web".equals(mode)) { app.setWebApplicationType(WebApplicationType.NONE); } // 将mode存入环境变量,供条件注解识别 app.setDefaultProperties(Map.of("app.run.mode", mode)); app.run(args); } // 通用参数提取方法 private static String extractCommandLineArg(String[] args, String argName) { for (String arg : args) { String prefix = "--" + argName + "="; if (arg.startsWith(prefix)) { return arg.substring(prefix.length()); } } return null; } }
2. Web服务组件:条件化加载
Web相关的控制器、配置仅在mode=web时加载:
@RestController @ConditionalOnProperty(name = "app.run.mode", havingValue = "web") @RequestMapping("/reports") public class ReportController { private final ReportService reportService; public ReportController(ReportService reportService) { this.reportService = reportService; } @GetMapping public List<ReportDto> generateReport() { return reportService.generateFromMysql(); } } // Web专属配置示例(可选) @Configuration @ConditionalOnProperty(name = "app.run.mode", havingValue = "web") public class WebConfig { // 配置Web相关Bean,如拦截器、跨域规则等 }
3. CLI进程实现:独立逻辑与条件化执行
每个CLI进程对应一个CommandLineRunner,仅在匹配的mode下执行:
数据采集进程(Ingestion)
@Component @ConditionalOnProperty(name = "app.run.mode", havingValue = "ingestion") public class IngestionProcess implements CommandLineRunner { private final PulsarProducer pulsarProducer; private final ExternalDataSource externalDataSource; public IngestionProcess(PulsarProducer pulsarProducer, ExternalDataSource externalDataSource) { this.pulsarProducer = pulsarProducer; this.externalDataSource = externalDataSource; } @Override public void run(String... args) throws Exception { // 拉取外部数据 List<DataRecord> data = externalDataSource.pullBatch(); // 发送到Pulsar队列 pulsarProducer.sendBatch("data-ingestion-topic", data); // 任务完成后主动退出进程 System.exit(0); } }
数据分析进程(Analysis)
@Component @ConditionalOnProperty(name = "app.run.mode", havingValue = "analysis") public class AnalysisProcess implements CommandLineRunner { private final PulsarConsumer pulsarConsumer; private final MysqlRepository mysqlRepository; public AnalysisProcess(PulsarConsumer pulsarConsumer, MysqlRepository mysqlRepository) { this.pulsarConsumer = pulsarConsumer; this.mysqlRepository = mysqlRepository; } @Override public void run(String... args) throws Exception { // 消费Pulsar数据并处理 pulsarConsumer.consume("data-ingestion-topic", record -> { ProcessedData processed = analyzeData(record); mysqlRepository.save(processed); }); // 持续消费,无需主动退出(如需批量处理可自行控制) } private ProcessedData analyzeData(DataRecord record) { // 数据分析逻辑 return new ProcessedData(); } }
4. 多采集进程扩展方案
针对你提到的20-30个轻量级采集进程,无需为每个进程写独立的CommandLineRunner,可以通过通用采集器+数据源参数实现:
// 定义采集器接口 public interface DataSourceCollector { List<DataRecord> pullData(); String getSourceType(); } // MySQL采集实现 @Component public class MysqlCollector implements DataSourceCollector { @Override public List<DataRecord> pullData() { /* MySQL拉取逻辑 */ } @Override public String getSourceType() { return "mysql"; } } // Kafka采集实现 @Component public class KafkaCollector implements DataSourceCollector { @Override public List<DataRecord> pullData() { /* Kafka拉取逻辑 */ } @Override public String getSourceType() { return "kafka"; } } // 通用采集进程 @Component @ConditionalOnProperty(name = "app.run.mode", havingValue = "ingestion") public class GenericIngestionProcess implements CommandLineRunner { private final List<DataSourceCollector> collectors; private final PulsarProducer pulsarProducer; public GenericIngestionProcess(List<DataSourceCollector> collectors, PulsarProducer pulsarProducer) { this.collectors = collectors; this.pulsarProducer = pulsarProducer; } @Override public void run(String... args) throws Exception { String source = extractCommandLineArg(args, "source"); if (source == null) { System.err.println("必须指定数据源:--source=mysql | --source=kafka | ..."); System.exit(1); } DataSourceCollector collector = collectors.stream() .filter(c -> source.equals(c.getSourceType())) .findFirst() .orElseThrow(() -> new IllegalArgumentException("未知数据源:" + source)); List<DataRecord> data = collector.pullData(); pulsarProducer.sendBatch("data-ingestion-topic", data); System.exit(0); } }
5. Spring Modulith的角色
Spring Modulith主要用于单模块内的代码边界划分,它不直接控制进程启动,但可以配合上述方案优化代码结构:
- 将Web、Ingestion、Analysis逻辑分别放在
com.xxx.app.web、com.xxx.app.ingestion、com.xxx.app.analysis包下 - 用
@Module标记每个子模块,通过Modulith工具检查模块间依赖合理性 - 进程启动逻辑仍由主类的
--mode参数控制,Modulith仅帮助保持代码整洁,不增加部署复杂度
启动命令示例
# 启动Web服务(8080端口) java -jar your-app.jar --mode=web # 启动MySQL数据采集进程 java -jar your-app.jar --mode=ingestion --source=mysql # 启动数据分析进程 java -jar your-app.jar --mode=analysis
方案优势
- 单模块部署,避免多模块的构建、维护复杂度
- 完全基于Spring条件化配置最佳实践,符合Spring生态规范
- 灵活扩展CLI进程,新增采集器仅需实现
DataSourceCollector接口 - 代码结构清晰,小团队易上手、易维护,适配MVP快速交付
内容的提问来源于stack exchange,提问作者Nawab
相关产品推荐
相关产品推荐

