spring quartz scheduler web application example

Let’s create an application which can be used to schedule any kinds of jobs with spring boot. In this blog, we will be using a famous open source java library for scheduling named Quartz Scheduler.


As an example, we will be creating an email scheduling application to schedule emails. But remember, it can be used to schedule any kind of Jobs. 


And also we will explore all of the CRUD operations involving quartz-scheduler.


Let's get started.




First Step


Let’s create the application. 


Head out to Spring Initializr.


URL - https://start.spring.io/


Fill out the blanks required and add below dependencies. 


spring initializr tutorial



After that, click on Generate and download the project zip to your workspace. Now, unzip the project and import the project in your favourite IDE and start working.


So, now we have a starter spring boot project with some dependencies. Soon, as we start working, we will add other dependencies as well. 




Dependencies 


We need quartz dependency. So, let's add it in the pom file. 


<!--Quartz Dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--Mail Dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

Let's also add the mail dependency to our pom file. 



Make sure to explore the dependencies added and its purpose. 




Spring profiles


For each environment, we will have different profile and different properties. So, as a first step in our project, we will set up the spring profiles. 

 

In src > main > resources,  there is already an application.properties file. 


The properties which will be common across all environments will be written here. 


Now, In the same location let’s create a file with name application-local.properties.   


Let’s use application-{env}.properties as convention for different environments. 


When the application is deployed, the application need to pick the properties file according to the environment. So, at the top of the  QuartzSchedulerApplication

 class, add this configuration annotation.


@Configuration("classpath:application-" + "${spring.profiles.active}" + ".properties")



spring profiles active in spring boot


If no profile is mentioned, the default profile will be default. 



Configuring Mysql Database  and Quartz Scheduler


One of the many advantages of quartz scheduler is to persist jobs. 

What does that mean? 


If a failure occurs and application stops. When we restart the application, the previously scheduled jobs data is not lost as it persists the job information. So, when the application comes back, the missed jobs can run again.


Lets install the mysql database first. Before that, install the docker. 


Developer tip - Just install docker and whenever you may need anything like mysql, Postgres, monogdb etc, pull the respective docker image. No hassle of installing and configuring everything. 


Just download the image and run the container. If you are new to the world of docker, I recommend you to spend some time on it. It is super useful.


Docker is out of scope for this blog but there are lot of good resources out there. 


Refer this doc for installation - https://docs.docker.com/engine/install/




Pulling mysql image


docker pull mysql

Verify the image with below command


docker images




Running the mysql container 


 docker run  -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=root -d   mysql:5.7                                                            


The above command will run the container in detached mode. In laymen terms, it will be running in background. It will bind to 3306 on our local system.  So, our mysql is running on port 3306. 



Accessing the container


Execute the below commands to go into the container and from there we will access the mysql console. 



docker ps

docker exec -it containerId


You will logon to bash terminal of the container. You can login to mysql console by typing below command.



mysql -proot


Remember, by default user root is created and we gave the password as root. 


If you have Mysql workbench, connect to it with username and password as root and host and port as localhost and 3306 respectively. 




Quartz tables


Quartz Scheduler has its own schema. It has its own tables which needs to be created along with our application tables. 


I have included all the tables in file quartz_scheduler/scripts/mysql_tables.sql


We will copy this sql script to docker container, so you don’t have to copy and paste the queries on sql console. It is not  very elegant to copy the scripts as text and paste on the mysql console as it is bound to formatting errors.  



Elegant way


Open another terminal and type below commands in the bash shell



cd quartz_scheduler/scripts/;

docker cp mysql_tables.sql containerId:/


The last command will copy the sql script to root location of the container i.e /




Creating the tables


So, lets create a database named quartz_scheduler. 


create database quartz_scheduler;

use quartz_scheduler;

source mysql_tables.sql;

 

Now, the quartz tables are created and also one table related to our application, let’s move on to developing the application. 



Application requirements


Before moving any further, let's discuss about the requirements,  create the API contracts. 


The requirements are really simple

  1. The user should be able to create a schedule to send an email at specific time on a specific date. 
  2. The user should be able to view all the schedules created and active and also individual schedule by schedule Id. 
  3. The user should be able to update the schedule like mail content and schedule time.
  4. The user should be able to delete the schedule as well. 
  5. Version one will contain only single fire schedules. No recurring ones. This means, schedules fired will be deleted after completing the job. But schedules can be updated as long as they are not fired.


Simple Just four APIs.  Let’s not complicate it any further. Just the CRUD operations. 





Mail Schedule Table


schedule_id BIGINT AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
to_email VARCHAR(100) NOT NULL,
schedule_datetime VARCHAR(200) NOT NULL,
schedule_zoneId VARCHAR(200) NOT NULL,
is_deleted TINYINT(1) NOT NULL,


We will discuss more about the table in the APIs section. 

Here, username is used to identify a user in the application. A user will have control on schedules created by him only. And rest of the fields are self explanatory.




Quartz Scheduler Terminology


Before starting the development, actually we should know a bit about quartz scheduler terminology. This will very helpful while development. 


The key components are

  1. Scheduler - It is the main interface for interacting with the job scheduler. Jobs and Triggers are registered with the Scheduler. 
  2. Job - It is the interface to be implemented by the classes that represent the Job in Quartz and it has a single method executeInternal() where the work needs to be done will be written.
  3. Job Detail - It represents instance of a job. It stores the type of job needs to be executed. Additional information is stored in job data map. It is identified by a job key which consists of a name and group. Job builder is used to construct job detail entities. 
  4. Trigger - A trigger is a mechanism to schedule a job. It is also identified by a trigger key which consists of a name and group. TriggerBuilder is used to construct trigger entities. 


More information about quartz can be found here.



Let’s Begin the development




## Spring Datasource Properties
spring.datasource.username = root
spring.datasource.password = root
spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql = true
## MySQL Properties
spring.datasource.url = jdbc:mysql://localhost:3306/quartz_scheduler?useSSL=false
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
## Quartz Properties
spring.quartz.properties.org.quartz.jobStore.dataSource = quartzDataSource
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.driver = com.mysql.cj.jdbc.Driver
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.provider=hikaricp
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.URL = jdbc:mysql://localhost:3306/quartz_scheduler?useSSL=false
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.user = root
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.password = root
spring.quartz.job-store-type = jdbc
spring.quartz.properties.org.quartz.threadPool.threadCount = 5
## Mail Properties
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=<your-mail@gmail.com>
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

Provide spring.datasource properties and quartz properties as per mysql installation. 


Here I am using hikaricp  for my connection pool of 5 threads for quartz. 

Springboot default connection pool is hikaricp and default size is 10 for your information.  


Now you can run the application with the given profile using the property 


-Dspring.profiles.active=local




Writing the APIs


Lets create a folder named controller at path - quartz-scheduler/src/main/java/com/pranay/quartz/ 


Inside the controller folder, let’s create a class with name AppController.  It will contain all the APIs. 


Similarly, let’s create a folder named service at the same path as controller.  It contains the business logic. 


Let’s create a Dao layer where we will have just the DB operations. Nothing more. Just the code interacting with DB and exception handling in case of DB failures.


We will Java Persistence API  to abstract out the complexity of writing the database queries and playing with data directly. 



The flow is from controller to service to Dao layer and from there it is reverse.  We will ignore the filters for this post. It will come become controller.





Adding the swagger


Swagger is a set of open-source tools built around the OpenAPI Specification that can help you design, build, document and consume REST APIs.



<!--Swagger dependencies-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${io.springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${io.springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-data-rest</artifactId>
<version>${io.springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<version>${io.springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${io.springfox.version}</version>
</dependency>

The below swagger config lets us filter out the controllers to be exposed.



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
@Profile({"local","local-mysql","default","local-postgres"})
public class ApplicationSwaggerConfig {
@Bean
public Docket employeeApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.ant("/quartz/**"))
.build();
}
}
view raw Swagger-config.java hosted with ❀ by GitHub



Let's create the Rest APIs to schedule the email jobs via gmail in Quartz Scheduler. 

Also, previously mentioned mail schedule table is for our convenience to keep track of which schedules are created and which are active. 




Create the Mail Schedule 


First, let's define the request to create a mail schedule . Let's see what properties we need to create a Schedule.

 

package com.pranay.quartz.models.request;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.pranay.quartz.models.entity.MailSchedule;
import lombok.Data;
import lombok.ToString;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.time.ZoneId;
@Data
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class Request {
private Integer scheduleId;
@NotNull(message = "username cannot be null")
private String username;
@Email(message = "Provided email format is not valid")
@NotNull(message = "toEmail cannot be null")
private String toEmail;
@NotNull(message = "subject cannot be null")
private String subject;
@NotNull(message = "message cannot be null")
private String message;
@NotNull(message = "scheduledTime cannot be null")
private LocalDateTime scheduledTime;
@NotNull(message = "zoneId cannot be null")
private ZoneId zoneId;
public MailSchedule toMailSchedule() {
MailSchedule mailSchedule = new MailSchedule();
mailSchedule.setDeleted(false);
mailSchedule.setScheduleId(this.scheduleId);
mailSchedule.setUsername(this.username);
mailSchedule.setToEmail(this.toEmail);
mailSchedule.setScheduleDateTime(this.scheduledTime.toString());
mailSchedule.setScheduleZoneId(this.zoneId.toString());
return mailSchedule;
}
}
view raw Schedule-request.java hosted with ❀ by GitHub
public String createSchedule(Request request) {
ZonedDateTime zonedDateTime = ZonedDateTime.of(request.getScheduledTime(), request.getZoneId());
if (zonedDateTime.isBefore(ZonedDateTime.now())) {
throw new BadRequestException("Scheduled Time should be greater than current time");
}
return mailScheduleDao.createSchedule(request, zonedDateTime);
}
@Component
public class MailScheduleDaoImpl implements MailScheduleDao {
@Autowired
private Scheduler scheduler;
@Autowired
private MailScheduleRepository mailScheduleRepository;
@Override
@Transactional
public String createSchedule(Request request, ZonedDateTime zonedDateTime) {
String scheduleId = saveSchedule(request);
JobDetail jobDetail = getJobDetail(scheduleId, request);
Trigger simpleTrigger = getSimpleTrigger(jobDetail, zonedDateTime);
scheduleJob(jobDetail, simpleTrigger);
return scheduleId;
}
public String saveSchedule(Request request) {
try {
MailSchedule save = mailScheduleRepository.save(request.toMailSchedule());
return save.getScheduleId().toString();
} catch (Exception e) {
throw new InternalServerException("Unable to save schedule to DB");
}
}
public JobDetail getJobDetail(String scheduleId, Request request) {
Integer jobId = Integer.valueOf(scheduleId);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(TO_MAIL, request.getToEmail());
jobDataMap.put(SUBJECT, request.getSubject());
jobDataMap.put(MESSAGE, request.getMessage());
jobDataMap.put(SCHEDULE_ID, scheduleId);
return JobBuilder.newJob(MailScheduleJob.class)
.withIdentity(String.valueOf(jobId), JOB_DETAIL_GROUP_ID)
.withDescription(JOB_DETAIL_DESCRIPTION)
.usingJobData(jobDataMap)
.storeDurably()
.build();
}
public Trigger getSimpleTrigger(JobDetail jobDetail, ZonedDateTime zonedDateTime) {
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(jobDetail.getKey().getName(), TRIGGER_GROUP_ID)
.withDescription(TRIGGER_DESCRIPTION)
.startAt(Date.from(zonedDateTime.toInstant()))
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow())
.build();
}
public Trigger getCronTrigger(JobDetail jobDetail, String cronExpression) {
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(jobDetail.getKey().getName(), TRIGGER_GROUP_ID)
.withDescription(TRIGGER_DESCRIPTION)
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression).
withMisfireHandlingInstructionFireAndProceed().inTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)))
.build();
}
public void scheduleJob(JobDetail jobDetail, Trigger trigger) {
try {
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException schedulerException) {
throw new InternalServerException("Error creating the schedule");
}
}
}


 


List all the mail schedules and get schedule by Id


// Get all schedules from our reference table
public List<Response> getSchedules(String username, int page, int size) {
List<Response> responses = new ArrayList<>();
try {
Pageable pageable = PageRequest.of(page, size);
Page<MailSchedule> schedules = mailScheduleRepository.findByUsernameAndIsDeletedFalse(username, pageable);
List<MailSchedule> mailSchedules = schedules.getContent();
responses = AppUtils.getResponseDtoListFrom(mailSchedules);
} catch (Exception e) {
throw new InternalServerException("Unable to fetch all schedules from DB");
}
return responses;
}
// Get a schedule by id - returns only active schedule.
public Response getSchedule(int id, String username) {
Response response = mailScheduleDao.getSchedule(id, username);
if (response == null) {
throw new BadRequestException(String.format("No Active Schedule exists with id : %s", id));
}
return response;
}
public Response getSchedule(int scheduleId, String username) {
Response response = null;
try {
Optional<MailSchedule> optional = mailScheduleRepository.findByScheduleIdAndIsDeletedFalse(scheduleId);
if (optional.isPresent()) {
response = optional.get().toResponse();
}
} catch (Exception e) {
throw new InternalServerException("Error fetching the schedule");
}
return response;
}





Update the Mail Schedule



public String updateSchedule(Request request, ZonedDateTime zonedDateTime) {
updateMailSchedule(request);
JobDetail jobDetail = updateJobDetail(request);
updateTriggerDetails(request, jobDetail, zonedDateTime);
return null;
}
// Updates our reference table
public void updateMailSchedule(Request request) {
try {
Optional<MailSchedule> optional = mailScheduleRepository.findByScheduleIdAndIsDeletedFalse(request.getScheduleId());
if (optional.isPresent()) {
MailSchedule mailSchedule = optional.get();
mailSchedule.setUsername(request.getUsername());
mailSchedule.setToEmail(request.getToEmail());
mailSchedule.setScheduleDateTime(request.getScheduledTime().toString());
mailSchedule.setScheduleZoneId(request.getZoneId().toString());
mailScheduleRepository.save(mailSchedule);
}
} catch (Exception e) {
throw new InternalServerException("Unable to update the schedule in DB");
}
}
// updates our quartz tables -
public JobDetail updateJobDetail(Request request) {
JobDetail jobDetail = null;
try {
if (request.getScheduleId() != null) {
jobDetail = scheduler.getJobDetail(new JobKey(request.getScheduleId().toString(), JOB_DETAIL_GROUP_ID));
jobDetail.getJobDataMap().put(TO_MAIL, request.getToEmail());
jobDetail.getJobDataMap().put(SUBJECT, request.getSubject());
jobDetail.getJobDataMap().put(MESSAGE, request.getMessage());
scheduler.addJob(jobDetail, true);
}
} catch (SchedulerException schedulerException) {
throw new InternalServerException("Unable to update the job data map");
}
return jobDetail;
}
public void updateTriggerDetails(Request request, JobDetail jobDetail, ZonedDateTime zonedDateTime) {
Trigger newTrigger = getSimpleTrigger(jobDetail, zonedDateTime);
try {
scheduler.rescheduleJob(new TriggerKey(request.getScheduleId().toString(), TRIGGER_GROUP_ID), newTrigger);
} catch (SchedulerException schedulerException) {
throw new InternalServerException("Unable to update the trigger in DB");
}
}



Delete the Schedule


public void deleteSchedule(int scheduleId, String username) {
deleteMailSchedule(scheduleId);
deleteJobAndTrigger(scheduleId);
}
public void deleteMailSchedule(Integer scheduleId) {
try {
Optional<MailSchedule> optional = mailScheduleRepository.findById(scheduleId);
if (optional.isPresent()) {
MailSchedule mailSchedule = optional.get();
mailSchedule.setDeleted(true);
mailScheduleRepository.save(mailSchedule);
}
} catch (Exception e) {
throw new InternalServerException("Error deleting schedule");
}
}
public void deleteJobAndTrigger(Integer scheduleId) {
try {
scheduler.unscheduleJob(new TriggerKey(scheduleId.toString(), TRIGGER_GROUP_ID));
scheduler.deleteJob(new JobKey(scheduleId.toString(), JOB_DETAIL_GROUP_ID));
} catch (SchedulerException schedulerException) {
throw new InternalServerException("Unable to delete the job from scheduler");
}
}



Mail Schedule Job


// Mail Service to send the mails.
@Component
public class MailServiceImpl implements MailService {
@Autowired
private JavaMailSender javaMailSender;
public void sendMail(String fromEmail, String toEmail, String subject, String message) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, StandardCharsets.UTF_8.toString());
messageHelper.setSubject(subject);
messageHelper.setText(message, true);
messageHelper.setFrom(fromEmail);
messageHelper.setTo(toEmail);
javaMailSender.send(mimeMessage);
} catch (MessagingException ex) {
throw new InternalServerException("Error Sending mail");
}
}
}
// MailScheduleJob.java - at the specified time, the quartz schedule gets triggered and it calls this job via executeInternal
// method. After the job is completed, the data in quartz tables gets deleted for one time jobs. So, we need to mark the job
// as deleted in our reference table as well.
public class MailScheduleJob extends QuartzJobBean {
@Autowired
private MailService mailService;
@Value("${spring.mail.username}")
private String from;
@Autowired
MailScheduleDao mailScheduleDao;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
String subject = jobDataMap.getString(SUBJECT);
String message = jobDataMap.getString(MESSAGE);
String toMail = jobDataMap.getString(TO_MAIL);
Integer scheduleId = jobDataMap.getInt(SCHEDULE_ID);
mailService.sendMail(from, toMail, subject, message);
mailScheduleDao.deleteMailSchedule(scheduleId);
}
}
view raw mail-sender-job.java hosted with ❀ by GitHub




Enabling Gmail's SMTP access


ScreenBefore getting started with testing our application, we need to enable SMTP access in gmail to allow our application to send emails using Gmail. It is disabled by default.




Running our application 


mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=<env> --spring.mail.password=<YOUR_SMTP_PASSWORD>"                         


For this blog, you can set env=local and  provide your password. You can generate an app password for here.




Testing from swagger


Once the application is up, we can now start creating the mail schedules. 

Type the below URL in your browser. 

URL -  http://localhost:8080/swagger-ui/index.html


You should see the swagger UI like below. 


spring boot swagger example value



Let's create a schedule.


Request

spring quartz dynamic job scheduling


Response

 

quartz scheduler spring boot swagger


Got the mail at specified time


Gmail scheduling with springboot quartz



Update the schedule. 


Create another schedule with same content. later update via update API from swagger. 


spring boot gmail smtp example


Deleting the schedule


spring boot send email with attachment


spring boot starter mail


List the schedule


spring boot mail gmail


Mail Schedule quartz scheduler




Postman Collection


The postman collection of the above APIs can be found here -  Postman Collection




Github Repo


The above code can be found below 


Link - https://github.com/pranaybathini/quartz-scheduler




Custom Email Templates





Keep Experimenting πŸ”Ž 

Keep Learning πŸš€



Post a Comment