Reset state before each Spring scheduled (@Schedul

2019-08-05 19:36发布

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?

2条回答
我只想做你的唯一
2楼-- · 2019-08-05 20:07

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());
       ...
查看更多
霸刀☆藐视天下
3楼-- · 2019-08-05 20:11

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
查看更多
登录 后发表回答