springspring-bootspring-batch

Handling Spring Batch flow job


Situation :

There is a batch steps named A, B, C, D, E.

A is always executed first.

Depending on A's exitStatus, if A gives "SOMECONDITION", B is executed. On "OTHERCONDITION" C is executed. Job instances are not allowed to execute both B and C.

Regardless of executed Step is B or C, they should go to D.

After executing D, by using next(), E is executed.

Problem : Ugly code structure.

By redundant use of on(), to(), I managed to figure out the flow that I need.

Figure 1. Ugly nexts on each on-to conditionals

As directly observable, there is really ugly code next() on each on-to, to direct them into common flow.

Adding more ugliness, I have to use from()-on("*") to direct each of on-to-next to some common flow.

Question :

At least this works, but i wonder, is this the only code that satisfies above condition?


Solution

  • Problem : Ugly code structure.

    Adding more ugliness, I have to [...]

    We, the Spring team, are always open to constructive feedback. So if you have ideas on how to improve the DSL of defining job flows, you are welcome to open a ticket on our issue tracker and we will do our best to improve things if needed.

    Now to answer your question, the next(...) statements after each on("..").to("..") are not necessary. This is not what the documentation says, see example here: Conditional Flow.

    According to your description, the flow to implement is the following:

    flow

    This is actually a conditional flow (A -> decider -> B or C) followed by a sequential flow (D -> E).

    The way I would define such flow is something like:

    @Bean
    public Job job() {
        Flow conditionalFlow = new FlowBuilder<SimpleFlow>("conditionalFlow")
                .start(stepA())
                .next(decider()).on("SOMECONDITION").to(stepB())
                .from(decider()).on("OTHERCONDITION").to(stepC())
                .end();
    
    
        return new JobBuilder("myJob", jobRepository)
                .start(conditionalFlow)
                .next(stepD())
                .next(stepE())
                .end()
                .build();
    }
    

    This way, the flows are clearly defined. Here is a complete sample that you can run and check the results (make sure to use Spring Batch 5 and to have HSQLDB in the classpath):

    package org.springframework.batch.sample;
    
    import javax.sql.DataSource;
    
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.core.Step;
    import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
    import org.springframework.batch.core.job.builder.FlowBuilder;
    import org.springframework.batch.core.job.builder.JobBuilder;
    import org.springframework.batch.core.job.flow.Flow;
    import org.springframework.batch.core.job.flow.FlowExecutionStatus;
    import org.springframework.batch.core.job.flow.JobExecutionDecider;
    import org.springframework.batch.core.job.flow.support.SimpleFlow;
    import org.springframework.batch.core.launch.JobLauncher;
    import org.springframework.batch.core.repository.JobRepository;
    import org.springframework.batch.core.step.builder.StepBuilder;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
    import org.springframework.jdbc.support.JdbcTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    @Configuration
    @EnableBatchProcessing
    public class MyJobConfiguration {
    
        private final JobRepository jobRepository;
        private final PlatformTransactionManager transactionManager;
    
        public MyJobConfiguration(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
            this.jobRepository = jobRepository;
            this.transactionManager = transactionManager;
        }
    
        private Step createStep(String stepName) {
            return new StepBuilder("step" + stepName, jobRepository)
                    .tasklet((contribution, chunkContext) -> {
                        System.out.println("running step " + stepName);
                        return RepeatStatus.FINISHED;
                    }, transactionManager)
                    .build();
        }
    
        @Bean
        public Step stepA() {
            return createStep("A");
        }
    
        @Bean
        public Step stepB() {
            return createStep("B");
        }
    
        @Bean
        public Step stepC() {
            return createStep("C");
        }
    
        @Bean
        public Step stepD() {
            return createStep("D");
        }
    
        @Bean
        public Step stepE() {
            return createStep("E");
        }
    
        @Bean
        public JobExecutionDecider decider() {
            return (jobExecution, stepExecution) -> {
                return new FlowExecutionStatus("SOMECONDITION"); // SOMECONDITION, OTHERCONDITION
            };
        }
    
    @Bean
    public Job job() {
        Flow conditionalFlow = new FlowBuilder<SimpleFlow>("conditionalFlow")
                .start(stepA())
                .next(decider()).on("SOMECONDITION").to(stepB())
                .from(decider()).on("OTHERCONDITION").to(stepC())
                .end();
    
    
        return new JobBuilder("myJob", jobRepository)
                .start(conditionalFlow)
                .next(stepD())
                .next(stepE())
                .end()
                .build();
    }
    
        public static void main(String[] args) throws Exception {
            ApplicationContext context = new AnnotationConfigApplicationContext(MyJobConfiguration.class);
            JobLauncher jobLauncher = context.getBean(JobLauncher.class);
            Job job = context.getBean(Job.class);
            jobLauncher.run(job, new JobParameters());
        }
    
    
        // infrastructure beans
    
        static class DataSourceConfiguration {
            @Bean
            public DataSource dataSource() {
                return new EmbeddedDatabaseBuilder()
                        .setType(EmbeddedDatabaseType.HSQL)
                        .addScript("/org/springframework/batch/core/schema-hsqldb.sql")
                        .build();
            }
    
            @Bean
            public JdbcTransactionManager transactionManager(DataSource dataSource) {
                return new JdbcTransactionManager(dataSource);
            }
        }
    
    }
    

    This sample prints:

    running step A
    running step B
    running step D
    running step E
    

    If you make the decider return "OTHERCONDITION", the sample prints:

    running step A
    running step C
    running step D
    running step E
    

    which is what is required from your description.