Unravel › Java concepts › Scheduling and batch
At a glance
Think of a conference organiser.
- 1A class implementing Job (or extending Spring's QuartzJobBean) has one method, execute(JobExecutionContext)That method is the work. Quartz creates a new instance of the class for every run, so fields are not kept between runs.
- 2A JobDetail is built with JobBuilder.newJob(MyJob.class).withIdentity("report", "billing").build()A Trigger is built with TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")).build(). scheduler.scheduleJob(detail, trigger) registers the pair.
- 3Quartz cron has six or seven fields (seconds first, optional year) and uses ?for "no specific value" in day-of-month or day-of-week, which differs from Unix cron and from Spring's @Scheduled.
- 4Data for a run travels in a JobDataMap attached to the JobDetail or Trigger and read through the contextQuartz threads from a pool run the jobs, so many jobs can run at the same time.
- A trigger that could not fire on time (the scheduler was down, or all threads were busy) has a misfire instruction that decides what happens: fire now, skip to the next time, or run every missed execution
- @DisallowConcurrentExecution stops two runs of the same JobDetail from overlapping
- The scheduler runs one main thread that looks ahead for triggers due soon, acquires them (with a row lock in the JDBC store), and hands each to a worker in a thread pool
JobDetail job = JobBuilder.newJob(ReportJob.class) .withIdentity("nightly-report", "billing") .usingJobData("region", "EU") .build();Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("nightly-report-trigger", "billing") .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?") .withMisfireHandlingInstructionDoNothing()) .build();scheduler.scheduleJob(job, trigger);- line 1 uses JobDetail, the core of Quartz scheduler
- line 5 uses Trigger, the core of Quartz scheduler
- line 7 uses CronScheduleBuilder, the core of Quartz scheduler
Read the full explanation (about 2 min)
Quartz is a job scheduling library. You write a Job (the work), describe it with a JobDetail, attach one or more Triggers (when to run), and a Scheduler fires the jobs on time, optionally storing everything in a database so a cluster shares it.
A mental model
Think of a conference organiser. The Job is the talk, the JobDetail is the programme entry describing it, the Trigger is the time slot, and the Scheduler is the person with the clock who sends the speaker on stage when the slot arrives.
How it works
A class implementing Job (or extending Spring's QuartzJobBean) has one method, execute(JobExecutionContext). That method is the work. Quartz creates a new instance of the class for every run, so fields are not kept between runs.
A JobDetail is built with JobBuilder.newJob(MyJob.class).withIdentity("report", "billing").build(). A Trigger is built with TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")).build(). scheduler.scheduleJob(detail, trigger) registers the pair.
Quartz cron has six or seven fields (seconds first, optional year) and uses ? for "no specific value" in day-of-month or day-of-week, which differs from Unix cron and from Spring's @Scheduled.
Data for a run travels in a JobDataMap attached to the JobDetail or Trigger and read through the context. Quartz threads from a pool run the jobs, so many jobs can run at the same time.
Going deeper
A trigger that could not fire on time (the scheduler was down, or all threads were busy) has a misfire instruction that decides what happens: fire now, skip to the next time, or run every missed execution. Pick this on purpose for important jobs, because the default may not match what you expect.
@DisallowConcurrentExecution stops two runs of the same JobDetail from overlapping. Without it a slow job can start again while the previous run is still going, and the two fight over the same data.
In a cluster, Quartz uses a JDBC job store with database locks so each trigger fires on exactly one node. The in-memory store is per process: with three replicas of an application, every replica runs every job.
Exceptions thrown from execute do not stop the schedule; wrap them in JobExecutionException to tell Quartz whether to refire or unschedule, and log failures yourself, because nobody else will notice.
Under the hood
The scheduler runs one main thread that looks ahead for triggers due soon, acquires them (with a row lock in the JDBC store), and hands each to a worker in a thread pool. Time zones are applied when a cron trigger computes its next fire time, so daylight saving changes can skip or repeat a local time.
Common mistakes
- Keeping state in job fields, which is lost because a new instance is made per run.
- Running several replicas with the in-memory store and getting duplicate runs.
- Ignoring misfire handling for critical jobs.
- Copying a Unix cron expression into a Quartz trigger without adding the seconds field and the
?.
How Unravel recognises it
When you paste code, Unravel looks for these signs of quartz scheduler. Spotting them yourself is a good habit when you read unfamiliar code:
- Annotations:
@DisallowConcurrentExecution@PersistJobDataAfterExecution - Types:
SchedulerJobDetailTriggerCronTriggerJobBuilderTriggerBuilderCronScheduleBuilderSimpleScheduleBuilderSchedulerFactoryBeanQuartzJobBeanJobExecutionContextJobExecutionExceptionJobDataMapSchedulerFactoryand 1 more - Created with new:
JobKey
Part of a learning path
- Background work in Spring · previous: @Scheduled jobs · next: Executors and thread pools
- Scheduled jobs, batch and integration · previous: Cron expressions in Java · next: Distributed locks and job locking (ShedLock)
Related concepts
Frequently asked questions
What is Quartz scheduler?
Quartz is a job scheduling library. You write a Job (the work), describe it with a JobDetail, attach one or more Triggers (when to run), and a Scheduler fires the jobs on time, optionally storing everything in a database so a cluster shares it.
What are the most common mistakes with Quartz scheduler?
Keeping state in job fields, which is lost because a new instance is made per run. Running several replicas with the in-memory store and getting duplicate runs. Ignoring misfire handling for critical jobs. Copying a Unix cron expression into a Quartz trigger without adding the seconds field and the ?.
How can I see Quartz scheduler in real code?
Paste your own Java into Unravel and it will point out where quartz scheduler appears, explain how it works at the detail level you choose, and draw the flow of the code around it.