Implementing Scheduler with QuartzScheduler with SpringBoot

Seonggil Jeong
5 min readNov 26, 2022

--

java : 11
springBoot : 2.7.5

Part 1 : Learn about the Quartz Scheduler
Part 2 :
Implementing Scheduler with Quartz Scheduler with SpringBoot
part 3 :
Implement sending alerts at specific times per user with QuartzScheduler

In this post, I will implement the Quartz Scheduler setting and simple examples

See Part 1 for the theoretical part.

After creating the project, set the dependency and .yml files first

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-quartz'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
}

I chose to store JOB information in DB(H2) and using JPA

server:
port: 8080

spring:
application:
name: Quartz-Sample

## DataBase
h2:
console:
enabled: true
path: /h2
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test;MODE=MySQL
username: sa
password:

jpa:
hibernate:
ddl-auto: create-drop
open-in-view: false
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
naming:
physical-strategy = org.hibernate.model.naming.PhysicalNamingStrategyStandardImpl
database: mysql
generate-ddl: true

quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always
scheduler-name: "sample"

# Logging Info
logging:
level:
org.springframework: info

And below the resources folder, create quartz.properties

See QuartzDoc for additional configuration information and explanations

org.quartz.scheduler.instanceName=sample
org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=3
org.quartz.context.key.QuartzTopic=QuartzProperties
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true

org.quartz.jobStore.dataSource=sample

org.quartz.dataSource.sample.provider=hikaricp
org.quartz.dataSource.sample.URL = jdbc:h2:mem:test;MODE=MySQL
org.quartz.dataSource.sample.driver = org.h2.Driver
serverTimezone=UTC&characterEncoding=UTF-8
org.quartz.dataSource.sample.user = sa

I don’t know if you’ve experienced it,
can’t do DI(Dependency Injection) on Job Class
However, since many functions are executed through DI(Dependency Injection), we will create AutoWiringSpringBeanJobFactory.class for helping Job.class

public class AutoWiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;

@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
beanFactory = applicationContext.getAutowireCapableBeanFactory();
}

@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);

return job;
}
}

After that, we will implement the Listeners described in Part 1
JobListener can receive events before and after Job execution,
TriggerListener can receive events before and after Trigger execution

@Slf4j
public class JobsListener implements JobListener {

@Override
public String getName() {
return "JobsListener";
}

@Override
public void jobToBeExecuted(JobExecutionContext context) {
log.info("Before Start Job");
JobKey jobKey = context.getJobDetail().getKey();


log.info("jobKey : {}", jobKey);

}

@Override
public void jobExecutionVetoed(JobExecutionContext context) {
log.info("Operation aborted");
JobKey jobKey = context.getJobDetail().getKey();


log.info("jobKey : {}", jobKey);

}

@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
log.info("Job Was Executed");
JobKey jobKey = context.getJobDetail().getKey();


log.info("jobKey : {}", jobKey);

}
}
@Slf4j
public class TriggersListener implements TriggerListener {

@Override
public String getName() {
return "TriggersListener";
}

@Override
public void triggerFired(Trigger trigger, JobExecutionContext context) {
log.info("Trigger is Starting");
final JobKey jobKey = trigger.getJobKey();

log.info("triggerFired at {} :: jobKey : {}", trigger.getStartTime(), jobKey);

}

/**
* @Content : if this return value is true,
* request to JobListener jobExecutionVetoed()
*/
@Override
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
log.info("check Trigger health");

return false;
}

@Override
public void triggerMisfired(Trigger trigger) {
log.info("Trigger Misfired");
final JobKey jobKey = trigger.getJobKey();

log.info("triggerMisfired at {} :: jobKey : {}", trigger.getStartTime(), jobKey);

}

@Override
public void triggerComplete(Trigger trigger, JobExecutionContext context,
Trigger.CompletedExecutionInstruction triggerInstructionCode) {
log.info("Trigger Complete");
final JobKey jobKey = trigger.getJobKey();

log.info("triggerMisfired at {} :: jobKey : {}", trigger.getStartTime(), jobKey);

}
}

Now use the setting information that you write before

@Slf4j
@Configuration
@RequiredArgsConstructor
public class QuartzConfig {
private final DataSource dataSource;
private final ApplicationContext applicationContext;
private final PlatformTransactionManager platformTransactionManager;

@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
AutoWiringSpringBeanJobFactory autoWiringSpringBeanJobFactory = new AutoWiringSpringBeanJobFactory();

autoWiringSpringBeanJobFactory.setApplicationContext(applicationContext);
schedulerFactoryBean.setJobFactory(autoWiringSpringBeanJobFactory);
schedulerFactoryBean.setDataSource(dataSource);
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setAutoStartup(true);
schedulerFactoryBean.setTransactionManager(platformTransactionManager);
schedulerFactoryBean.setQuartzProperties(quartzProperties());
return schedulerFactoryBean;
}

private Properties quartzProperties() {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("quartz.properties"));
Properties properties = null;

try {
propertiesFactoryBean.afterPropertiesSet();
properties = propertiesFactoryBean.getObject();
} catch (IOException e) {
e.printStackTrace();
}
return properties;
}

}

Now let’s implement actions that are executed through triggers
Make sure to use @Autowired because @RequiredArgConstructor is not working in Job Class

/**
* an interface to be implemented by components that you wish to have executed by the scheduler
*/
@Slf4j
@Component
@PersistJobDataAfterExecution // to change the job data map when a job runs
@DisallowConcurrentExecution
public class SampleJob implements Job {

@Autowired
private TestRepository testRepository;


@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
System.out.println(this.getClass().getName() + "Sample Job Start! [ " + LocalDate.now() + " ]");


}
}

When this Job is called by the Trigger, the contents of the execute method are executed

I will manage Job creation and Trigger creation in QuartzHandler

/**
* Managing Trigger and Job Creation
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class QuartzHandler {
private final Scheduler scheduler;

@PostConstruct
public void init() {
try {
// Initialization (DB, Scheduler)
scheduler.clear();

// set JobListener
scheduler.getListenerManager()
.addJobListener(new JobsListener());

// set TriggerListener
scheduler.getListenerManager()
.addTriggerListener(new TriggersListener());


} catch (Exception e) {
e.printStackTrace();
}
}

// add Job
public <T extends Job> void addJob(
Class<? extends Job> job, final String name, String description, Map paramsMap, String cron)
throws SchedulerException {

JobDetail jobDetail = buildJobDetail(job, name, description, paramsMap);
Trigger trigger = buildCronTrigger(cron);

if (scheduler.checkExists(jobDetail.getKey())) { // if already job -> change
scheduler.deleteJob(jobDetail.getKey());
scheduler.scheduleJob(jobDetail, trigger);

} else {
scheduler.scheduleJob(jobDetail, trigger);
}
}

// create JobDetail
public <T extends Job> JobDetail buildJobDetail(
Class<? extends Job> job,final String jobName, final String jobDescription, Map paramsMap) {

JobDataMap jobDataMap = new JobDataMap();
jobDataMap.putAll(paramsMap);

return JobBuilder.newJob(job)
.withIdentity(jobName)
.withDescription(jobDescription)
.usingJobData(jobDataMap)
.build();
}

// create CronTrigger
private Trigger buildCronTrigger(String cronExp) {
CronTriggerFactoryBean cronTriggerFactory = new CronTriggerFactoryBean();
cronTriggerFactory.setCronExpression(cronExp);
cronTriggerFactory.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
try {
cronTriggerFactory.afterPropertiesSet();
} catch (ParseException e) {
e.printStackTrace();

}
return cronTriggerFactory.getObject();

}
}

Now create a job with Request
ex.) localhost:8080/jobs/10

@Slf4j
@RestController
@RequiredArgsConstructor
public class QuartzController {
private final QuartzHandler quartzHandler;


@RequestMapping("/jobs/{second}")
public void createJobSample(@PathVariable final String second) throws SchedulerException {
log.info("[ {} ]--------------------- creatJob Start ! ", LocalTime.now());
final String cron = second + " * * * * ?";

Map<String, Object> map = new HashMap<>();
map.put("name", this.getClass().getName());

quartzHandler.addJob(SampleJob.class, "jobName", "SampleDescription", map, cron);

}
}
Log Result

You can see that both Listener and Job are functioning normally,
also find this Trigger information in the DB we set up

H2 Database

In the next article, I will implement a service that executes the Job 5 minutes before the time stored in the DB, which is the final goal

you can see code in my gitHub

--

--