前言:
之前開發定時任務時,有兩種方式:
a、如果是SpringBoot項目,在方法上加上 @Scheduled 注解,然后開配置下cron就可以了。 缺點:不支持通過某種條件來開啟任務
b、使用 Executors.newScheduledThreadPool() 啟動一個定時線程。缺點:服務重啟或者任務失敗,線程就結束了
項目中使用了Quartz框架,很完美的解決了以上兩個問題。本文主要記錄Quartz框架的基本使用
上代碼:
以下配置是基於SpringBoot 2.1.0 + Quartz 2.3.0版本
1、pom.xml文件():
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--定時任務需要依賴context模塊 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> </dependency>
2、application.yml配置文件
app: db: host: 127.0.01 port: 3306 dbname: xwj server: port: 18090 spring: application: name: quarts-one datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: net.sf.log4jdbc.DriverSpy url: jdbc:log4jdbc:mysql://${app.db.host}:${app.db.port}/${app.db.dbname}?autoReconnect=true&failOverReadOnly=false&createDatabaseIfNotExist=true&useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: 123456 logging: level: jdbc: off jdbc.sqltiming: error #記錄sql執行的時間 #root: INFO com.xwj: debug
3、Quartz的配置文件 quartz.properties (發現只能用properties文件,如果用yml文件不生效)
org.quartz.scheduler.instanceName = instance_one org.quartz.scheduler.instanceId = instance_id_one 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.useProperties = false org.quartz.jobStore.clusterCheckinInterval = 20000 org.quartz.scheduler.idleWaitTime = 5000 org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 20 org.quartz.threadPool.threadPriority = 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
4、Quartz的配置類
/** * Quartz配置類 */ @Configuration public class QuartzConfig { /** * 繼承org.springframework.scheduling.quartz.SpringBeanJobFactory 實現任務實例化方式 */ public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { private transient AutowireCapableBeanFactory beanFactory; @Override public void setApplicationContext(final ApplicationContext context) { beanFactory = context.getAutowireCapableBeanFactory(); } /** * 將job實例交給spring ioc托管 我們在job實例實現類內可以直接使用spring注入的調用被spring ioc管理的實例 */ @Override protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { final Object job = super.createJobInstance(bundle); // 將job實例交付給spring ioc beanFactory.autowireBean(job); return job; } } /** * 配置任務工廠實例 */ @Bean public JobFactory jobFactory(ApplicationContext applicationContext) { // 采用自定義任務工廠 整合spring實例來完成構建任務 AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); return jobFactory; } /** * 配置任務調度器 使用項目數據源作為quartz數據源 * * @param jobFactory 自定義配置任務工廠 * @param dataSource 數據源實例 */ @Bean public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); // 將spring管理job自定義工廠交由調度器維護 schedulerFactoryBean.setJobFactory(jobFactory); // 設置覆蓋已存在的任務 schedulerFactoryBean.setOverwriteExistingJobs(true); // 項目啟動完成后,等待2秒后開始執行調度器初始化 schedulerFactoryBean.setStartupDelay(2); // 設置調度器自動運行 schedulerFactoryBean.setAutoStartup(true); // 設置數據源,使用與項目統一數據源 schedulerFactoryBean.setDataSource(dataSource); // 設置上下文spring bean name schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext"); // 設置配置文件位置 schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties")); return schedulerFactoryBean; } }
5、Quartz工具類
@Slf4j @Component public class MyQuartzScheduler { @Autowired private Scheduler scheduler; // 任務 private final String JOB_NAME_PREFIX = "JOB_"; // 任務名稱前綴 /** * 指定時間后執行任務(只會執行一次) * * @param triggerStartTime 指定時間 */ @SneakyThrows public void addJob(Class<? extends Job> jobClass, String jobName, Date triggerStartTime, Map<String, Object> params) { // 使用job類名作為組名 String groupName = jobClass.getSimpleName(); // 創建任務觸發器 Trigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, groupName).startAt(triggerStartTime).build(); // 將觸發器與任務綁定到調度器內 this.scheduleJob(jobClass, groupName, jobName, params, trigger); } /** * 帶觸發器的任務(執行多次) * * @param cronExpression 定時任務表達式 */ @SneakyThrows public void addJobWithCron(Class<? extends Job> jobClass, String jobName, String cronExpression, Map<String, Object> params) { // 使用job類名作為組名 String groupName = jobClass.getSimpleName(); // 基於表達式構建觸發器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(jobName, groupName).withSchedule(cronScheduleBuilder).build(); // 將觸發器與任務綁定到調度器內 this.scheduleJob(jobClass, groupName, jobName, params, cronTrigger); } /** * 帶觸發器的任務,同時指定時間段(立馬執行) * * @param timeoutSeconds 超時時間(秒) * @param cronExpression 定時任務表達式 */ @SneakyThrows public void addJobWithCron(Class<? extends Job> jobClass, String jobName, String cronExpression, long timeoutSeconds,
Map<String, Object> params) { // 使用job類名作為組名 String groupName = jobClass.getSimpleName(); // 計算結束時間 Date endDate = TimeUtil.localDateTime2Date(LocalDateTime.now().plusSeconds(timeoutSeconds)); // 基於表達式構建觸發器,同時指定時間段 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(jobName, groupName)
.startNow().endAt(endDate)
.withSchedule(cronScheduleBuilder).build(); // 將觸發器與任務綁定到調度器內 this.scheduleJob(jobClass, groupName, jobName, params, cronTrigger); } @SneakyThrows private void scheduleJob(Class<? extends Job> jobClass, String groupName, String jobName, Map<String, Object> params, Trigger trigger) { jobName = StringUtils.join(JOB_NAME_PREFIX, jobName); log.info("創建任務,任務名稱:{}", jobName); // 創建任務 JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, groupName).build(); // 添加參數 jobDetail.getJobDataMap().putAll(params); // 將觸發器與任務綁定到調度器內 scheduler.scheduleJob(jobDetail, trigger); } /** * 刪除某個任務 */ @SneakyThrows public boolean deleteJob(String name, String group) { JobKey jobKey = new JobKey(name, group); JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) { throw new RuntimeException("任務不存在"); } return scheduler.deleteJob(jobKey); } /** * 修改某個任務的執行時間 */ @SneakyThrows public boolean modifyJob(String name, String group, String time) { Date date = null; TriggerKey triggerKey = new TriggerKey(name, group); CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey); String oldTime = cronTrigger.getCronExpression(); if (!oldTime.equalsIgnoreCase(time)) { CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(time); CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(name, group).withSchedule(cronScheduleBuilder).build(); date = scheduler.rescheduleJob(triggerKey, trigger); } return date != null; } /** * 獲取任務狀態 */ @SneakyThrows public TriggerState getJobState(String name, String group) { TriggerKey triggerKey = TriggerKey.triggerKey(name, group); return scheduler.getTriggerState(triggerKey); } /** * 獲取任務狀態 */ @SneakyThrows public TriggerState getJobState(TriggerKey triggerKey) { return scheduler.getTriggerState(triggerKey); } /** * 暫停所有任務 */ @SneakyThrows public void pauseAllJob() { scheduler.pauseAll(); } /** * 暫停某個任務 */ @SneakyThrows public void pauseJob(String name, String group) { JobKey jobKey = new JobKey(name, group); JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) { throw new RuntimeException("任務不存在"); } scheduler.pauseJob(jobKey); } /** * 恢復所有任務 */ @SneakyThrows public void resumeAllJob() { scheduler.resumeAll(); } /** * 恢復某個任務 */ @SneakyThrows public void resumeJob(String name, String group) { JobKey jobKey = new JobKey(name, group); JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (jobDetail == null) { throw new RuntimeException("任務不存在"); } scheduler.resumeJob(jobKey); }/** * 通過group查詢有多少個運行的任務 */ @SneakyThrows public long getRunningJobCountByGroup(Class<? extends Job> jobClass) { String groupName = jobClass.getSimpleName(); GroupMatcher<JobKey> matcher = GroupMatcher.jobGroupEquals(groupName); Set<JobKey> jobKeySet = scheduler.getJobKeys(matcher); if (CollectionUtils.isNotEmpty(jobKeySet)) { return jobKeySet.stream().filter(d -> StringUtils.equals(d.getGroup(), groupName)).count(); } return 0; } }
6、新建一個job任務
@Slf4j public class MyJob extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { JobDetail jobDetail = context.getJobDetail(); JobKey jobKey = jobDetail.getKey(); JobDataMap dataMap = jobDetail.getJobDataMap(); // 接收參數 log.info("執行MyJob任務,任務名稱:{},接收參數:{}", jobKey.getName(), dataMap.getString("id")); } }
7、新建一個Controller測試類
@RestController @RequestMapping("/quartz") public class QuartzApiController { @Autowired private MyQuartzScheduler quartzScheduler; /** * 指定時間點觸發的任務() */ @RequestMapping("/job/start/{id}") public void startQuartzJob(@PathVariable String id) { // 20s之后執行 LocalDateTime ldt = LocalDateTime.now(); Date date = TimeUtil.localDateTime2Date(ldt.plusSeconds(20)); Map<String, Object> params = new HashMap<String, Object>(); params.put("id", id); quartzScheduler.addJob(MyJob.class, id, date, params); } /** * 定時任務 */ @RequestMapping("/job/cron/{id}") public void cronQuartzJob(@PathVariable String id) { Map<String, Object> params = new HashMap<>(); params.put("id", id); // 每10秒執行一次 quartzScheduler.addJobWithCron(MyJob.class, id, "0/10 * * * * ?", params); } /** * 刪除某個任務 */ @RequestMapping(value = "/job/delete") public boolean deleteJob(String name, String group) { return quartzScheduler.deleteJob(name, group); } /** * 修改任務執行時間 */ @RequestMapping("/job/modify") public boolean modifyQuartzJob(String name, String group, String time) { return quartzScheduler.modifyJob(name, group, time); } /** * 暫停某個任務 */ @RequestMapping(value = "/job/pause") public void pauseQuartzJob(String name, String group) { quartzScheduler.pauseJob(name, group); } /** * 暫停所有任務 */ @RequestMapping(value = "/job/pauseAll") public void pauseAllQuartzJob() { quartzScheduler.pauseAllJob(); } }
8、在數據庫中執行新建quartz相關表的sql (腳本太長,可自己百度,網上一大堆)
9、啟動SpringBoot服務,控制台可以看到Quartz相關的日志信息(表示Quartz配置成功):
打開數據庫中的 qrtz_scheduler_state 表,會發現表中多了一條數據(SCHED_NAME和INSTANCE_NAME 分別是quartz.properties配置文件中的instanceName和instanceId):
10、在瀏覽器請求 http://localhost:18090/quartz/job/start/123,在控制台會看到如下日志:
2020-12-11 21:22:03.635 INFO 13052 --- [io-18090-exec-6] c.x.q.MyQuartzScheduler : 創建任務,任務名稱:JOB_123
同時在表 qrtz_job_details 和 qrtz_triggers 中會分別插入一條數據,表示該任務的詳細信息
過了20秒之后(上面配置的定時任務是20秒之后執行),可以看到又打印出一條日志:
2020-12-11 21:22:23.667 INFO 13052 --- [ce_one_Worker-2] c.x.q.j.MyJob : 執行MyJob任務,任務名稱:JOB_123,接收參數:123
表示任務已經成功執行,並且表 qrtz_job_details 和 qrtz_triggers 的任務信息已經被刪除
至此Quartz配置和測試完成,其它復雜的測試,可請求QuartzApiController的方法自行操作
擴展:
使用SpringBoot封裝好的Quartz,會比上面的方式簡單一些,但是使用方法跟上面一模一樣。需要修改的點包括:
1、不使用 quartz.properties 配置文件和 QuartzConfig 配置類,直接在application.yml文件中增加Quartz配置:
spring: quartz: #quartz相關屬性配置 properties: org: quartz: scheduler: instanceName: instance_one #調度器實例名稱 #instanceId: AUTO #調度器實例編號自動生成 instanceId: instance_id_one #調度器實例id jobStore: class: org.quartz.impl.jdbcjobstore.JobStoreTX #持久化方式配置 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #持久化方式配置數據驅動,MySQL數據庫 tablePrefix: QRTZ_ #quartz相關數據表前綴名 isClustered: true #開啟分布式部署 clusterCheckinInterval: 10000 #分布式節點有效性檢查時間間隔,單位:毫秒 useProperties: false #配置是否使用 threadPool: class: org.quartz.simpl.SimpleThreadPool #線程池實現類 threadCount: 10 #執行最大並發線程數量 threadPriority: 5 #線程優先級 threadsInheritContextClassLoaderOfInitializingThread: true #配置是否啟動自動加載數據庫內的定時任務,默認true #數據庫方式 job-store-type: jdbc
Quartz詳細說明:Quartz官方中文文檔