Developer Docs

Custom Scheduled Tasks with SpringBot

SpringBot supports the creation of scheduled tasks to complete jobs at a specific time or frequency.

For this, we use the Quartz Job Scheduling Library to enable us to support a larger range of jobs, from simple to complex.

This guide will introduce you to how the the job scheduling works as well as how to create your own custom job.

Configuration

Quartz configuration is defined in two places. They are both found under serverside/src/main/resources/

Location  
quartz/quartz.properties You won’t need to modify this for the most part, but if you do, the configurable features can be found in the Quartz documentation.
application-default.properties Contains Spring-specific configurations for the Quartz scheduler. To modify these, override them in your profile of choice, e.g. serverside/src/main/resources/application-dev.properties

Basic concepts

A trigger defines the when, a job defines the how, and a job detail defines the what.

Trigger

A job trigger is the event that causes the job to run. This can be a simple trigger or a cron trigger.

Simple Trigger: A trigger that is based upon a frequency, e.g runs every two minutes.

Cron Trigger: A trigger that can be defined by a cron expression. This enable more complex scheduling, e.g. run at 2pm every second day.

Job

A job is the action that runs when the trigger fires. This can be as simple as logging “Hello World”, to something as complex as running an import once a day or updating the status of records based upon some expiry dates.

Job detail

Whereas a trigger defines the when, a job defines the how, a job detail defines the what. A job detail consists of a job and a trigger.

Task

In the context of this article, we refer to a task as the collection of job, trigger and job detail.

Getting started

We will be using the dev profile for the purpose of this example.

Enable your job scheduling.

  1. Open serverside/src/main/resources/application.properties
  2. Find the config option that looks like the following:

    # % protected region % [Disable or enable quartz here] off begin
     application.scheduled-tasks-enabled=false
        
     # % protected region % [Disable or enable quartz here] end
    
  3. Turn on the protected region and set application.scheduled-tasks-enabled=true. It should now look like the following:

    # % protected region % [Disable or enable quartz here] on begin
     application.scheduled-tasks-enabled=true
        
     # % protected region % [Disable or enable quartz here] end
    

Any jobs that we create can now run.

Making a custom scheduled task

For this project, we will be continuing with the LMS Example Project repository

User Story : As a Content Manager I want the readers to be aware if content has not been updated in the past month.

Acceptance Criteria :

  1. If the Last Modified Date was greater than 30 days ago (approx 1 month), the Summary must be edited to warn readers that the content may be out of date.

Create the job

The job is the key piece of logic that defines what is completed when a trigger fires.

For this job, we are going to set it to update the Article Summary based on the Last Modified Date date.

Files to change or to create:

File Name Description
ArticleRepository DAO layer of the article
ArticleService Service layer of Article, contains the actually business logic
OutdatedArticleJobService Service contains the business logic used by the Job
OutdatedArticleJob Quartz job class
  1. Open the repository found under serverside/src/main/java/lmsspring/repositories/ArticleRepository.java called ArticleRepository.java. Turn on the protected region called Import any additional imports here and copy in the following:
import lmsspring.entities.QArticleEntity;
import org.apache.commons.collections4.IterableUtils;
import java.time.OffsetDateTime;
  1. Next, turn on the protected region called Add any additional class methods here and copy in the following:
// % protected region % [Add any additional class methods here] on begin
default List<ArticleEntity> findOldArticleEntities(@NotNull OffsetDateTime earliestDate) {
    QArticleEntity articleEntity = QArticleEntity.articleEntity;
    return IterableUtils.toList(this.findAll(articleEntity.modified
            .before(
                    earliestDate
            )
        )
    );
    }
// % protected region % [Add any additional class methods here] end
  1. Open the service found under serverside/src/main/java/lmsspring/services/ArticleService.java called ArticleService.java. Turn on the protected region called Add any additional class methods here and copy in the following:
// % protected region % [Add any additional class methods here] on begin
  /**
    * Search for all articles that have a last modification date grater than 30 days prior to the current date.
    *
    * @return List of all Articles that are out of date.
    */
    public List<ArticleEntity> findByOutOfDate() {
        return this.repository.findOldArticleEntities(OffsetDateTime.now().minusDays(30)
        );
    }
// % protected region % [Add any additional class methods here] end
  1. Open the entity found under serverside/src/main/java/lmsspring/entities/ called ArticleEntity.java . Turn on the protected region called Add any additional class methods here and copy the following into it:
// % protected region % [Add any additional class methods  here] on begin
/*
 * Add an out of date warning to the article description.
 */
public void articleOutdated() {
    String outdatedMessage = " Some of the content in this article may be out of date.";
        if (!this.summary.contains(outdatedMessage)) {
            this.summary = this.summary + outdatedMessage;
        }
    }
// % protected region % [Add any additional class methods  here] end
  1. Create a new file under serverside/src/main/java/lmsspring/services/jobs called OutdatedArticleJobService.java. For this activity, an example job class already exists, and you can add more jobs here if required for future tasks.
package lmsspring.services.jobs;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import lmsspring.services.ArticleService;
import lmsspring.configs.security.helpers.AnonymousHelper;
import lmsspring.entities.ArticleEntity;

@Service
@ConditionalOnProperty(name = "application.scheduled-tasks-enabled")
@Slf4j
public class OutdatedArticleJobService implements JobService {

    private final ArticleService articleService;

    @Autowired
    public OutdatedArticleJobService(ArticleService articleService) {
        this.articleService = articleService;
    }

    public void executeJob() {
        /** 
         * The anonymous helper is a tool that 
         * allows us to bypass any security restrictions 
         * which is useful as a scheduled task has not got 
         * an authentication profile.
         * */
        AnonymousHelper.runAnonymously(() -> {
            processOutdatedArticles();
        });
    }

    /**
     *  Update summary of outdated Articles 
     */
    private void processOutdatedArticles() {
        var articles = articleService.findByOutOfDate();
        articles.forEach(ArticleEntity::articleOutdated);
        articleService.saveAll(articles);

        log.info("Marked {} articles as outdated", articles.size());
    }
}
We create a separate `Service` and `JobService` to separate the logic. The `Service` is just the business logic for that entity, like the `ArticleService` is just for the business logic for `Article`. Job service relates to the business logic for a certain `Job`. This means your code for `Job` and `Entity` are decoupled from each other.
  1. Finally, create the Job class. Create a new file in serverside/src/main/java/lmsspring/jobs called OutdatedArticleJob.java. You can make additional jobs by creating more job classes.
package lmsspring.jobs;

import lmsspring.services.jobs.OutdatedArticleJobService;
import lombok.Getter;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnProperty(name = "application.scheduled-tasks-enabled")
@DisallowConcurrentExecution
public class OutdatedArticleJob extends AbstractJob {

    @Getter
    private final static String description = "Update the summary of articles according to last modification date";

    @Getter
    private final static String name = "Outdated Article Job";

    @Autowired
    private OutdatedArticleJobService jobService;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        jobService.executeJob();
    }
}

This file is where we can define a description and name for the job. We can additionally link our service here. For details on the@DisallowConcurrentExecution annotation please see the Quartz documentation.

Considering the life cycle of the Job, we put the most of the business logic into the JobService. The Job simply invokes the method from the JobService

Complete the task

Now that we have a job, we need to create job detail and job trigger to complete the task.

Job detail

  1. Open the file located at serverside/src/main/java/lmsspring/configs/quartz/SchedulerConfig.java and locate the job detail. Turn on the protected region Add any additional imports here, and add the following code:
// % protected region % [Add any additional imports here] on begin
import lmsspring.jobs.OutdatedArticleJob; // required for Simple Job Trigger
import lmsspring.jobs.SimpleJob; // required for Cron Job
// % protected region % [Add any additional imports here] end
  1. Turn on the protected region [Add trigger and job details here], and add the following code:

    @Bean
    public JobDetailFactoryBean outdatedArticleJobDetail() {
     return JobHelpers.createJobDetail(
         OutdatedArticleJob.class,
         OutdatedArticleJob.getName(),
         OutdatedArticleJob.getDescription()
     );
    }
    

In the above, OutdatedArticleJob.class links to our job class created in step x above. " OutdatedArticleJob" refers to the job name and OutdatedArticleJob.getDescription() sets the description of the job.

Job trigger

There are two ways we can do this.

  1. Simple Job Trigger
  2. Cron Trigger
Simple job trigger
  1. Open the file located at serverside/src/main/java/lmsspring/configs/quartz/SchedulerConfig.java and locate the protected region at the bottom of the file, it will look like the following
// % protected region % [Add trigger and job details here] off begin
@Bean
public JobDetailFactoryBean simpleJobDetail() {
   return JobHelpers.createJobDetail(
           SimpleJob.class,
           "SimpleJob",
           SimpleJob.getDescription()
   );
}

@Bean
public SimpleTriggerFactoryBean simpleJobTrigger(@Qualifier("simpleJobDetail") JobDetail jobDetail) {
   return JobHelpers.createSimpleTrigger(jobDetail, Frequency.MINUTE.getMillis() * 2); // Run every two minutes
}
// % protected region % [Add trigger and job details here] end

You will notice that there is code already in this protected region, a job detail (simpleJobDetail) and a job trigger (simpleJobTrigger).

  1. Turn on the protected region and put the following code into the protected region. The frequency is every two minutes.
@Bean
public SimpleTriggerFactoryBean outdatedArticleJobTrigger(@Qualifier(" outdatedArticleJobDetail") JobDetail jobDetail) {
return JobHelpers.createSimpleTrigger(jobDetail, Frequency.MINUTE.getMillis() * 2); // Run every 2 minutes
}

The Frequency.java file in the same directory stores an enum with useful values for frequency.

Another important item to keep in mind is that the bean name of the Job Detail has to match the Qualifier in the parameter of the of the trigger method. This is to find the specific bean for the job detail in the container.

  1. Now start your application. Any articles that you create will automatically have their description changed based on their last modified date.
Cron job
  1. Open the file located at serverside/src/main/java/lmsspring/configs/quartz/SchedulerConfig.java and locate the protected region Add trigger and job details here at the bottom of the file.
  2. Turn on the protected region and put the following code into the protected region
@Bean
public CronTriggerFactoryBean outdatedArticleJobeJobMidnightTrigger(@Qualifier(" outdatedArticleJobDetail") JobDetail jobDetail) {
    return JobHelpers.createCronTrigger(jobDetail, CronFrequency.MIDNIGHT.getExpression());
}

It is worth noting that Frequency.java in the same directory stores an enum with useful values for frequency. You could defined other useful value of frequency in the Frequency.java file

Another important item to keep in mind is that the bean name of the Job Detail has to match the Qualifier in the parameter of the of the trigger method.

To help create the Cron Expression, you can use the too in Cron Expression Generator

  1. Now start your application. Any articles that you create will automatically have their status changed based on the rules we laid out above once a day at midnight.
  2. For quicker testing change the time parameters to seconds, and then add articles with the interface. After the given time period you should see the outdated message appear on the screen.

Solution

Have a look at the custom-scheduler-task branch to see the code solution.