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.
- Open
serverside/src/main/resources/application.properties
-
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
-
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 :
- If the
Last Modified Date
was greater than 30 days ago (approx 1 month), theSummary
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 |
-
Open the repository found under
serverside/src/main/java/lmsspring/repositories/ArticleRepository.java
calledArticleRepository.java
. Turn on the protected region calledImport any additional imports here
and copy in the following:import lmsspring.entities.QArticleEntity; import org.apache.commons.collections4.IterableUtils; import java.time.OffsetDateTime;
-
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
-
Open the service found under
serverside/src/main/java/lmsspring/services/ArticleService.java
calledArticleService.java
. Turn on the protected region calledAdd 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
-
Open the entity found under
serverside/src/main/java/lmsspring/entities/
calledArticleEntity.java
. Turn on the protected region calledAdd 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
-
Create a new file under
serverside/src/main/java/lmsspring/services/jobs
calledOutdatedArticleJobService.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
andJobService
to separate the logic. TheService
is just the business logic for that entity, like theArticleService
is just for the business logic forArticle
. Job service relates to the business logic for a certainJob
. This means your code forJob
andEntity
are decoupled from each other. -
Finally, create the Job class. Create a new file in
serverside/src/main/java/lmsspring/jobs
calledOutdatedArticleJob.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
-
Open the file located at
serverside/src/main/java/lmsspring/configs/quartz/SchedulerConfig.java
and locate the job detail. Turn on the protected regionAdd 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
-
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.
Simple job trigger
-
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
). -
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.
-
Now start your application. Any articles that you create will automatically have their description changed based on their last modified date.
Cron job
- Open the file located at
serverside/src/main/java/lmsspring/configs/quartz/SchedulerConfig.java
and locate the protected regionAdd trigger and job details here
at the bottom of the file. -
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
- 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.
- 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.
Related articles
Was this article helpful?