在電商系統中會常常遇到這樣一種場景,就是商品的定時上下架功能,總不能每次都手動執行吧,這個時候咱們首先想到的就是利用定時任務來實現這個功能。html
目前實現定時任務主要有如下幾種方式:java
JDK自帶 :JDK自帶的Timer以及JDK1.5+ 新增的ScheduledExecutorService;web
第三方框架 :使用 Quartz、elastic-job、xxl-job 等開源第三方定時任務框架,適合分佈式項目應用。該方式的缺點是配置複雜。spring
Spring :使用 Spring 提供的一個註解 @Schedule
,開發簡單,使用比較方便。apache
本文博主主要向你們介紹Quartz框架和Spring定時任務的使用。json
Quartz 是一個徹底由 Java 編寫的開源做業調度框架,爲在 Java 應用程序中進行做業調度提供了簡單卻強大的機制。框架
Quartz 能夠與 J2EE 與 J2SE 應用程序相結合也能夠單獨使用。maven
Quartz 容許程序開發人員根據時間的間隔來調度做業。分佈式
Quartz 實現了做業和觸發器的多對多的關係,還能把多個做業與不一樣的觸發器關聯。ide
在正式學習使用Quartz以前,咱們須要瞭解幾個有關Quartz的核心概念,方便咱們後面學習
Job 表示一個工做,要執行的具體內容。此接口中只有一個方法,以下:
void execute(JobExecutionContext context) // context是重要的上下文,能夠訪問到關聯的JobDetail對象和本次觸發的Trigger對象,以及在此之上設定的數據。
JobDetail 表示一個具體的可執行的調度程序,Job 是這個可執行程調度程序所要執行的內容,另外 JobDetail 還包含了這個任務調度的方案和策略。
Trigger 表明一個調度參數的配置,何時去調。
Scheduler 表明一個調度容器,一個調度容器中能夠註冊多個 JobDetail 和 Trigger。當 Trigger 與 JobDetail 組合,就能夠被 Scheduler 容器調度了。
1、建立一個SpringBoot項目,pom.xml配置以下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.songguoliang</groupId> <artifactId>spring-boot-quartz</artifactId> <version>1.0-SNAPSHOT</version> <name>spring-boot-quartz</name> <description>Spring Boot使用Quartz定時任務</description> <!-- Spring Boot啓動器父類 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- Spring Boot web啓動器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- quartz --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2、建立一個Job(Job裏面是要執行的具體內容)
package com.example.quartz; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.SchedulerException; import java.time.LocalDateTime; public class TestJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { // 經過context獲取trigger中的數據 Object tv1 = context.getTrigger().getJobDataMap().get("t1"); Object tv2 = context.getTrigger().getJobDataMap().get("t2"); // 經過context獲取JobDetail中的數據 Object jv1 = context.getJobDetail().getJobDataMap().get("j1"); Object jv2 = context.getJobDetail().getJobDataMap().get("j2"); Object sv = null; try { sv = context.getScheduler().getContext().get("skey"); } catch (SchedulerException e) { e.printStackTrace(); } System.out.println(tv1+":"+tv2); System.out.println(jv1+":"+jv2); System.out.println(sv); System.out.println("date:"+ LocalDateTime.now()); } }
3、執行Job
package com.example.quartz; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; public class QuartzTest { public static void main(String[] args) { try { //建立一個scheduler Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); //向scheduler中put值 scheduler.getContext().put("skey", "svalue"); //建立一個Trigger Trigger trigger = TriggerBuilder.newTrigger() //給該Trigger起一個id .withIdentity("trigger1") //以Key-Value形式關聯數據 .usingJobData("t1", "tv1") //每3秒觸發一次,無限循環 .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3) .repeatForever()).build(); trigger.getJobDataMap().put("t2","tv2"); //建立一個JobDetail JobDetail jobDetail = JobBuilder.newJob(TestJob.class) //給該JobDetail起一個id .withIdentity("myJob", "myGroup") .usingJobData("j1", "jv1") .build(); jobDetail.getJobDataMap().put("j2", "jv2"); //註冊trigger並啓動scheduler scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); //若是想要中止這個Job,能夠調用shutdown方法 //scheduler.shutdown(); } catch (SchedulerException e) { e.printStackTrace(); } } }
控制檯輸出
10:46:54.075 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 10:46:54.079 [main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main 10:46:54.089 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 10:46:54.089 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created. 10:46:54.090 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 10:46:54.091 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 10:46:54.091 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 10:46:54.091 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2 10:46:54.104 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started. 10:46:54.104 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:46:54.106 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob 10:46:54.110 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:46:54.110 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob tv1:tv2 jv1:jv2 svalue date:2020-12-19T10:46:54.144 10:46:57.092 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob 10:46:57.092 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:46:57.092 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob tv1:tv2 jv1:jv2 svalue date:2020-12-19T10:46:57.092 10:47:00.101 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob 10:47:00.101 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:47:00.101 [DefaultQuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob tv1:tv2 jv1:jv2 svalue date:2020-12-19T10:47:00.101 10:47:03.096 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob 10:47:03.096 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers 10:47:03.096 [DefaultQuartzScheduler_Worker-4] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob tv1:tv2 jv1:jv2 svalue date:2020-12-19T10:47:03.096
從輸出結果咱們能夠看到此Job每隔3秒執行一次
有關概念
一、Job
job的一個 trigger 被觸發後(稍後會講到),execute() 方法會被 scheduler 的一個工做線程調用;傳遞給 execute() 方法的 JobExecutionContext 對象中保存着該 job 運行時的一些信息 ,執行 job 的 scheduler 的引用,觸發 job 的 trigger 的引用,JobDetail 對象引用,以及一些其它信息。
二、JobDetail :
JobDetail 對象是在將 job 加入 scheduler 時,由客戶端程序(你的程序)建立的。它包含 job 的各類屬性設置,以及用於存儲 job 實例狀態信息的 JobDataMap
三、Trigger:
Trigger 用於觸發 Job 的執行。當你準備調度一個 job 時,你建立一個 Trigger 的實例,而後設置調度相關的屬性。Trigger 也有一個相關聯的 JobDataMap,用於給 Job 傳遞一些觸發相關的參數。Quartz 自帶了各類不一樣類型的 Trigger,最經常使用的主要是 SimpleTrigger 和 CronTrigger。SimpleTrigger 主要用於一次性執行的 Job(只在某個特定的時間點執行一次),或者 Job 在特定的時間點執行,重複執行 N 次,每次執行間隔T個時間單位。CronTrigger 在基於日曆的調度上很是有用,如「每一個星期五的正午」,或者「每個月的第十天的上午 10:15」等。
在定義一個Job時,咱們須要實現Job接口,該接口只有一個execute
方法。
從上一節的案例中咱們能夠發現,咱們經過Scheduler去執行Job,咱們傳給scheduler一個JobDetail實例,由於咱們在建立JobDetail時,將要執行的job的類名傳給了JobDetail,因此scheduler就知道了要執行何種類型的job。(這裏利用了Java中的反射建立實例對象)每次當scheduler執行job時,在調用其execute(…)方法以前會建立該類的一個新的實例;執行完畢,對該實例的引用就被丟棄了,實例會被垃圾回收;這種執行策略帶來的一個後果是,job必須有一個無參的構造函數(當使用默認的JobFactory時);另外一個後果是,在job類中,不該該定義有狀態的數據屬性,由於在job的屢次執行中,這些屬性的值不會保留。
那麼咱們該如何給Job配置相關屬性呢?答案就是經過JobDetail
JobDataMap實現了Map接口,能夠存放鍵值對數據,在Job執行的時候,咱們就能夠經過JobExecutionContext獲取到JobDataMap中的數據,以下
JobDetail jobDetail = JobBuilder.newJob(TestJob.class) .withIdentity("myJob", "myGroup") .usingJobData("j1", "jv1") .usingJobData("j2","jv2") .build();
在job的執行過程當中,能夠從JobDataMap中取出數據,以下示例:
Object jv1 = context.getJobDetail().getJobDataMap().get("j1");
固然,若是你但願實現屬性的自動注入,那麼你可使用下面的方法
package com.example.quartz; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; public class QuartzTest2 { public static void main(String[] args) { try { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1") .usingJobData("t1", "tv1") .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3) .repeatForever()) .build(); JobDetail jobDetail = JobBuilder.newJob(TestJob2.class) .withIdentity("jd") .usingJobData("name", "張三") .usingJobData("age", 12) .build(); scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); } catch (SchedulerException e) { e.printStackTrace(); } } }
package com.example.quartz; import org.quartz.*; public class TestJob2 implements Job { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobKey jobKey = context.getJobDetail().getKey(); JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); System.out.println("name:" + name + "age:" +age); } }
給Job類加上get和set方法(屬性名稱要和JobDataMap中的key相同),那麼JobDataMap中的值就是自動注入到Job中,不須要手動獲取
Trigger 用於觸發 Job 的執行。當你準備調度一個 job 時,你建立一個 Trigger 的實例,而後設置調度相關的屬性。全部類型的trigger都有TriggerKey這個屬性,表示trigger的身份;除此以外,trigger還有不少其它的公共屬性。這些屬性,在構建trigger的時候能夠經過TriggerBuilder設置。
若是你的trigger不少(或者Quartz線程池的工做線程太少),Quartz可能沒有足夠的資源同時觸發全部的trigger;這種狀況下,你可能但願控制哪些trigger優先使用Quartz的工做線程,要達到該目的,能夠在trigger上設置priority屬性。好比,你有N個trigger須要同時觸發,但只有Z個工做線程,優先級最高的Z個trigger會被首先觸發。若是沒有爲trigger設置優先級,trigger使用默認優先級,值爲5;priority屬性的值能夠是任意整數,正數、負數均可以。
注意:只有同時觸發的trigger之間纔會比較優先級。10:59觸發的trigger老是在11:00觸發的trigger以前執行。
注意:若是trigger是可恢復的,在恢復後再調度時,優先級與原trigger是同樣的。
trigger還有一個重要的屬性misfire;若是scheduler關閉了,或者Quartz線程池中沒有可用的線程來執行job,此時持久性的trigger就會錯過(miss)其觸發時間,即錯過觸發(misfire)。不一樣類型的trigger,有不一樣的misfire機制。它們默認都使用「智能機制(smart policy)」,即根據trigger的類型和配置動態調整行爲
SimpleTrigger簡單點說,就是在具體的時間點執行一次,或者在具體的時間點執行,而且以指定的間隔重複執行若干次。相似於鬧鐘,你定了一個週末早晨7點的鬧鐘,這個鬧鐘會在週末早上7點準時響起。鬧鐘還有個功能就是過5分鐘以後再響一次,這對應着指定的間隔重複執行若干次。
一、指定時間開始觸發,不重複:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("st1", "group1") .startAt(new Date()) // 從當前時間開始執行一次,不重複 .build();
二、指定時間觸發,每隔2秒執行一次,重複5次:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("st2", "group1") .startAt(new Date()) .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(2) // 2秒 .withRepeatCount(5)// 5次 ) .build();
三、1分鐘之後開始觸發,僅執行一次:
long time = 1 * 60 * 1000; Date now = new Date(); SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("st3", "group1") .startAt(new Date(now.getTime() + time)) .build();
四、當即觸發,每隔2秒鐘執行一次,直到2020-12-19 13:20:00
String dateStr="2020-12-19 13:20:00"; String pattern="yyyy-MM-dd HH:mm:ss"; SimpleDateFormat dateFormat=new SimpleDateFormat(pattern); Date date = dateFormat.parse(dateStr); SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("st4", "group1") .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(2) .repeatForever()) .endAt(date) .build();
五、在13:00觸發,而後每2小時重複一次:
String dateStr="2020-12-19 13:00:00"; String pattern="yyyy-MM-dd HH:mm:ss"; SimpleDateFormat dateFormat=new SimpleDateFormat(pattern); Date date = dateFormat.parse(dateStr); SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("st2", "group1") .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInHours(2) .repeatForever()) .build();
misfire:被錯過的執行任務策略
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("st6") .withSchedule( SimpleScheduleBuilder.simpleSchedule() .withIntervalInMinutes(5) .repeatForever() .withMisfireHandlingInstructionNextWithExistingCount() ) .build();
CronTrigger一般比Simple Trigger更有用,若是你須要在指定日期執行某項任務,使用CronTrigger就很是方便,好比若是你想在每個月的15號給會員發放優惠券,或者每週五中午12點統計用戶本週使用產品時長。
cron
表達式是一個字符串,該字符串由 6 個空格分爲 7 個域,每個域表明一個時間含義。 一般定義 「年」 的部分能夠省略,實際經常使用的 Cron 表達式由前 6 部分組成。格式以下
[秒] [分] [時] [日] [月] [周] [年] Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)
域 | 是否必填 | 值以及範圍 | 通配符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
時 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * ? / L W |
月 | 是 | 1-12 或 JAN-DEC | , - * / |
周 | 是 | 1-7 或 SUN-SAT | , - * ? / L # |
年 | 否 | 1970-2099 | , - * / |
須要說明的是,Cron 表達式中,「周」 是從週日開始計算的。「周」 域上的 1
表示的是週日,7
表示週六。
天天晚上12點觸發任務:
0 0 0 * * ?
每隔 1 分鐘執行一次:
0 */1 * * * ?
每個月 1 號凌晨 1 點執行一次:
0 0 1 1 * ?
每個月最後一天 23 點執行一次:
0 0 23 L * ?
每週週六凌晨 3 點實行一次:
0 0 3 ? * L
在24分,30分執行一次:
0 24,30 * * * ?
是否是有點沒看懂,不要緊,咱們可使用Cron表達式生成器幫助咱們生成Cron表達式
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 8-17 * * ?")) .build();
不少時候咱們都須要爲系統創建一個定時任務來幫咱們作一些事情,SpringBoot 已經幫咱們實現好了一個,咱們只須要直接使用便可
1、引入依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2、開啓註解
在 SpringBoot 中咱們只須要在啓動類上加上@EnableScheduling
即可以啓動定時任務了。
@SpringBootApplication @EnableScheduling public class TaskApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
3、建立scheduled task
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; /** * @author wugongzi */ @Component public class ScheduledTasks { private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); /** * fixedRate:固定速率執行。每5秒執行一次。 */ @Scheduled(fixedRate = 5000) public void reportCurrentTimeWithFixedRate() { log.info("Current Thread : {}", Thread.currentThread().getName()); log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date())); } /** * fixedDelay:固定延遲執行。距離上一次調用成功後2秒才執。 */ @Scheduled(fixedDelay = 2000) public void reportCurrentTimeWithFixedDelay() { try { TimeUnit.SECONDS.sleep(3); log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date())); } catch (InterruptedException e) { e.printStackTrace(); } } /** * initialDelay:初始延遲。任務的第一次執行將延遲5秒,而後將以5秒的固定間隔執行。 */ @Scheduled(initialDelay = 5000, fixedRate = 5000) public void reportCurrentTimeWithInitialDelay() { log.info("Fixed Rate Task with Initial Delay : The time is now {}", dateFormat.format(new Date())); } /** * cron:使用Cron表達式。 每分鐘的1,2秒運行 */ @Scheduled(cron = "1-2 * * * * ? ") public void reportCurrentTimeWithCronExpression() { log.info("Cron Expression: The time is now {}", dateFormat.format(new Date())); } }
啓動項目即可以看到效果。