spring如何設定定時任務詳解(@Scheduled)

yoylee_web發表於2018-12-26

以前用過這個註解實現定時任務,但是隻是使用,現在做專案又用到了這個功能,系統的學習一下~

spring定時任務設定有兩種方式,註解和xml配置。推薦使用註解,在本文章也主要介紹註解方式配置

一:註解方式配置定時任務:

下面的步驟預設spring的其他配置項都已經配置好(比如啟動註解配置,包路徑掃描等)

1:在spring配置檔案中配置,新增名稱空間

  • xmlns新增:
xmlns:task="http://www.springframework.org/schema/task"
  • xsi:schemaLocation新增
    • 注意"4.3"這是版本號,要修改和你的其他xsd版本號一致
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd"
  • 啟動註解驅動
    • 注意“dataScheduler”為自定義名稱,可以通過自己的業務定義 合適 的名稱
<task:annotation-driven scheduler="dataScheduler"/>
  • 開啟任務排程器,並配置執行緒池大小
    • 注意此處的id指定的就是上面的自定義名稱
    • spring的任務排程預設是單執行緒的,如果你的專案會有多工定時執行,並且執行時間會相交的話,應該根據任務的具體執行情況配置執行緒池大小
    • 如果不配置執行緒池,並且A和B任務在同一時間執行,A先執行的話,B要等待A執行完才可以執行,AB不會同時執行
<task:scheduler id="dataScheduler" pool-size="5"/>

2:使用註解配置定時任務

  • 在你需要配置定時任務的方法上使用註解@Scheduled即可,下面一個簡單案例:

    • 注意 下面的案例是在每天的早上2點執行
    • “0 0 2 * * *”是怎麼組合的?下面會詳細介紹@Scheduled()註解
@Scheduled(cron = "0 0 2 * * *")
public void init(){
    todo...
}

在此需要注意:@Scheduled只能註釋在無參的方法上,我看網上有許多部落格說必須無參無返回值的,但是經過我的測試有返回值是可以的,可能是版本更新了吧。

現在就算是完成spring定時器的使用了,下面讓我們來詳細的看一下@Scheduled註解吧~

二:@Scheduled

@Scheduled註解是Spring專門為定時任務設計的註解

首先,讓我們來看看這個註解是怎麼組成的吧(適用於版本JDK8與spring4.3及其以上)

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

    String cron() default "";
    String zone() default "";
    long fixedDelay() default -1L;
    String fixedDelayString() default "";
    long fixedRate() default -1L;
    String fixedRateString() default "";
    long initialDelay() default -1L;
    String initialDelayString() default "";
}

從上述程式碼中看以看出:

1:@Scheduled被註解部分:

  • 元註解@Target表明@Scheduled註解可以在方法上使用(ElementType.METHOD),也可以作為元註解對其他註解進行註解(ElementType.ANNOTATION_TYPE)
  • 元註解@Retention表明此註解會被JVM所保留,也就是會儲存在執行時(RetentionPolicy.RUNTIME)
  • 元註解@Documented表明此註解應該被 javadoc工具記錄。預設情況下javadoc是不包括註解的。
  • JDK8新增的註解@Repeatable表明此註解可以在同一個地方被重複使用

上述的所涉及到的註解有不清楚作用的,可以自行baidu\google,網上有好多介紹的文章。

2:@Scheduled引數部分,總共包含8各部分,我們來分別看一下其作用:

  • cron:一個類似cron的表示式,擴充套件了通常的UN * X定義,包括秒,分,時,星期,月,年的觸發器。
  • fixedDelay:在最後一次呼叫結束和下一次呼叫開始之間以固定週期(以毫秒為單位)執行帶註釋的方法。(要等待上次任務完成後)
  • fixedDelayString:同上面作用一樣,只是String型別
  • fixedRate:在呼叫之間以固定的週期(以毫秒為單位)執行帶註釋的方法。(不需要等待上次任務完成)
  • fixedRateString:同上面作用一樣,只是String型別
  • initialDelay:第一次執行fixedRate()或fixedDelay()任務之前延遲的毫秒數 。
  • initialDelayString:同上面作用一樣,只是String型別
  • zone:指明解析cron表示式的時區。

cron可以組合出更多的定時情況,fixedDelay和fixedRate只能定義每隔多長時間執行一次。

在上述cron、fixedDelay、fixedRate 只能同時存在一個,使用其中一個就不能使用另外的一個,否則會報錯“java.lang.IllegalStateException”

3:cron引數

一個cron表示式可以有6個元素或者7個元素組成(“年”這個元素可以省略,省略之後就是預設“每一年”)

3.1:按順序依次為:

  1. 秒(0~59)
  2. 分鐘(0~59)
  3. 小時(0~23)
  4. 天(0~31)
  5. 月(0~11)
  6. 星期(1~7 )或者( SUN,MON,TUE,WED,THU,FRI,SAT。其中SUN = 1)
  7. 年份(1970-2099)

3.2:每個元素可以接受的值:

欄位 允許值 允許的特殊字元
0-59 , - * /
0-59 , - * /
小時 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * ? / L C #
空, 1970-2099 , - * /

3.3:一些特殊字元解釋與注意事項,可以結合下面的小案例來理解:

  • 其中每個元素可以是一個值(如6),一個連續區間(9-12),一個間隔時間(8-18/4)(/表示每隔4小時),一個列表(1,3,5),萬用字元。
  • 其中的“日”由於"月份中的日期"和"星期"這兩個元素互斥的,必須要對其中一個設定“?”。
  • 有些子表示式能包含一些範圍或列表
    • 例如:子表示式(天(星期))可以為 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”
  • “*”字元代表所有可能的值
  • “/”字元用來指定數值的增量
    • 例如:在子表示式(分鐘)裡的“0/15”表示從第0分鐘開始,每15分鐘
    • 在子表示式(分鐘)裡的“3/20”表示從第3分鐘開始,每20分鐘(它和“3,23,43”)的含義一樣
  • “?”字元僅被用於天(月)和天(星期)兩個子表示式,表示不指定值
    • 當2個子表示式其中之一被指定了值以後,為了避免衝突,需要將另一個子表示式的值設為“?”
  • “L” 字元僅被用於天(月)和天(星期)兩個子表示式,它是單詞“last”的縮寫
    • 如果在“L”前有具體的內容,它就具有其他的含義了。例如:“6L”表示這個月的倒數第6天
    • 注意:在使用“L”引數時,不要指定列表或範圍,因為這會導致問題
  • “W” 字元代表著平日(Mon-Fri),並且僅能用於日域中。它用來指定離指定日的最近的一個平日。大部分的商業處理都是基於工作周的,所以 W 字元可能是非常重要的。
    • 例如,日域中的 15W 意味著 “離該月15號的最近一個平日。” 假如15號是星期六,那麼 trigger 會在14號(星期五)觸發,因為星期四比星期一離15號更近。
  • “C”:代表“Calendar”的意思。它的意思是計劃所關聯的日期,如果日期沒有被關聯,則相當於日曆中所有日期。例如5C在日期欄位中就相當於日曆5日以後的第一天。1C在星期欄位中相當於星期日後的第一天。

3.4:一些小案例:

  • “0 0 10,14,16 * * ?” 每天上午10點,下午2點,4點
  • “0 0/30 9-17 * * ?” 朝九晚五工作時間內每半小時
  • “0 0 12 ? * WED” 表示每個星期三中午12點
  • “0 0 12 * * ?” 每天中午12點觸發
  • “0 15 10 ? * *” 每天上午10:15觸發(這個和下一個案例說明,必須"月份中的日期"和"星期"中有一個設定為“?”)
  • “0 15 10 * * ?” 每天上午10:15觸發
  • “0 15 10 * * ? *” 每天上午10:15觸發(7個元素型別案例,第七個元素代表年)
  • “0 15 10 * * ? 2005” 2005年的每天上午10:15觸發
  • “0 * 14 * * ?” 在每天下午2點到下午2:59期間的每1分鐘觸發
  • “0 0/5 14 * * ?” 在每天下午2點到下午2:55期間的每5分鐘觸發
  • “0 0/5 14,18 * * ?” 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
  • “0 0-5 14 * * ?” 在每天下午2點到下午2:05期間的每1分鐘觸發
  • “0 10,44 14 ? 3 WED” 每年三月的星期三的下午2:10和2:44觸發
  • “0 15 10 ? * MON-FRI” 週一至週五的上午10:15觸發
  • “0 15 10 15 * ?” 每月15日上午10:15觸發
  • “0 15 10 L * ?” 每月最後一日的上午10:15觸發
  • “0 15 10 ? * 6L” 每月的最後一個星期五上午10:15觸發
  • “0 15 10 ? * 6L 2002-2005” 2002年至2005年的每月的最後一個星期五上午10:15觸發
  • “0 15 10 ? * 6#3” 每月的第三個星期五上午10:15觸發

到這個地方你應該對@Scheduled有一個較全面的理解了,下面我們就來簡單的看一下其實現原理吧~

三:原理簡介

1:主要過程:

  1. spring在使用applicationContext將類全部初始化。

  2. 呼叫ScheduledAnnotationBeanPostProcessor類中的postProcessAfterInitialization方法獲取專案中所有被註解 @Scheduled註解的方法 。

  3. 通過processScheduled方法將所有定時的方法存放在Set tasks = new LinkedHashSet(4); 定時任務佇列中,並解析相應的引數。順序存放,任務也是順序執行。存放順序為cron>fixedDelay>fixedRate

  4. 將解析引數後的定時任務存放在一個初始容量為16 的map中,key為bean name,value為定時任務:private final Map<Object, Set> scheduledTasks = new IdentityHashMap(16);

  5. 之後交給ScheduledTaskRegistrar類的方法scheduleTasks去新增定時任務。

2:上述就是一個大致過程,下面看一下相應的原始碼:

注意 :spring對定時任務的操作的原始碼全部在spring-context.jar包下的org.springframework.scheduling包下面,主要包含三部分:annotation、config、 support,大家有興趣的話可以去看看

1:獲取專案中所有被註解 @Scheduled註解的方法

public Object postProcessAfterInitialization(Object bean, String beanName) {
    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
    if (!this.nonAnnotatedClasses.contains(targetClass)) {
        Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, new MetadataLookup<Set<Scheduled>>() {
            public Set<Scheduled> inspect(Method method) {
                //獲取註解方法
                **Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);**
                return !scheduledMethods.isEmpty() ? scheduledMethods : null;
            }
        });
        if (annotatedMethods.isEmpty()) {
            ...
        } else {
            Iterator var5 = annotatedMethods.entrySet().iterator();
            while(var5.hasNext()) {

                Entry<Method, Set<Scheduled>> entry = (Entry)var5.next();
                Method method = (Method)entry.getKey();
                Iterator var8 = ((Set)entry.getValue()).iterator();

                while(var8.hasNext()) {
                    Scheduled scheduled = (Scheduled)var8.next();
                    //將獲取的任務進行引數解析並存放到任務佇列
                    this.processScheduled(scheduled, method, bean);
                }
            }
           ...
        }
    }
    return bean;
}

2:通過processScheduled方法將所有定時的方法存放在定時任務佇列中

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
    try {
        ...
        //解析initialDelayString引數
        String initialDelayString = scheduled.initialDelayString();
        if (StringUtils.hasText(initialDelayString)) {
           ...
        }
        //解析cron引數
        String cron = scheduled.cron();
        if (StringUtils.hasText(cron)) {
            ...
            //存放到任務佇列中
            tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
        }
        ...
        //解析fixedDelay引數
        long fixedDelay = scheduled.fixedDelay();
        if (fixedDelay >= 0L) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)));
        }
        String fixedDelayString = scheduled.fixedDelayString();
        if (StringUtils.hasText(fixedDelayString)) {
            ...
            //存放到任務佇列中
            tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)));
        }
        //解析fixedRate引數
        long fixedRate = scheduled.fixedRate();
        if (fixedRate >= 0L) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)));
        }
        String fixedRateString = scheduled.fixedRateString();
        if (StringUtils.hasText(fixedRateString)) {
            ...
            //存放到任務佇列中
            tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)));
        }
        Assert.isTrue(processedSchedule, errorMessage);
        Map var19 = this.scheduledTasks;
        //併發控制並將任務存放在map中
        synchronized(this.scheduledTasks) {
            Set<ScheduledTask> registeredTasks = (Set)this.scheduledTasks.get(bean);
            if (registeredTasks == null) {
                registeredTasks = new LinkedHashSet(4);
                //將任務存放在map中
                this.scheduledTasks.put(bean, registeredTasks);
            }
            ((Set)registeredTasks).addAll(tasks);
        }
    } catch (IllegalArgumentException var26) {
        throw new IllegalStateException("Encountered invalid @Scheduled method '" + method.getName() + "': " + var26.getMessage());
    }
}

3:之後交給ScheduledTaskRegistrar類的方法scheduleTasks去新增定時任務

protected void scheduleTasks() {
    if (this.taskScheduler == null) {
        this.localExecutor = Executors.newSingleThreadScheduledExecutor();
        this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
    }
    Iterator var1;
    if (this.triggerTasks != null) {
        var1 = this.triggerTasks.iterator();
        while(var1.hasNext()) {
            TriggerTask task = (TriggerTask)var1.next();
            this.addScheduledTask(this.scheduleTriggerTask(task));
        }
    }
    if (this.cronTasks != null) {
        var1 = this.cronTasks.iterator();
        while(var1.hasNext()) {
            CronTask task = (CronTask)var1.next();
            this.addScheduledTask(this.scheduleCronTask(task));
        }
    }
    IntervalTask task;
    if (this.fixedRateTasks != null) {
        var1 = this.fixedRateTasks.iterator();
        while(var1.hasNext()) {
            task = (IntervalTask)var1.next();
            this.addScheduledTask(this.scheduleFixedRateTask(task));
        }
    }
    if (this.fixedDelayTasks != null) {
        var1 = this.fixedDelayTasks.iterator();
        while(var1.hasNext()) {
            task = (IntervalTask)var1.next();
            this.addScheduledTask(this.scheduleFixedDelayTask(task));
        }
    }
}

此部分只是對原理進行了簡單的介紹,如果有興趣深入瞭解,可以去看看原始碼~

四:其他

做定時任務還可以使用java自帶的原生API,Timer和TimerTask去設計。

  • Timer:一種工具,執行緒用其安排以後在後臺執行緒中執行的任務。可安排任務執行一次,或者定期重複執行。

  • TimerTask:定義一個被執行的任務,Timer 安排該任務為一次執行或重複執行的任務。

可以這樣理解Timer是一種定時器工具,用來在一個後臺執行緒計劃執行指定任務,而TimerTask一個抽象類,它的子類代表一個可以被Timer計劃的任務。

這裡就簡單的提一下,並不是本文的重點,具體的用法自行google吧~

refer:部落格 官網

如果轉載此博文,請附上本文連結:https://blog.csdn.net/CSDN___LYY/article/details/85266567 謝謝合作~

如果感覺這篇文章對您有所幫助,請點選一下喜歡或者關注博主,您的喜歡和關注將是我前進的最大動力!

相關文章