Spring Boot Quartz Scheduler Example Building an Email Scheduling App

Quartz is an open source Java library for scheduling Jobs. It has a very rich set of features including but not limited to persistent Jobs, transactions, and clustering.

You can schedule Jobs to be executed at a certain time of day, or periodically at a certain interval, and much more. Quartz provides a fluent API for creating jobs and scheduling them.

Quartz Jobs can be persisted into a database, or a cache, or in-memory.

In this article, you’ll learn how to schedule Jobs in spring boot using Quartz Scheduler by building a simple Email Scheduling application. The application will have a Rest API that allows clients to schedule Emails at a later time.

We’ll use MySQL to persist all the jobs and other job-related data.

Creating the Application

Let’s bootstrap the application using Spring Boot CLI. Open your terminal and type the following command –

spring init -d=web,jpa,mysql,quartz,mail -n=quartz-demo quartz-demo

The above command will generate the project with all the specified dependencies in a folder named quartz-demo.

Note that, you can also use Spring Initializr web tool to bootstrap the project by following the instructions below –

  • Open http://start.spring.io
  • Enter quartz-demo in the Artifact field.
  • Add WebJPAMySQLQuartz, and Mail in the dependencies section.
  • Click Generate to generate and download the project.

That’s it! You may now import the project into your favorite IDE and start working.

Directory Structure

Following is the directory structure of the complete application for your reference. We’ll create all the required folders and classes one-by-one in this article –

Spring Boot Quartz Scheduler Email Scheduling App directory structure

Configuring MySQL database, Quartz Scheduler, and Mail Sender

Let’s configure Quartz Scheduler, MySQL database, and Spring Mail. MySQL database will be used for storing Quartz Jobs, and Spring Mail will be used to send emails.

Open src/main/resources/application.properties file and add the following properties –

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/quartz_demo?useSSL=false
spring.datasource.username = root
spring.datasource.password = password

## QuartzProperties
spring.quartz.job-store-type = jdbc
spring.quartz.properties.org.quartz.threadPool.threadCount = 5

## MailProperties
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=testme@gmail.com
spring.mail.password=

spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

You’ll need to create a MySQL database named quartz_demo. Also, don’t forget to change the spring.datasource.username and spring.datasource.password properties as per your MySQL installation.

We’ll be using Gmail’s SMTP server for sending emails. Please add your password in the spring.mail.password property. You may also pass this property at runtime as command line argument or set it in the environment variable.

Note that, Gmail’s SMTP access is disabled by default. To allow this app to send emails using your Gmail account –

All the quartz specific properties are prefixed with spring.quartz. You can refer to the complete set of configurations supported by Quartz in its official documentation. To directly set configurations for Quartz scheduler, you can use the format spring.quartz.properties.<quartz_configuration_name>=<value>.

Creating Quartz Tables

Since we have configured Quartz to store Jobs in the database, we’ll need to create the tables that Quartz uses to store Jobs and other job-related meta-data.

Please download the following SQL script and run it in your MySQL database to create all the Quartz specific tables.

After downloading the above SQL script, login to MySQL and run the script like this –

mysql> source <PATH_TO_QUARTZ_TABLES.sql> 

Overview of Quartz Scheduler’s APIs and Terminologies

1. Scheduler

The Primary API for scheduling, unscheduling, adding, and removing Jobs.

2. Job

The interface to be implemented by classes that represent a ‘job’ in Quartz. It has a single method called execute() where you write the work that needs to be performed by the Job.

3. JobDetail

A JobDetail represents an instance of a Job. It also contains additional data in the form of a JobDataMap that is passed to the Job when it is executed.

Every JobDetail is identified by a JobKey that consists of a name and a group. The name must be unique within a group.

4. Trigger

A Trigger, as the name suggests, defines the schedule at which a given Job will be executed. A Job can have many Triggers, but a Trigger can only be associated with one Job.

Every Trigger is identified by a TriggerKey that comprises of a name and a group. The name must be unique within a group.

Just like JobDetails, Triggers can also send parameters/data to the Job.

5. JobBuilder

JobBuilder is a fluent builder-style API to construct JobDetail instances.

6. TriggerBuilder

TriggerBuilder is used to instantiate Triggers.

Creating a REST API to schedule Email Jobs dynamically in Quartz

All right! Let’s now create a REST API to schedule email Jobs in Quartz dynamically. All the Jobs will be persisted in the database and executed at the specified schedule.

Before writing the API, Let’s create the DTO classes that will be used as request and response payloads for the scheduleEmail API –

ScheduleEmailRequest

package com.example.quartzdemo.payload;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class ScheduleEmailRequest {
    @Email
    @NotEmpty
    private String email;

    @NotEmpty
    private String subject;

    @NotEmpty
    private String body;

    @NotNull
    private LocalDateTime dateTime;

    @NotNull
    private ZoneId timeZone;
	
	// Getters and Setters (Omitted for brevity)
}

ScheduleEmailResponse

package com.example.quartzdemo.payload;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ScheduleEmailResponse {
    private boolean success;
    private String jobId;
    private String jobGroup;
    private String message;

    public ScheduleEmailResponse(boolean success, String message) {
        this.success = success;
        this.message = message;
    }

    public ScheduleEmailResponse(boolean success, String jobId, String jobGroup, String message) {
        this.success = success;
        this.jobId = jobId;
        this.jobGroup = jobGroup;
        this.message = message;
    }

    // Getters and Setters (Omitted for brevity)
}

ScheduleEmail Rest API

The following controller defines the /scheduleEmail REST API that schedules email Jobs in Quartz –

package com.example.quartzdemo.controller;

import com.example.quartzdemo.job.EmailJob;
import com.example.quartzdemo.payload.ScheduleEmailRequest;
import com.example.quartzdemo.payload.ScheduleEmailResponse;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.UUID;

@RestController
public class EmailJobSchedulerController {
    private static final Logger logger = LoggerFactory.getLogger(EmailJobSchedulerController.class);

    @Autowired
    private Scheduler scheduler;

    @PostMapping("/scheduleEmail")
    public ResponseEntity<ScheduleEmailResponse> scheduleEmail(@Valid @RequestBody ScheduleEmailRequest scheduleEmailRequest) {
        try {
            ZonedDateTime dateTime = ZonedDateTime.of(scheduleEmailRequest.getDateTime(), scheduleEmailRequest.getTimeZone());
            if(dateTime.isBefore(ZonedDateTime.now())) {
                ScheduleEmailResponse scheduleEmailResponse = new ScheduleEmailResponse(false,
                        "dateTime must be after current time");
                return ResponseEntity.badRequest().body(scheduleEmailResponse);
            }

            JobDetail jobDetail = buildJobDetail(scheduleEmailRequest);
            Trigger trigger = buildJobTrigger(jobDetail, dateTime);
            scheduler.scheduleJob(jobDetail, trigger);

            ScheduleEmailResponse scheduleEmailResponse = new ScheduleEmailResponse(true,
                    jobDetail.getKey().getName(), jobDetail.getKey().getGroup(), "Email Scheduled Successfully!");
            return ResponseEntity.ok(scheduleEmailResponse);
        } catch (SchedulerException ex) {
            logger.error("Error scheduling email", ex);

            ScheduleEmailResponse scheduleEmailResponse = new ScheduleEmailResponse(false,
                    "Error scheduling email. Please try later!");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(scheduleEmailResponse);
        }
    }

    private JobDetail buildJobDetail(ScheduleEmailRequest scheduleEmailRequest) {
        JobDataMap jobDataMap = new JobDataMap();

        jobDataMap.put("email", scheduleEmailRequest.getEmail());
        jobDataMap.put("subject", scheduleEmailRequest.getSubject());
        jobDataMap.put("body", scheduleEmailRequest.getBody());

        return JobBuilder.newJob(EmailJob.class)
                .withIdentity(UUID.randomUUID().toString(), "email-jobs")
                .withDescription("Send Email Job")
                .usingJobData(jobDataMap)
                .storeDurably()
                .build();
    }

    private Trigger buildJobTrigger(JobDetail jobDetail, ZonedDateTime startAt) {
        return TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(jobDetail.getKey().getName(), "email-triggers")
                .withDescription("Send Email Trigger")
                .startAt(Date.from(startAt.toInstant()))
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow())
                .build();
    }
}

Spring Boot has built-in support for Quartz. It automatically creates a Quartz Scheduler bean with the configuration that we supplied in the application.properties file. That’s why we could directly inject the Scheduler in the controller.

In the /scheduleEmail API,

  • We first validate the request body
  • Then, Build a JobDetail instance with a JobDataMap that contains the recipient email, subject, and body. The JobDetail that we create is of type EmailJob. We’ll define EmailJob in the next section.
  • Next, we Build a Trigger instance that defines when the Job should be executed.
  • Finally, we schedule the Job using scheduler.scheduleJob() API.

Creating the Quartz Job to sends emails

Let’s now define the Job that sends the actual emails. Spring Boot provides a wrapper around Quartz Scheduler’s Job interface called QuartzJobBean. This allows you to create Quartz Jobs as Spring beans where you can autowire other beans.

Let’s create our EmailJob by extending QuartzJobBean –

package com.example.quartzdemo.job;

import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.nio.charset.StandardCharsets;

@Component
public class EmailJob extends QuartzJobBean {
    private static final Logger logger = LoggerFactory.getLogger(EmailJob.class);

    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private MailProperties mailProperties;
    
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        logger.info("Executing Job with key {}", jobExecutionContext.getJobDetail().getKey());

        JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
        String subject = jobDataMap.getString("subject");
        String body = jobDataMap.getString("body");
        String recipientEmail = jobDataMap.getString("email");

        sendMail(mailProperties.getUsername(), recipientEmail, subject, body);
    }

    private void sendMail(String fromEmail, String toEmail, String subject, String body) {
        try {
            logger.info("Sending Email to {}", toEmail);
            MimeMessage message = mailSender.createMimeMessage();

            MimeMessageHelper messageHelper = new MimeMessageHelper(message, StandardCharsets.UTF_8.toString());
            messageHelper.setSubject(subject);
            messageHelper.setText(body, true);
            messageHelper.setFrom(fromEmail);
            messageHelper.setTo(toEmail);

            mailSender.send(message);
        } catch (MessagingException ex) {
            logger.error("Failed to send email to {}", toEmail);
        }
    }
}

Running the Application and Testing the API

It’s time to run the application and watch the live action. Open your terminal, go to the root directory of the project and type the following command to run it –

mvn spring-boot:run -Dspring.mail.password=<YOUR_SMTP_PASSWORD>

You don’t need to pass the spring.mail.password command line argument if you have already set the password in the application.properties file.

The application will start on port 8080 by default. Let’s now schedule an email using the /scheduleEmail API –

Spring Boot Quartz Scheduler Email Job Scheduler API

And, Here I get the email at the specified time 🙂

Spring Boot Quartz Scheduler Dynamic Email Job Scheduler API Example

Conclusion

That’s all folks! I hope you enjoyed the article. You can find the complete source code of the project in the Github Repository. Consider giving the project a star on Github if you find it useful.

References

Sounds interesting? Let’s start…