| 組件 | 職責 |
|---|---|
| Job | 任務邏輯執行單元,實現 Job 介面 |
| JobDetail | Job 定義元數據(名稱、組、數據映射) |
| Trigger | 觸發條件:SimpleTrigger(間隔)、CronTrigger(Cron 表達式) |
| Scheduler | 調度器核心,管理 Job/Trigger 生命週期 |
| SchedulerFactory | 創建 Scheduler,StdSchedulerFactory 為標準實現 |
<!-- 1. JobDetail 定義 -->
<bean id="testJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.example.TestQuartzJob"/>
<property name="jobDataAsMap">
<map>
<entry key="testService" value-ref="testServiceImpl"/>
</map>
</property>
</bean>
<!-- 2. CronTrigger 定義 -->
<bean id="testTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="testJobDetail"/>
<property name="cronExpression" value="0 0/1 * * * ?"/> <!-- 每分鐘 -->
</bean>
<!-- 3. SchedulerFactoryBean 組裝 -->
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list><ref bean="testTrigger"/></list>
</property>
<property name="overwriteExistingJobs" value="true"/>
<property name="autoStartup" value="true"/>
</bean>
public class TestQuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
// 從 JobDataMap 獲取 Spring Bean
TestService testService = (TestService) context.getJobDetail()
.getJobDataMap().get("testService");
testService.executeBusinessLogic();
}
}
優化:直接使用 @Autowired 注入 Spring Bean,需 Job 類交由 Spring 管理(@Component + @Configurable 或 Spring 3.1+ SpringBeanJobFactory)。
public class DynamicCronTriggerFactoryBean extends CronTriggerFactoryBean {
@Autowired private SysParamService sysParamService;
private String paramKey;
public void setParamKey(String paramKey) { this.paramKey = paramKey; }
@PostConstruct
public void initCronExpression() {
setCronExpression(fetchCronFromDB());
}
private String fetchCronFromDB() {
if (StringUtils.isEmpty(paramKey)) return "0 0/5 * * * ?"; // 預設 5 分鐘
try {
SysParam param = sysParamService.getByKey(paramKey);
return (param != null && StringUtils.isNotBlank(param.getValue()))
? param.getValue() : "0 0/5 * * * ?";
} catch (Exception e) {
return "0 0/5 * * * ?";
}
}
}
<bean id="dynamicTrigger" class="com.example.DynamicCronTriggerFactoryBean">
<property name="jobDetail" ref="testJobDetail"/>
<property name="paramKey" value="quartz.cron.testJob"/>
<property name="sysParamService" ref="sysParamService"/>
</bean>
基於 Scheduler API 封裝通用管理器:
@Component
public class QuartzManager {
private static final Logger log = LoggerFactory.getLogger(QuartzManager.class);
private final Scheduler scheduler;
public QuartzManager(Scheduler scheduler) { this.scheduler = scheduler; }
/** 新增任務 */
public void addJob(String jobName, String jobGroup, Class<? extends Job> jobClass,
String cronExpression, Map<String, Object> jobData) {
try {
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, jobGroup)
.usingJobData(new JobDataMap(jobData))
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName + "Trigger", jobGroup)
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build();
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isStarted()) scheduler.start();
log.info("任務新增成功: {}.{}", jobGroup, jobName);
} catch (SchedulerException e) {
throw new RuntimeException("新增任務失敗", e);
}
}
/** 移除任務 */
public void removeJob(String jobName, String jobGroup) {
try {
JobKey jobKey = new JobKey(jobName, jobGroup);
scheduler.deleteJob(jobKey);
log.info("任務移除成功: {}.{}", jobGroup, jobName);
} catch (SchedulerException e) {
throw new RuntimeException("移除任務失敗", e);
}
}
/** 修改 Cron 表達式 */
public void rescheduleJob(String jobName, String jobGroup, String newCron) {
try {
TriggerKey triggerKey = new TriggerKey(jobName + "Trigger", jobGroup);
Trigger newTrigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(newCron))
.build();
scheduler.rescheduleJob(triggerKey, newTrigger);
log.info("任務重新調度: {}.{} -> {}", jobGroup, jobName, newCron);
} catch (SchedulerException e) {
throw new RuntimeException("重新調度失敗", e);
}
}
/** 暫停/恢復任務 */
public void pauseJob(String jobName, String jobGroup) throws SchedulerException {
scheduler.pauseJob(new JobKey(jobName, jobGroup));
}
public void resumeJob(String jobName, String jobGroup) throws SchedulerException {
scheduler.resumeJob(new JobKey(jobName, jobGroup));
}
/** 查詢所有任務狀態 */
public List<JobInfo> listJobs() throws SchedulerException {
List<JobInfo> result = new ArrayList<>();
for (String groupName : scheduler.getJobGroupNames()) {
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
for (Trigger trigger : triggers) {
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
result.add(new JobInfo(
jobKey.getName(), jobKey.getGroup(),
trigger.getKey().getName(),
trigger instanceof CronTrigger ? ((CronTrigger) trigger).getCronExpression() : "N/A",
state.name(),
trigger.getNextFireTime(),
trigger.getPreviousFireTime()
));
}
}
}
return result;
}
@Data @AllArgsConstructor
public static class JobInfo {
private String jobName, jobGroup, triggerName, cronExpression, state;
private Date nextFireTime, prevFireTime;
}
}
@RestController
@RequestMapping("/api/quartz")
@RequiredArgsConstructor
public class QuartzController {
private final QuartzManager quartzManager;
@PostMapping("/jobs")
public Result addJob(@RequestBody JobRequest req) {
quartzManager.addJob(req.getJobName(), req.getJobGroup(), req.getJobClass(),
req.getCron(), req.getJobData());
return Result.success();
}
@DeleteMapping("/jobs/{group}/{name}")
public Result removeJob(@PathVariable String group, @PathVariable String name) {
quartzManager.removeJob(name, group);
return Result.success();
}
@PutMapping("/jobs/{group}/{name}/cron")
public Result reschedule(@PathVariable String group, @PathVariable String name,
@RequestParam String cron) {
quartzManager.rescheduleJob(name, group, cron);
return Result.success();
}
@GetMapping("/jobs")
public Result listJobs() throws SchedulerException {
return Result.success(quartzManager.listJobs());
}
@PostMapping("/jobs/{group}/{name}/pause")
public Result pause(@PathVariable String group, @PathVariable String name) throws SchedulerException {
quartzManager.pauseJob(name, group);
return Result.success();
}
@PostMapping("/jobs/{group}/{name}/resume")
public Result resume(@PathVariable String group, @PathVariable String name) throws SchedulerException {
quartzManager.resumeJob(name, group);
return Result.success();
}
}
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.dataSource = myDS
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz?useSSL=false
org.quartz.dataSource.myDS.user = quartz
org.quartz.dataSource.myDS.password = quartz
org.quartz.dataSource.myDS.maxConnections = 20
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setQuartzProperties(quartzProperties());
factory.setOverwriteExistingJobs(true);
factory.setAutoStartup(true);
return factory;
}
@Bean
public Properties quartzProperties() {
PropertiesFactoryBean pf = new PropertiesFactoryBean();
pf.setLocation(new ClassPathResource("quartz.properties"));
return pf.getObject();
}
| 問題 | 解決方案 |
|---|---|
| 任務重複執行 | 集群模式下確保 isClustered=true、同一數據源、實例 ID 自動生成 |
| Spring Bean 注入失效 | 使用 SpringBeanJobFactory 或 @Autowired 配合 @Configurable |
| Cron 表達式錯誤 | 使用 CronExpression.isValidExpression() 啟動前校驗 |
| 任務執行時間過長阻塞線程池 | 調大 threadCount、異步執行業務邏輯、設置 misfireInstruction |
| 數據庫表未建立 | 執行 Quartz 官方 SQL 腳本(tables_mysql.sql 等) |
| Misfire 處理 | 配置 MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 或 DO_NOTHING |
@Configuration
@EnableScheduling
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setWaitForJobsToCompleteOnShutdown(true);
return factory;
}
}
配合 spring-boot-starter-quartz 依賴,自動建表、自動配置數據源。
Quartz 企業級定時任務解決方案完整鏈路:
QuartzManager 封裝 Scheduler API,支援運行時增刪改查核心原則:業務邏輯與調度解耦、狀態持久化、集群安全、變更可追溯。