Backend Development 25 min read

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.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Mastering Quartz Scheduler in Spring Boot: From Basics to Advanced Integration

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>&lt;!-- core package --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.quartz-scheduler&lt;/groupId&gt;
    &lt;artifactId&gt;quartz&lt;/artifactId&gt;
    &lt;version&gt;2.3.0&lt;/version&gt;
&lt;/dependency&gt;
&lt;!-- utility package --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.quartz-scheduler&lt;/groupId&gt;
    &lt;artifactId&gt;quartz-jobs&lt;/artifactId&gt;
    &lt;version&gt;2.3.0&lt;/version&gt;
&lt;/dependency&gt;</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>
JavaConcurrencySpring BootQuartzJob SchedulingCronTrigger
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.