DEV Community

Sadiul Hakim
Sadiul Hakim

Posted on

Spring Batch Tutorial Part #3

1. Understanding Conditional Flow

In Spring Batch, a Job is made up of Steps that usually run in a linear sequence. However, you can control the execution flow of these steps based on the ExitStatus of a preceding step. This allows you to create branching logic, such as:

  • If Step A succeeds, run Step B.
  • If Step A fails, run Step C (an error-handling step).
  • If Step A completes with a specific status (e.g., "NO_DATA_FOUND"), run Step D (a cleanup step).

This conditional logic is managed using the .on(), .to(), and .end() operators in the JobBuilder.


2. Key Components

  • ExitStatus: An object returned by a StepExecution that indicates the result of a step's execution. Common statuses are COMPLETED, FAILED, and UNKNOWN. You can also define your own custom ExitStatus.
  • StepExecutionListener: This listener is crucial for conditional flows. Its afterStep() method is where you can inspect the step's result and return a custom ExitStatus that the job flow will use to make its decision.
  • JobBuilder: The builder class used to define the job's flow logic. The key methods for conditional flow are:
    • .on(String pattern): Specifies a pattern to match the ExitStatus of the previous step.
    • .to(Step step): Defines the next step to execute if the pattern matches.
    • .from(Step step): Specifies the step from which to start a new conditional flow.
    • .end(): Marks the end of a conditional flow path.

3. Example: A Simple Conditional Flow

Let's create a job with three steps: step1, step2, and step3.

  • step1 will be a decision step. It will either return a custom ExitStatus of COMPLETED or NO_DATA.
  • step2 will run only if step1 returns COMPLETED.
  • step3 will run only if step1 returns NO_DATA.

Step 1: Define a Custom StepExecutionListener

This listener will check a condition and set a custom ExitStatus. For this example, we'll use a simple boolean to simulate a "no data" scenario.

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;

public class MyStepDecider implements StepExecutionListener {
    private boolean hasData = true; // Simulating a condition

    @Override
    public void beforeStep(StepExecution stepExecution) {
        // You could perform a check here, e.g., check a database for new records.
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        if (!hasData) {
            // Set a custom ExitStatus based on the condition
            System.out.println("No data found, exiting step with NO_DATA status.");
            return new ExitStatus("NO_DATA");
        } else {
            System.out.println("Data found, continuing with COMPLETED status.");
            return ExitStatus.COMPLETED;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure the Job with Conditional Flow

Now, let's configure the job using JobBuilder to use the custom listener and define the conditional flow.

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class ConditionalFlowConfig {

    @Autowired
    private JobRepository jobRepository;

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean
    public Job conditionalJob() {
        return new JobBuilder("conditionalJob", jobRepository)
                .start(step1())
                // If step1 completes successfully (ExitStatus.COMPLETED), proceed to step2
                .on("COMPLETED").to(step2())
                // If step1 returns a custom status of "NO_DATA", proceed to step3
                .from(step1()).on("NO_DATA").to(step3())
                .end() // Terminates the flow
                .build();
    }

    @Bean
    public Step step1() {
        return new StepBuilder("step1", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    // This is where your business logic for the first step would go.
                    System.out.println("Executing Step 1...");
                    return null;
                }, transactionManager)
                .listener(new MyStepDecider()) // Attach the custom listener
                .build();
    }

    @Bean
    public Step step2() {
        return new StepBuilder("step2", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    System.out.println("Executing Step 2 (Data was found!).");
                    return null;
                }, transactionManager)
                .build();
    }

    @Bean
    public Step step3() {
        return new StepBuilder("step3", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    System.out.println("Executing Step 3 (No data was found!).");
                    return null;
                }, transactionManager)
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

This configuration shows how the job flow is not linear. The .on() method acts as a switch, directing the flow to a different step based on the ExitStatus of the previous one. This provides a powerful way to handle complex batch processes with multiple decision points.

Top comments (0)