Introduce about Scheduled Tasks

In the world of Spring Boot, scheduling tasks is an essential part of managing various background processes. Quartz is a powerful library that simplifies task scheduling in Spring Boot applications. In this blog, we will explore the use of Quartz for scheduling tasks.

Why We Need Scheduled Tasks

Scheduled tasks play a crucial role in modern software applications. Here’s why they are indispensable:

  • Automation: Scheduled tasks automate routine processes, reducing manual intervention and the risk of human error.
  • Efficiency: They enable efficient resource utilization by running tasks at non-peak hours or during idle periods.
  • Notification: Scheduled tasks can trigger notifications, alerts, and reports, keeping stakeholders informed.
  • Maintenance: They aid in system maintenance, such as database cleanup, log rotation, and backups.

Foe example, imagine you have an e-commerce application, and you want to cancel those orders which are not paid in 15 minutes. You can’t ask consumers to use your API on time after 15 minutes. To achieve this, you can schedule a Job which will start on time and cancel the order.

Of course, this is just the tip of the iceberg of its many uses.

Quartz Operation Mechanism

Basic Understanding

Before diving into the usage of Quartz, it’s essential to have a basic understanding of the three fundamental modules used in Quartz.

  • Job: What need to be done
  • Trigger: What time to do your jobs
  • Scheduler: Link jobs and triggers

Processes

Here is the basic processes about how does a job been executed. We create a Job class implementing the Job interface which can execute the JobExecuteContext, create a JobDetail instance and a Trigger instance, and create a Scheduler instance. Then, bind the job to the trigger and send them to scheduler. When the scheduler starts and the set trigger time arrives, the job will be executed.

Basic_Processes

Besides, a job can be triggered by multiple triggers, and the job will be executed multiple times.

One_Job_Multiple_Triggers

Basic Usage

Let’s start with the fundamental aspects of using Quartz in Spring Boot.

Import Maven Dependency

To get started, you need to include the Quartz dependency in your Spring Boot project. Just like other dependencies, you need to import the dependency in your pom.xml file.

<!-- quartz -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

Job Class

Quartz jobs are at the heart of task scheduling. It is a class file implementing the Job interface. In this class, you are required to implement a method named “execute”.

For example:

public class YourJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("Your Job Is Been Executing");
    }
}

Trigger

Triggers determine when and how often a job should run. I usually use SimpleTrigger and CronTrigger, which can cover the majority of my use cases.

The simple trigger uses date as the Date Object as the parameter, while the cron trigger uses cron expression as the parameter.

Here are the examples of them:

// Simple Trigger
Date date = new Date();
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
    .withIdentity("simpleTriggerName", "simpleTriggerGroupName")
    .startAt(new Date date.setTime(date.getTime() + 5000)) // 5s later
    .build();

// Cron Trigger
CronTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("CronTriggerName", "CronTriggerGroupName")
    .startNow()
    .withSchedule(CronScheduleBuilder.cronSchedule("0 0 10 * * ?")) // Every day at 10 AM.
    .build();

JobDetail

The creation of JobDetail object is similar to trigger.

Here is the example:

JobDetail jobDetail = JobBuilder.newJob(YourJob.class)
                .withIdentity("JobDetailName", "JobDetailGroupName")
                .build();

Result

Finally, let’s put the job and the trigger into scheduler.

Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
scheduler.scheduleJob(jobDetail, simpleTrigger);

If the scheduler is running in the ApplicationTests, you should add these code below, unless the scheduler will stop with the ApplicationTests.

try {
    Thread.sleep(6000); // slepp 6s
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}

Then, you will see Your Job Is Been Executing in the terminal.

Advanced Usage

Once you’ve understood the basic part, it’s time to explore the advanced Quartz usage.

Encapsulation

Learn how to encapsulate your Quartz and make your code more maintainable and organized.

In our recent project, we encapsulated some methods we needed into SchedulerUtil, like addJob, resetJobTrigger, removeJob, isJobExist, and so on.

Here is the simple version of our SchedulerUtil

import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.lang.Nullable;

import java.util.Date;

@Slf4j
public class SchedulerUtil {
    private static final StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();

    public static Scheduler getScheduler() throws SchedulerException {
        return schedulerFactory.getScheduler();
    }

    public static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class<? extends org.quartz.Job> jobClass, @Nullable JobDataMap jobDataMap, String cron) throws SchedulerException {
        Scheduler scheduler = getScheduler();
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(jobName, jobGroupName)
                .setJobData(jobDataMap)
                .build();
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerName, triggerGroupName)
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                .build();
        scheduler.scheduleJob(jobDetail, trigger);
    }
    
    public static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class<? extends org.quartz.Job> jobClass, @Nullable JobDataMap jobDataMap, Date date) throws SchedulerException {
        Scheduler scheduler = getScheduler();
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(jobName, jobGroupName)
                .setJobData(jobDataMap)
                .build();
        SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
                .withIdentity(triggerName, triggerGroupName)
                .startAt(date)
                .build();
        scheduler.scheduleJob(jobDetail, simpleTrigger);
    }
                 
     public static void resetJobTrigger(String triggerName, String triggerGroupName, String cron) throws Exception {
        Scheduler scheduler = getScheduler();
        TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
        CronTriggerImpl trigger = (CronTriggerImpl) scheduler.getTrigger(triggerKey);
        if (!trigger.getCronExpression().equalsIgnoreCase(cron)) {
            trigger.setCronExpression(cron);
            scheduler.rescheduleJob(triggerKey, trigger);
        }
    }

    public static void resetJobTrigger(String triggerName, String triggerGroupName, Date date) throws Exception {
        Scheduler scheduler = getScheduler();
        TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
        SimpleTrigger trigger = (SimpleTrigger) scheduler.getTrigger(triggerKey);
        if (!trigger.getStartTime().equals(date)) {
            SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
                    .withIdentity(triggerKey)
                    .startAt(date)
                    .build();
            scheduler.rescheduleJob(triggerKey, simpleTrigger);
        }
    }
                 
	public static void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException {
        Scheduler scheduler = getScheduler();
        if (!isJobExist(jobName, jobGroupName, triggerName, triggerGroupName)) {
            log.info("Job: {} is not exist.", jobName);
            return;
        }
        TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
        scheduler.pauseTrigger(triggerKey);
        scheduler.unscheduleJob(triggerKey);
        scheduler.deleteJob(new JobKey(jobName, jobGroupName));
    }

    public static Boolean isJobExist(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException {
        Scheduler scheduler = getScheduler();
        return scheduler.checkExists(new JobKey(jobName, jobGroupName)) && scheduler.checkExists(new TriggerKey(triggerName, triggerGroupName));
    }
}

In our project, it is not allowed to directly create JobDetail or any kinds of trigger object in the service classes, because they will make the original business code verbose. In addition, we employ method polymorphism, which makes the use of SchedulerUtil more concise and user-friendly.

Start with Spring Boot

In our project, we have nothing to do with manual operation and every job is under control. As a result, let the Quartz scheduler start with Spring Boot Application can be convenient.

In the project, I write the starter in SchedulerConfig.

@Slf4j
@Configuration
public class SchedulerConfig implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        try {
            StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
            Scheduler scheduler = schedulerFactory.getScheduler();
            scheduler.start();
        } catch (SchedulerException e) {
            throw new RuntimeException();
        }
    }
}

When your Spring Boot Application starts, it will run the config class and start your scheduler instance.

Persistence with MySQL

To make your scheduled tasks more resilient, Quartz can store its tasks and triggers into database, like MySQL. For example, your application is going to update and has to restart, but some jobs are still in the scheduler and are stored in your RAM. When you shut down your application, all of the jobs will lose.

Before you start to store your jobs into your disks, you need to create a few tables in your database and improve the relative JDBC dependency. You can find the SQL file for your database here -> jdbcjobstore
Secondly, write Quartz properties and I prefer to use yml.

spring:
  datasource:
    quartz:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/quartz
      username: root
      password: 123456
  quartz:
    job-store-type: jdbc
    scheduler-name: MyScheduler
    wait-for-jobs-to-complete-on-shutdown: false
    jdbc:
      initialize-schema: never
    properties:
      org:
        quartz:
          jobStore:
            dataSource: quartzDataSource 
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
          threadPool:
            threadCount: 25 # default 10 。
            threadPriority: 5
            class: org.quartz.simpl.SimpleThreadPool

Then, complete the SchedulerConfig configuration class.

@Configuration
@EnableScheduling
public class SchedulerConfig {

    @Value("${spring.datasource.url}")
    private String dataSourceUrl;

    @Value("${spring.datasource.username}")
    private String dataSourceUsername;

    @Value("${spring.datasource.password}")
    private String dataSourcePassword;

    @Value("${spring.datasource.driver-class-name}")
    private String dataSourceDriverClassName;

    @Value("${spring.quartz.scheduler-name}")
    private String schedulerName;

    @Value("${spring.quartz.wait-for-jobs-to-complete-on-shutdown}")
    private boolean waitForJobsToCompleteOnShutdown;

    @Bean
    public DataSource quartzDataSource() {
        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName(dataSourceDriverClassName)
                .url(dataSourceUrl)
                .username(dataSourceUsername)
                .password(dataSourcePassword)
                .build();
        return dataSource;
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("quartzDataSource") DataSource quartzDataSource) {
        SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
        factoryBean.setDataSource(quartzDataSource);
        factoryBean.setQuartzProperties(quartzProperties());
        return factoryBean;
    }

    @Bean
    public Scheduler scheduler(@Qualifier("schedulerFactoryBean") SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        scheduler.start();
        return scheduler;
    }

    private Properties quartzProperties() {
        Properties properties = new Properties();
        properties.setProperty("org.quartz.jobStore.dataSource", "quartzDataSource");
        properties.setProperty("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
        properties.setProperty("org.quartz.jobStore.tablePrefix", "QRTZ_");
        properties.setProperty("org.quartz.threadPool.threadCount", "25");
        properties.setProperty("org.quartz.threadPool.threadPriority", "5");
        return properties;
    }
}

Here, most of the configuration is fixed, but there may be occasional changes with Quartz updates. Please refer to the official documentation for accuracy.

The End

In conclusion, Quartz is a powerful tool for scheduling tasks in Spring Boot, offering both basic and advanced features to suit your needs. Whether you’re scheduling simple periodic tasks or complex, dynamic workflows, Quartz can help you achieve reliable and efficient job scheduling.

Be a Neutral Listener, Dialectical Thinker, and Practitioner of Knowledge