Quartz 是 Java 生態最成熟的調度框架,支持Cron 表達式、集群、持久化、彈性誤火策略、監聽器鏈。Spring 提供無縫整合,本文展示從傳統 XML 到現代註解驅動的完整實踐。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.x</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
package com.example.task;
public class ApiMonitorTask {
private static final Logger log = LoggerFactory.getLogger(ApiMonitorTask.class);
public void verifyChannel() {
log.info("Quartz 任務執行:上游接口巡檢 - {}", LocalDateTime.now());
// 業務邏輯:調用 API、檢查響應、記錄異常、發送告警
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 1. 目標 Bean -->
<bean id="apiMonitorTask" class="com.example.task.ApiMonitorTask" />
<!-- 2. JobDetail: 定義調用對象與方法 -->
<bean id="monitorJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="apiMonitorTask" />
<property name="targetMethod" value="verifyChannel" />
<property name="concurrent" value="false" /> <!-- 禁止並發執行 -->
</bean>
<!-- 3. Trigger: Cron 表達式觸發 -->
<bean id="monitorTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="monitorJobDetail" />
<property name="cronExpression" value="0 0/30 * * * ?" /> <!-- 每 30 分鐘 -->
<property name="misfireInstructionName" value="MISFIRE_INSTRUCTION_FIRE_NOW" /> <!-- 誤火策略 -->
</bean>
<!-- 4. SchedulerFactoryBean: 總調度器 -->
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="monitorTrigger" />
</list>
</property>
<property name="startupDelay" value="30" /> <!-- 啟動延遲 30s,待應用完全啟動 -->
<property name="autoStartup" value="true" />
<property name="overwriteExistingJobs" value="true" /> <!-- 覆蓋同名任務 -->
</bean>
</beans>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/quartz-config.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
@Configuration
@EnableScheduling // 啟用 Spring 任務調度 (可選,Quartz 不依賴此註解)
public class QuartzConfig {
@Bean
public JobDetail apiMonitorJobDetail() {
return JobBuilder.newJob(ApiMonitorJob.class) // 實現 Job 接口的類
.withIdentity("apiMonitorJob")
.storeDurably() // 無 Trigger 時也保留
.build();
}
@Bean
public Trigger apiMonitorTrigger(JobDetail apiMonitorJobDetail) {
return TriggerBuilder.newTrigger()
.forJob(apiMonitorJobDetail)
.withIdentity("apiMonitorTrigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/30 * * * ?"))
.withMisfireHandlingInstructionFireAndProceed() // 誤火立即執行
.build();
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean(Trigger apiMonitorTrigger) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setTriggers(apiMonitorTrigger);
factory.setStartupDelay(30);
factory.setOverwriteExistingJobs(true);
// 集群配置 (需數據庫表)
// factory.setDataSource(dataSource);
// factory.setQuartzProperties(quartzProperties());
return factory;
}
}
@Component
@DisallowConcurrentExecution // 禁止並發 (等同 concurrent=false)
public class ApiMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiMonitorJob.class);
@Autowired
private ApiMonitorService apiMonitorService; // 注入 Service 層
@Override
public void execute(JobExecutionContext context) {
log.info("Quartz Job 執行:上游接口巡檢 - {}", LocalDateTime.now());
try {
apiMonitorService.verifyChannel();
} catch (Exception e) {
log.error("巡檢任務異常", e);
// 可拋出 JobExecutionException 觸發重試/誤火策略
throw new JobExecutionException(e);
}
}
}
@Service
public class DynamicTaskService {
@Autowired
private Scheduler scheduler;
/** 添加/更新任務 */
public void addOrUpdateJob(String jobName, String jobGroup, String cron, Class extends Job> jobClass) {
try {
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, jobGroup)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(jobName + "Trigger", jobGroup)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.scheduleJob(jobDetail, trigger); // 若存在則更新
} catch (SchedulerException e) {
throw new RuntimeException("任務調度失敗", e);
}
}
/** 暫停/恢復/刪除 */
public void pauseJob(String jobName, String jobGroup) throws SchedulerException {
scheduler.pauseJob(JobKey.jobKey(jobName, jobGroup));
}
public void resumeJob(String jobName, String jobGroup) throws SchedulerException {
scheduler.resumeJob(JobKey.jobKey(jobName, jobGroup));
}
public void deleteJob(String jobName, String jobGroup) throws SchedulerException {
scheduler.deleteJob(JobKey.jobKey(jobName, jobGroup));
}
}
-- 必須創建 11 張表:QRTZ_JOB_DETAILS, QRTZ_TRIGGERS, QRTZ_CRON_TRIGGERS,
-- QRTZ_SIMPLE_TRIGGERS, QRTZ_BLOB_TRIGGERS, QRTZ_CALENDARS, QRTZ_PAUSED_TRIGGER_GRPS,
-- QRTZ_FIRED_TRIGGERS, QRTZ_SCHEDULER_STATE, QRTZ_LOCKS, QRTZ_SIMPROP_TRIGGERS
org.quartz.scheduler.instanceName = MyClusteredScheduler
org.quartz.scheduler.instanceId = AUTO
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.clusterCheckinInterval = 20000
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 20
org.quartz.threadPool.threadPriority = 5
@Configuration
public class QuartzClusterConfig {
@Bean
public Properties quartzProperties() {
Properties props = new Properties();
props.setProperty("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
props.setProperty("org.quartz.jobStore.isClustered", "true");
// ... 其他屬性
return props;
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource, Properties quartzProperties) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setQuartzProperties(quartzProperties);
factory.setOverwriteExistingJobs(true);
return factory;
}
}
| 坑點 | 現象 | 解決 |
|---|---|---|
| 任務方法有返回值 | MethodInvokingJobDetailFactoryBean 不支持返回值 | 改用 Job 接口實現類,或返回 void |
| Cron 表達式錯誤 | 啟動報錯 / 不按預期執行 | 在線驗證工具測試、注意 6/7 位制 (Quartz 用 6 位:秒 分 時 日 月 周) |
| 集群下重複執行 | 多節點同時執行同一任務 | 開啟 `isClustered=true`、配置共享數據庫、同步時間 (NTP) |
| 誤火策略不生效 | 錯過執行時間後不補償 | 顯式設置 `misfireInstruction`、檢查 `misfireThreshold` (默認 60s) |
| Spring 事務不生效 | Job 類中 @Transactional 無效 | Job 非 Spring Bean 時需手動獲取 TransactionTemplate、或確保 @Component 掃描 |
格式:秒 分 時 日 月 周 (年可選)
* 任意值
, 列表值 (1,3,5)
- 範圍 (1-5)
/ 步長 (0/30 = 每 30 秒)
? 不指定值 (日/周 二擇一)
L 最後一天/最後一個周X
W 工作日 (最近工作日)
# 第 N 個周X (6#3 = 第 3 個周五)
常用示例:
0 0 2 * * ? 每天 02:00
0 0/5 * * * ? 每 5 分鐘
0 0 10,14,18 * * ? 每天 10:00, 14:00, 18:00
0 0/30 9-17 * * MON-FRI 工作日 9-17 點每 30 分鐘
0 0 0 1 * ? 每月 1 號 00:00
Spring + Quartz 定時任務演進路徑:
核心原則:任務邏輯無狀態、冪等設計、異常捕獲不拋出、監控告警全覆蓋。