You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Batch多格式CSV读取的动态适配方案优化咨询

问题背景

我有一个存放不同CSV文件的目录:

  • data_1_status.csv包含3个字段ID、status1和status2(无表头,空状态合法)
2984,COMPLETED,COMPLETED
2985,COMPLETED,COMPLETED
2986,COMPLETED,FAILED
2987,,
  • data_2_status.csv包含4个字段ID、status1、status2和status3(无表头,空状态合法)
16658,COMPLETED,COMPLETED,COMPLETED
16659,COMPLETED,COMPLETED,FAILED
16660,COMPLETED,FAILED,FAILED
16661,,,

从数据库中可获取各data_x对应的状态字段数量,我希望无需为每个data_x开发单独的Job/Step,后续新增data_x时也无需修改代码。

排除方案

我排除使用delimitedLineTokenizer.setStrict(false)搭配假字段列表的方案,例如:

delimitedLineTokenizer.setNames("id", "steps[0]", "steps[1]", "steps[2]", "steps[3]", "steps[4]", "steps[5]", "steps[6]", "steps[7]", "steps[8]", "steps[9]");

因为需要在字段缺失或冗余时抛出FlatFileParseException,且要区分空字段与缺失字段:

2984,COMPLETED,COMPLETED      -> 合法(3个字段)
2984,COMPLETED,COMPLETED,     -> 抛出FlatFileParseException(4个字段,冗余)
2984,COMPLETED,               -> 合法(3个字段)
2984,,                        -> 合法(3个字段)
2984,COMPLETED                -> 抛出FlatFileParseException(2个字段,缺失)
优化需求

我找到一种实现方式但并不满意:已废弃的getCurrentResource()让我顾虑,是否有替代方案?
当然无法使用ResourceAware,因为StatusCsvLine尚未完成映射。

@StepScope
@Bean(name = "statusesItemReader")
MultiResourceItemReader<StatusCsvLine> statusItemReader() throws IOException {
    MultiResourceItemReader<StatusInputCsvLine> reader = new MultiResourceItemReader<>();
    ResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
    Resource[] resources = patternResolver.getResources("file:/tmp/statuses/*_status.csv");
    reader.setStrict(false);
    reader.setResources(resources);
    // 此处为不妥之处:reader.getCurrentResource()已废弃,但必须使用
    // 且需通过Callable延迟调用,因为getCurrentResource()需在Step执行期间调用(而非Bean初始化时)
    FlatFileItemReader<StatusInputCsvLine> itemReader = statusFlatFileItemReader(() -> reader.getCurrentResource().getFilename());
    reader.setDelegate(itemReader);
    return reader;
}
FlatFileItemReader<StatusCsvLine> statusItemReader(Callable<String> resourceNameCallable) {
    BeanWrapperFieldSetMapper<StatusCsvLine> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
    beanWrapperFieldSetMapper.setTargetType(StatusCsvLine.class);

    DefaultLineMapper<StatusCsvLine> defaultLineMapper = new DefaultLineMapper<StatusCsvLine>() {
        @Override
        public StatusCsvLine mapLine(String line, int lineNumber) throws Exception {
            String dataType = resourceNameCallable.call().replace("_status.csv", ""); // data_1 或 data_2

            
            // 配置示例: {
            //      dataType: "data_1",
            //      steps: ["step_1", "step_2"]
            //  },{
            //      dataType: "data_2",
            //      steps: ["step_1", "step_2", "step_3"]
            //  }
            //
            //  @Getter
            //  @Setter
            //  class ConfigData {
            //      private String dataType;
            //      private String[] steps;
            //  }
            
            ConfigData conf = configDataRepository.findByName(dataType);


            //
            //  @Getter
            //  @Setter
            //  class StatusCsvLine {
            //      private String id;
            //      private String[] statusSteps;
            //  }
            //
            // 创建与StatusCsvLine类匹配的分词器字段名
            
            List<String> names = new ArrayList<>();
            // 添加"id"字段
            names.add("id");
            // 添加"statusSteps[x]"字段
            for (int i = 0; i < conf.status().length; i++) {
                names.add("statusSteps[" + i + "]");
            }

            // 创建新的分词器
            DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer();
            // 将List<String>转换为String[]
            delimitedLineTokenizer.setNames(names.toArray(new String[0])); // 等价于delimitedLineTokenizer.setNames("id", "statusSteps[0]", "statusSteps[1]", "statusSteps[2]")
            delimitedLineTokenizer.setStrict(true);
            
            // 实时更新分词器
            super.setLineTokenizer(delimitedLineTokenizer);
            
            // 使用新的自定义分词器调用映射方法
            return super.mapLine(line, lineNumber);
        }
    };
    defaultLineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);

    return new FlatFileItemReaderBuilder<StatusCsvLine>()
            .name("statusItemReader")
            .lineMapper(defaultLineMapper)
            .build();
}

编辑1:基于Mahmoud的回答

由于MultiResourceItemReader并非为处理多格式设计,我决定不再使用它。
我改为基于配置动态创建Step,这意味着每次执行都需重建Job(是否正确?),此方案可行。
我通过创建原型作用域的Job Bean实现:

@Bean(name = "statusJob")
@Scope("prototype")
Job statusJob() {
    SimpleJob statusJob = new SimpleJob("statusJob");
    statusJob.setJobRepository(jobRepository);
    statusJob.setSteps(statusJobSteps()); // 动态创建Step
    return statusJob;
}

调度器通过以下方式获取新的Job实例:

@Scheduled(cron = "${batch.scheduler.cron.status}")
public void statusJobScheduler() throws /*...*/ {
    jobLauncher.run(context.getBean("statusJob", Job.class), new JobParametersBuilder()
            .addLong("timestamp", System.currentTimeMillis())
            .toJobParameters());
}

我对该方案的合理性存疑,虽比之前的方案更满意,但不确定Spring是否提供无需原型作用域即可重建Job的方案,或能否在现有Job中增删Step以适配配置变更?是否有更优实现方式?


内容的提问来源于stack exchange,提问作者Dracuire

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 17:34:57