2

I have a Spring Boot Batch application that needs to run daily. It reads a daily file, does some processing on its data, and writes the processed data to a database. Along the way, the application holds some state such as the file to be read (stored in the FlatFileItemReader and JobParameters), the current date and time of the run, some file data for comparison between read items, etc.

One option for scheduling is to use Spring's @Scheduled such as:

@Scheduled(cron = "${schedule}")
public void runJob() throws Exception {
    jobRunner.runJob(); //runs the batch job by calling jobLauncher.run(job, jobParameters);
}

The problem here is that the state is maintained between runs. So, I have to update the file to be read, the current date and time of the run, clear the cached file data, etc.

Another option is to run the application via a unix cron job. This will obviously meet the need to clear state between runs but I prefer to tie the job scheduling to the application instead of the OS (and prefer it to OS agnostic). Can the application state be reset between @Scheduled runs?

James
  • 2,876
  • 18
  • 72
  • 116
  • @Thomas Kåsene, @Hansjoerg Wingeier, Thanks for the help. The approach works but I do have a fair number of beans with singleton scope. I suppose that I could change them all to `step` or perhaps `prototype` scope. That may be possible but seems cumbersome as compared to just using unix cron. I also looked at refreshing the context by using a scheduled static `runJob` in the main application class. But that fails giving a `GenericApplicationContext does not support multiple refresh attempts`. – James Aug 29 '16 at 15:46
  • I've added en example how you could treat singleton beans. have a look at my edited example. – Hansjoerg Wingeier Aug 30 '16 at 05:18

2 Answers2

4

You could always move the code that performs your task (and more importantly, keeps your state) into a prototype-scoped bean. Then you can retrieve a fresh instance of that bean from the application context every time your scheduled method is run.

Example

I created a GitHub repository which contains a working example of what I'm talking about, but the gist of it is in these two classes:

ScheduledTask.java

Notice the @Scope annotation. It specifies that this component should not be a singleton. The randomNumber field represents the state that we want to reset with every invocation. "Reset" in this case means that a new random number is generated, just to show that it does change.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class ScheduledTask {

    private double randomNumber = Math.random();

    void execute() {
        System.out.printf(
            "Executing task from %s. Random number is %f%n",
            this,
            randomNumber
        );
    }
}

TaskScheduler.java

By autowiring in ApplicationContext, you can use it inside the scheduledTask method to retrieve a new instance of ScheduledTask.

@Component
public class TaskScheduler {

    @Autowired
    private ApplicationContext applicationContext;

    @Scheduled(cron = "0/5 * * * * *")
    public void scheduleTask() {
        ScheduledTask task = applicationContext.getBean(ScheduledTask.class);
        task.execute();
    }
}

Output

When running the code, here's an example of what it looks like:

Executing task from com.thomaskasene.example.schedule.reset.ScheduledTask@329c8d3d. Random number is 0.007027
Executing task from com.thomaskasene.example.schedule.reset.ScheduledTask@3c5b751e. Random number is 0.145520
Executing task from com.thomaskasene.example.schedule.reset.ScheduledTask@3864e64d. Random number is 0.268644
Thomas Kåsene
  • 5,301
  • 3
  • 18
  • 30
  • Thanks for your answer. I would have marked as accepted, but Hansjoerg Wingeier added more information on how to handle this with multiple singleton beans in the context of a Spring Batch application. I did upvote your answer as it was very helpful. – James Sep 02 '16 at 20:28
2

Thomas' approach seems to be a reasonable solution, that's why I upvoted it. What is missing is how this can be applied in the case of a spring batch job. Therefore I adapted his example little bit:

@Component
public class JobCreatorComponent {


    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Job createJob() {
       // use the jobBuilderFactory to create your job as usual
       return jobBuilderFactory.get() ...
    }
}

your component with the launch method @Component public class ScheduledLauncher {

   @Autowired
   private ... jobRunner;

   @Autwired
   private JobCreatorComponent creator;

@Scheduled(cron = "${schedule}")
public void runJob() throws Exception {

    // it would probably make sense to check the applicationContext and
    // remove any existing job

    creator.createJob(); // this should create a complete new instance of 
                         //  the Job
    jobRunner.runJob(); //runs the batch job by calling jobLauncher.run(job, jobParameters);
}

I haven't tried out the code, but this is the approach I would try.

When constructing the job, it is important to ensure that all reader, processors and writers used in this job are complete new instances as well. This means, if they are not instantiated as pure java objects (not as spring beans) or as spring beans with scope "step" you must ensure that always a new instance is used.

Edited: How to handle SingeltonBeans Sometimes singleton beans cannot be prevented, in these cases there must be a way to "reset" them.

An simple approach would be to define an interface "ResetableBean" with a reset method that is implemented by such beans. Autowired can then be used to collect a list of all such beans.

@Component
public class ScheduledLauncher {

    @Autowired
    private List<ResetableBean> resetables;

    ...

    @Scheduled(cron = "${schedule}")
    public void runJob() throws Exception {
       // reset all the singletons
       resetables.forEach(bean -> bean.reset());
       ...
Hansjoerg Wingeier
  • 4,274
  • 4
  • 17
  • 25