Spring Batch - 如何基于上一步中创建的参数生成并行步骤

时间:2016-05-18 21:48:29

标签: java spring-batch jobs spring-java-config late-binding

简介

我正在尝试使用在tasklet中创建的作业参数来创建执行tasklet之后的步骤。

一个tasklet试图找到一些文件(findFiles()),如果找到一些文件,它会将文件名保存到一个字符串列表中。

在tasklet中,我传递数据如下:     chunkContext.getStepContext().getStepExecution().getExecutionContext().put("files", fileNames);

下一步是并行流程,对于每个文件,将执行一个简单的读取器 - 处理器 - 写入器步骤(如果您对我如何到达那里感兴趣,请参阅我之前的问题:Spring Batch - Looping a reader/processor/writer step

在构建作业readFilesJob()时,最初使用“假”文件列表创建流,因为只有在执行了tasklet之后才知道真正的文件列表。

问题

如何配置我的作业以便首先执行tasklet,然后使用从tasklet生成的文件列表执行并行流程?

我认为它归结为在运行时正确的时刻获取加载了正确数据的文件名列表......但是如何?

重现

这是我的简化配置:

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    private static final String FLOW_NAME = "flow1";
    private static final String PLACE_HOLDER = "empty";

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    public List<String> files = Arrays.asList(PLACE_HOLDER);

    @Bean
    public Job readFilesJob() throws Exception {   
        List<Step> steps = files.stream().map(file -> createStep(file)).collect(Collectors.toList());

        FlowBuilder<Flow> flowBuilder = new FlowBuilder<>(FLOW_NAME);

        Flow flow = flowBuilder
                .start(findFiles())             
                .next(createParallelFlow(steps))
                .build();       

        return jobBuilderFactory.get("readFilesJob")                
                .start(flow)                
                .end()
                .build();
    }

    private static Flow createParallelFlow(List<Step> steps){
        SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
        taskExecutor.setConcurrencyLimit(steps.size());

        List<Flow> flows = steps.stream()
                .map(step ->
                        new FlowBuilder<Flow>("flow_" + step.getName()) 
                        .start(step) 
                        .build()) 
                .collect(Collectors.toList());

        return new FlowBuilder<SimpleFlow>("parallelStepsFlow").split(taskExecutor) 
             .add(flows.toArray(new Flow[flows.size()]))
             .build();      
    }

    private Step createStep(String fileName){
        return stepBuilderFactory.get("readFile" + fileName)
                .chunk(100)
                .reader(reader(fileName))               
                .writer(writer(filename))                               
                .build();
    }

    private FileFinder findFiles(){
        return new FileFinder();
    }
}

研究

来自How to safely pass params from Tasklet to step when running parallel jobs的问答建议在读者/作者中使用这样的结构:

@Value("#{jobExecutionContext[filePath]}") String filePath

但是,由于在createParallelFlow()方法中创建步骤的方式,我真的希望可以将fileName作为字符串传递给读取器/写入器。因此,即使这个问题的答案可能是我的问题的解决方案,它也不是理想的解决方案。但如果我错了,请不要纠正我。

我正在使用文件名示例来更好地澄清问题。我的问题实际上不是从目录中读取多个文件。我的问题实际上归结为在运行时生成数据并将其传递给下一个动态生成的步骤的想法。

编辑:

添加了fileFinder的简化tasklet。

@Component
public class FileFinder implements Tasklet, InitializingBean {

    List<String> fileNames;

    public List<String> getFileNames() {
        return fileNames;
    }

    @PostConstruct
    public void afterPropertiesSet() {
        // read the filenames and store dem in the list
        fileNames.add("sample-data1.csv");
        fileNames.add("sample-data2.csv");
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        // Execution of methods that will find the file names and put them in the list...
        chunkContext.getStepContext().getStepExecution().getExecutionContext().put("files", fileNames);                     
        return RepeatStatus.FINISHED;
    }    
}

1 个答案:

答案 0 :(得分:1)

我不确定,如果我确实理解了您的问题,但据我所知,您需要在动态构建作业之前使用文件名的列表。

你可以这样做:

@Component
public class MyJobSetup {
    List<String> fileNames;

    public List<String> getFileNames() {
        return fileNames;
    }

    @PostConstruct
    public void afterPropertiesSet() {
        // read the filenames and store dem in the list
        fileNames = ....;
    }
}

之后,您可以在JobConfiguration Bean中注入此Bean

@Configuration
@EnableBatchProcessing
@Import(MyJobSetup.class)
public class BatchConfiguration {

    private static final String FLOW_NAME = "flow1";
    private static final String PLACE_HOLDER = "empty";

    @Autowired
    private  MyJobSetup jobSetup; // <--- Inject
          // PostConstruct of MyJobSetup was executed, when it is injected

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    public List<String> files = Arrays.asList(PLACE_HOLDER);

    @Bean
    public Job readFilesJob() throws Exception {   
        List<Step> steps = jobSetUp.getFileNames() // get the list of files
             .stream() // as stream
             .map(file -> createStep(file)) // map...
             .collect(Collectors.toList()); // and create the list of steps