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
相关产品推荐
相关产品推荐

