Mastering Quartz Scheduler in Spring Boot: From Basics to Advanced Integration
This article introduces Quartz, a Java job‑scheduling library, explains its core components (Job, Trigger, Scheduler), provides step‑by‑step Maven demos, shows how to integrate it with Spring Boot, configure persistence, manage concurrency, and handle advanced features like cron expressions and calendar exclusions.
Preface
Quartz is an open‑source Java job‑scheduling library from the OpenSymphony project, offering persistent jobs, job management and more compared with java.util.Timer .
Core Components
Quartz consists of three main parts:
Job : a class that implements org.quartz.Job and defines the execute() method.
Trigger : determines when a job runs; common types are SimpleTrigger and CronTrigger .
Scheduler : the engine that fires triggers and executes the associated jobs.
Demo
Add the Maven dependencies for Quartz and the optional Quartz‑jobs module, implement a job class, create a scheduler, job detail and trigger in a main method, and run the program.
<code><!-- core package -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<!-- utility package -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency></code> <code>public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("Task executed…");
}
}</code> <code>public static void main(String[] args) throws Exception {
// 1. Create Scheduler
SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
// 2. Create JobDetail bound to MyJob
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("job1", "group1")
.build();
// 3. Build a SimpleTrigger that fires every 30 seconds
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(30)
.repeatForever())
)
.build();
// 4. Schedule the job
scheduler.scheduleJob(job, trigger);
System.out.println(System.currentTimeMillis());
scheduler.start();
// Let the main thread sleep 1 minute, then shut down
TimeUnit.MINUTES.sleep(1);
scheduler.shutdown();
System.out.println(System.currentTimeMillis());
}
</code>Log output shows the job being executed at the configured interval.
JobDetail
JobDetail binds a Job instance and stores extended parameters. Each time the Scheduler fires a job, it creates a fresh Job object, executes execute() , and then discards the instance, avoiding concurrent access to the same object.
JobExecutionContext
When the Scheduler invokes a job, it passes a JobExecutionContext to the execute() method.
The context gives the job access to the runtime environment and the JobDetail's data map.
<code>public interface Job {
void execute(JobExecutionContext context) throws JobExecutionException;
}
</code>Builder methods such as usingJobData allow custom data to be attached to a job.
<code>usingJobData("tiggerDataMap", "test param")
</code>Retrieve the data inside execute() :
<code>context.getTrigger().getJobDataMap().get("tiggerDataMap");
context.getJobDetail().getJobDataMap().get("tiggerDataMap");
</code>Job State Parameters
A stateful job can retain data between executions via JobDataMap . By default, jobs are stateless and receive a fresh map each run.
<code>@PersistJobDataAfterExecution
public class JobStatus implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
long count = (Long) context.getJobDetail().getJobDataMap().get("count");
System.out.println("Run #" + count);
context.getJobDetail().getJobDataMap().put("count", ++count);
}
}
</code> <code>JobDetail job = JobBuilder.newJob(JobStatus.class)
.withIdentity("statusJob", "group1")
.usingJobData("count", 1L)
.build();
</code>Output demonstrates the incrementing counter across executions.
<code>Current execution, #1
[main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
Current execution, #2
Current execution, #3
</code>Trigger
SimpleTrigger
SimpleTrigger is suitable for basic scenarios such as running once, repeating at a fixed interval, or repeating a specific number of times.
<code>TriggerBuilder.newTrigger()
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(30)
.repeatForever())
</code>withRepeatCount(count) sets the repeat count; the actual number of executions equals count + 1 .
<code>TriggerBuilder.newTrigger()
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(30)
.withRepeatCount(5))
</code>CronTrigger
CronTrigger uses calendar‑based cron expressions for flexible scheduling.
<code>TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ?"))
</code>Spring Boot Integration
Add the spring-boot-starter-quartz dependency and configure the datasource and Quartz properties in application.yml (or application.properties ).
<code># Development environment configuration
server:
port: 80
servlet:
context-path: /
tomcat:
uri-encoding: UTF-8
spring:
datasource:
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&useSSL=true
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-timeout: 60000
validation-timeout: 3000
idle-timeout: 60000
login-timeout: 5
max-lifetime: 60000
maximum-pool-size: 10
minimum-idle: 10
read-only: false
</code>Quartz ships with a set of tables; create them using the provided quartz.sql script.
<code>CREATE TABLE `quartz_job` (
`job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Task ID',
`job_name` varchar(64) NOT NULL DEFAULT '' COMMENT 'Task name',
`job_group` varchar(64) NOT NULL DEFAULT 'DEFAULT' COMMENT 'Task group',
`invoke_target` varchar(500) NOT NULL COMMENT 'Invocation target string',
`cron_expression` varchar(255) DEFAULT '' COMMENT 'Cron expression',
`misfire_policy` varchar(20) DEFAULT '3' COMMENT 'Misfire policy (1 immediate, 2 once, 3 discard)',
`concurrent` char(1) DEFAULT '1' COMMENT 'Allow concurrent execution (0 yes, 1 no)',
`status` char(1) DEFAULT '0' COMMENT 'Status (0 normal, 1 paused)',
`remark` varchar(500) DEFAULT '' COMMENT 'Remarks',
PRIMARY KEY (`job_id`,`job_name`,`job_group`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='Scheduled task table';
</code>Define a job bean, for example MysqlJob , and implement an execute(String param) method.
<code>@Slf4j
@Component("mysqlJob")
public class MysqlJob {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
public void execute(String param) {
logger.info("Executing MySQL Job, time: {}, param: {}",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
param);
}
}
</code>Configure a SchedulerFactoryBean with datasource, thread‑pool, JobStore, and clustering options.
<code>@Configuration
public class ScheduleConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
Properties prop = new Properties();
prop.put("org.quartz.scheduler.instanceName", "shivaScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "20");
prop.put("org.quartz.threadPool.threadPriority", "5");
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
factory.setQuartzProperties(prop);
factory.setSchedulerName("shivaScheduler");
factory.setStartupDelay(1);
factory.setOverwriteExistingJobs(true);
factory.setAutoStartup(true);
return factory;
}
}
</code>ScheduleUtils contains helper methods to create jobs, build triggers, handle misfire policies, and generate JobKey / TriggerKey objects.
<code>public class ScheduleUtils {
private static Class<? extends Job> getQuartzJobClass(QuartzJob job) {
boolean isConcurrent = "0".equals(job.getConcurrent());
return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
}
public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
return TriggerKey.triggerKey("TASK_" + jobId, jobGroup);
}
public static JobKey getJobKey(Long jobId, String jobGroup) {
return JobKey.jobKey("TASK_" + jobId, jobGroup);
}
public static void createScheduleJob(Scheduler scheduler, QuartzJob job) throws Exception {
Class<? extends Job> jobClass = getQuartzJobClass(job);
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(getJobKey(job.getJobId(), job.getJobGroup()))
.build();
CronScheduleBuilder cronBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression())
.withMisfireHandlingInstructionDoNothing(); // example policy
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(getTriggerKey(job.getJobId(), job.getJobGroup()))
.withSchedule(cronBuilder)
.build();
jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
if (scheduler.checkExists(getJobKey(job.getJobId(), job.getJobGroup()))) {
scheduler.deleteJob(getJobKey(job.getJobId(), job.getJobGroup()));
}
scheduler.scheduleJob(jobDetail, trigger);
if (ScheduleConstants.Status.PAUSE.getValue().equals(job.getStatus())) {
scheduler.pauseJob(getJobKey(job.getJobId(), job.getJobGroup()));
}
}
// ... other utility methods omitted for brevity ...
}
</code>AbstractQuartzJob provides a template with before , after hooks and an abstract doExecute method that concrete subclasses implement.
<code>public abstract class AbstractQuartzJob implements Job {
private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
private static final ThreadLocal<Date> threadLocal = new ThreadLocal<>();
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
QuartzJob job = new QuartzJob();
BeanUtils.copyBeanProp(job, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
try {
before(context, job);
if (job != null) {
doExecute(context, job);
}
after(context, job, null);
} catch (Exception e) {
log.error("Task execution exception", e);
after(context, job, e);
}
}
protected void before(JobExecutionContext context, QuartzJob job) {
threadLocal.set(new Date());
}
protected void after(JobExecutionContext context, QuartzJob job, Exception e) {}
protected abstract void doExecute(JobExecutionContext context, QuartzJob job) throws Exception;
}
</code>Two concrete implementations decide whether concurrency is allowed:
<code>public class QuartzJobExecution extends AbstractQuartzJob {
@Override
protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
JobInvokeUtil.invokeMethod(job);
}
}
@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
@Override
protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
JobInvokeUtil.invokeMethod(job);
}
}
</code>JobInvokeUtil parses the invokeTarget string, resolves the bean or class, and uses reflection to call the specified method with optional parameters.
<code>public class JobInvokeUtil {
public static void invokeMethod(QuartzJob job) throws Exception {
String target = job.getInvokeTarget();
String beanName = getBeanName(target);
String methodName = getMethodName(target);
List<Object[]> params = getMethodParams(target);
Object bean = isValidClassName(beanName) ? Class.forName(beanName).newInstance() : SpringUtils.getBean(beanName);
invokeMethod(bean, methodName, params);
}
// helper methods (getBeanName, getMethodName, getMethodParams, etc.) omitted for brevity
}
</code>At application startup, existing Quartz schedules are cleared and rebuilt from the database to keep the scheduler in sync.
<code>@PostConstruct
public void init() throws Exception {
scheduler.clear();
List<QuartzJob> jobList = quartzMapper.selectJobAll();
for (QuartzJob job : jobList) {
ScheduleUtils.createScheduleJob(scheduler, job);
}
}
</code>Concurrency Control
By default Quartz permits concurrent execution of the same JobDetail. Adding @DisallowConcurrentExecution to a Job class prevents overlapping runs of that specific JobDetail, while still allowing different JobDetails to run in parallel.
Excluding Specific Dates
Use a Calendar to mark dates as excluded and attach it to a trigger.
<code>Calendar c = new GregorianCalendar(2014, 7, 15); // August 15, 2014
cal.setDayExcluded(c, true);
scheduler.addCalendar("exclude", cal, false, false);
Trigger trigger = TriggerBuilder.newTrigger()
.modifiedByCalendar("exclude")
// other trigger settings
.build();
</code>Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.