Spring Boot 2.x學習筆記:實現定時任務詳解

Maven依賴配置

<!-- 引入 spring-boot-starter-web 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-data-jpa 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- 引入 mysql-connector-java 依賴 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-quartz 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-test 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

application.properties配置

# DATASOURCE CONFIGURATION
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc\:mysql\://localhost\:3306/spring_boot_demo_db_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=tb890705

# Tomcat will use the above plus the following to setup connection pooling
spring.datasource.tomcat.max-active=100
spring.datasource.tomcat.max-idle=8
spring.datasource.tomcat.min-idle=8
spring.datasource.tomcat.initial-size=10
spring.datasource.tomcat.init-s-q-l=SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci
# spring.datasource.tomcat.validation-query=
# spring.datasource.tomcat.test-on-borrow=false
# spring.datasource.tomcat.test-on-return=false
# spring.datasource.tomcat.test-while-idle=
# spring.datasource.tomcat.time-between-eviction-runs-millis=
# spring.datasource.tomcat.min-evictable-idle-time-millis=
# spring.datasource.tomcat.max-wait=

# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
# spring.jpa.properties.*= # properties to set on the JPA connection
spring.jpa.open-in-view=true
spring.jpa.show-sql=false
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.database=MYSQL
spring.jpa.generate-ddl=false
# spring.jpa.hibernate.naming-strategy= # naming classname
# spring.jpa.hibernate.ddl-auto= # defaults to create-drop for embedded dbs
spring.data.jpa.repositories.enabled=true

SpringBoot啓動類

package com.yuanx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * 
 * @ClassName: DemoApplication
 * @Description: Demo啓動類
 * @author YuanXu
 * @date 2019年7月5日 下午4:09:33
 *
 */
@SpringBootApplication
@EnableScheduling
public class DemoApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(DemoApplication.class);
    }

    /**
     * @Title main
     * @Description Demo啓動入口
     * @author YuanXu
     * @param args
     */
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

創建Entity、Repository、Service類

基於註解的靜態單線程定時任務實現

package com.yuanx.schedule.task;

import java.time.LocalDateTime;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * 
 * @ClassName: SaticScheduleTask
 * @Description: 靜態定時任務(基於註解@Scheduled 默認爲單線程,開啓多個任務時,任務的執行時機會受上一個任務執行時間的影響)
 * @author YuanXu
 * @date 2019年7月22日 下午5:30:33
 *
 */
@Component
public class SaticScheduleTask {

    @SuppressWarnings("unused")
    private static Logger log = LoggerFactory.getLogger(SaticScheduleTask.class);

    /**
     * 
     * @Title: task_one
     * @Description: 靜態定時任務一(每10秒執行一次)
     * @author YuanXu
     */
    @Scheduled(cron = "0/10 * * * * ?")
    private void task_one() throws InterruptedException {
        System.out.println(String.format("靜態定時任務一\r\n{%s}-[%s]\r\n執行時間: %s", Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now()));
        Thread.sleep(6 * 1_000);
        System.out.println();
    }

    /**
     * 
     * @Title: task_two
     * @Description: 靜態定時任務二(每15秒執行一次)
     * @author YuanXu
     */
    @Scheduled(cron = "0/15 * * * * ?")
    private void task_two() {
        System.out.println(String.format("靜態定時任務二\r\n{%s}-[%s]\r\n執行時間: %s", Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now()));
        System.out.println();
    }
}

基於註解的靜態單線程定時任務測試結果

基於註解的靜態多線程定時任務實現

package com.yuanx.schedule.task;

import java.time.LocalDateTime;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * 
 * @ClassName: MultithreadScheduleTask
 * @Description: 多線程定時任務
 * @author YuanXu
 * @date 2019年7月24日 上午11:41:59
 *
 */
@Component
@EnableAsync
public class MultithreadScheduleTask {

    /**
     * 
     * @Title: task_one
     * @Description: 多線程定時任務一
     * @author YuanXu
     * @throws InterruptedException
     */
    @Async
    @Scheduled(fixedDelay = 1000)
    public void task_one() throws InterruptedException {
        System.out.println(String.format("多線程定時任務一\r\n{%s}-[%s]\r\n執行時間: %s", Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now().toLocalTime()));
        System.out.println();
        Thread.sleep(1000 * 10);
    }

    /**
     * 
     * @Title: task_two
     * @Description: 多線程定時任務二
     * @author YuanXu
     */
    @Async
    @Scheduled(fixedDelay = 2000)
    public void task_two() {
        System.out.println(String.format("多線程定時任務二\r\n{%s}-[%s]\r\n執行時間: %s", Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now().toLocalTime()));
        System.out.println();
    }

}

基於註解的靜態多線程定時任務測試結果

基於接口的動態多線程定時任務實現

package com.yuanx.schedule.task;

import java.time.LocalDateTime;
import java.util.List;

import org.quartz.CronExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.yuanx.schedule.entity.TTaskCronEntity;
import com.yuanx.schedule.service.ITTaskCronService;

/**
 * 
 * @ClassName: DynamicScheduleTask
 * @Description: 動態定時任務(基於接口[SchedulingConfigurer])
 * @author YuanXu
 * @date 2019年7月23日 下午4:32:51
 *
 */
@Component
public class DynamicScheduleTask implements SchedulingConfigurer {

    private static Logger log = LoggerFactory.getLogger(DynamicScheduleTask.class);

    @Autowired
    private ITTaskCronService taskCronService;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 動態定時任務添加多線程支持
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(20);
        taskScheduler.initialize();
        taskRegistrar.setTaskScheduler(taskScheduler);

        List<TTaskCronEntity> tasks = taskCronService.getEnableTask();
        if (tasks != null && tasks.size() > 0) {
            tasks.forEach(task -> {
                String corn = task.getCron();
                if (!StringUtils.isEmpty(corn) && CronExpression.isValidExpression(corn)) {
                    // 任務內容
                    Runnable runnable = () -> {
                        System.out.println(String.format("[1]動態定時任務: %s\r\n{%s}-[%s]\r\n執行時間: %s", task.getId(), Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now()));
                        System.out.println();
                    };

                    // 任務觸發器
                    Trigger trigger = triggerContext -> {
                        TTaskCronEntity tsk = taskCronService.findById(task.getId());
                        String corn_new = tsk.getCron();
                        if (!StringUtils.isEmpty(corn_new) && !corn_new.equals(corn) && CronExpression.isValidExpression(corn_new)) {
                            CronTrigger cronTrigger = new CronTrigger(corn_new);
                            return cronTrigger.nextExecutionTime(triggerContext);
                        } else {
                            CronTrigger cronTrigger = new CronTrigger(corn);
                            return cronTrigger.nextExecutionTime(triggerContext);
                        }
                    };

                    // 註冊任務
                    taskRegistrar.addTriggerTask(runnable, trigger);
                } else if (task.getFixedDelay() != null) {
                    // 任務內容
                    Runnable runnable = new Runnable() {
                        @Override
                        public void run() {
                            System.out.println(String.format("[2]動態定時任務: %s\r\n{%s}-[%s]\r\n執行時間: %s", task.getId(), Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now()));
                            System.out.println();
                        }
                    };

                    // 註冊任務
                    taskRegistrar.addFixedDelayTask(runnable, task.getFixedDelay());
                } else if (task.getFixedRate() != null) {
                    Runnable runnable = () -> {
                        System.out.println(String.format("[3]動態定時任務: %s\r\n{%s}-[%s]\r\n執行時間: %s", task.getId(), Thread.currentThread().getName(), this.getClass().getName(), LocalDateTime.now()));
                        System.out.println();
                    };

                    // 註冊任務
                    taskRegistrar.addFixedRateTask(runnable, task.getFixedRate());
                } else {
                    log.error("ID爲[{}]的定時任務參數異常,初始化失敗!", task.getId());
                }
            });
        }
    }
}

說明:默認情況下SchedulingConfigurer採用的是單線程的實現方式,如果需要實現多線程,則需要指定PoolSize,代碼如下:

// 動態定時任務添加多線程支持
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(20);
taskScheduler.initialize();
taskRegistrar.setTaskScheduler(taskScheduler);

動態定時任務配置表結構

基於接口的動態多線程定時任務測試結果

@Scheduled註解的cron表達式說明

Cron表達式是一個字符串,字符串以5或6個空格隔開,分爲6或7個域,每一個域代表一個含義,Cron有如下兩種語法格式:

Seconds Minutes Hours DayofMonth Month DayofWeek Year或 Seconds Minutes Hours DayofMonth Month DayofWeek

說明:每一個域都使用數字,但還可以出現如下特殊字符,它們的含義是:

  • *:表示匹配該域的任意值,假如在Minutes域使用*, 即表示每分鐘都會觸發事件。
  • ? :只能用在DayofMonth和DayofWeek兩個域。因爲DayofMonth和 DayofWeek會相互影響,所以按我的理解?應該是佔位符。例如想在每月的20日觸發調度,不管20日到底是星期幾,則只能使用如下寫法: 13 13 15 20 * ?, 其中最後一位只能用,而不能使用*。
  • - :表示範圍,例如在Minutes域使用5-20,表示從5分到20分鐘每分鐘觸發一次
  • /:表示起始時間開始觸發,然後每隔固定時間觸發一次,例如在Minutes域使用5/20,則意味着5分鐘觸發一次,而25,45等分別觸發一次。
  • , :表示列出枚舉值值。例如:在Minutes域使用5,20,則意味着在5和20分每分鐘觸發一次。
  • L :表示最後,只能出現在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最後的一個星期四觸發。
  • W: 表示有效工作日(週一到週五),只能出現在DayofMonth域,系統將在離指定日期的最近的有效工作日觸發事件。例如:在 DayofMonth使用5W,如果5日是星期六,則將在最近的工作日:星期五,即4日觸發。如果5日是星期天,則在6日(週一)觸發;如果5日在星期一 到星期五中的一天,則就在5日觸發。另外一點,W的最近尋找不會跨過月份。
  • LW :這兩個字符可以連用,表示在某個月最後一個工作日,即最後一個星期五。
  • # :用於確定每個月第幾個星期幾,只能出現在DayofMonth域。例如在4#2,表示某月的第二個星期三。

corn表達式例子:

cron表達式 描述
0 0 10,15,16 * * ? 每天上午10點,下午3點,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 * * ? 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觸發

corn表達式正則校驗

String regEx = "(((^([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]))|^([0-9]|[0-5][0-9])|^(\\* ))((([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|([0-9]|[0-5][0-9]) |(\\* ))((([0-9]|[01][0-9]|2[0-3])(\\,|\\-|\\/){1}([0-9]|[01][0-9]|2[0-3]) )|([0-9]|[01][0-9]|2[0-3]) |(\\* ))((([0-9]|[0-2][0-9]|3[01])(\\,|\\-|\\/){1}([0-9]|[0-2][0-9]|3[01]) )|(([0-9]|[0-2][0-9]|3[01]) )|(\\? )|(\\* )|(([1-9]|[0-2][0-9]|3[01])L )|([1-7]W )|(LW )|([1-7]\\#[1-4] ))((([1-9]|0[1-9]|1[0-2])(\\,|\\-|\\/){1}([1-9]|0[1-9]|1[0-2]) )|([1-9]|0[1-9]|1[0-2]) |(\\* ))(([1-7](\\,|\\-|\\/){1}[1-7])|([1-7])|(\\?)|(\\*)|(([1-7]L)|([1-7]\\#[1-4]))))|(((^([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|^([0-9]|[0-5][0-9]) |^(\\* ))((([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|([0-9]|[0-5][0-9]) |(\\* ))((([0-9]|[01][0-9]|2[0-3])(\\,|\\-|\\/){1}([0-9]|[01][0-9]|2[0-3]) )|([0-9]|[01][0-9]|2[0-3]) |(\\* ))((([0-9]|[0-2][0-9]|3[01])(\\,|\\-|\\/){1}([0-9]|[0-2][0-9]|3[01]) )|(([0-9]|[0-2][0-9]|3[01]) )|(\\? )|(\\* )|(([1-9]|[0-2][0-9]|3[01])L )|([1-7]W )|(LW )|([1-7]\\#[1-4] ))((([1-9]|0[1-9]|1[0-2])(\\,|\\-|\\/){1}([1-9]|0[1-9]|1[0-2]) )|([1-9]|0[1-9]|1[0-2]) |(\\* ))(([1-7](\\,|\\-|\\/){1}[1-7] )|([1-7] )|(\\? )|(\\* )|(([1-7]L )|([1-7]\\#[1-4]) ))((19[789][0-9]|20[0-9][0-9])\\-(19[789][0-9]|20[0-9][0-9])))";
String tests = "0 0 0 L * ?";
System.out.println(tests.matches(regEx));

corn表達式CronExpression校驗

這個方法需要引入quartz的jar包,然後只需要一行代碼就可以驗證了。

CronExpression.isValidExpression(cron)

@Scheduled註解的fixedRate屬性說明

fixedDelay是設定上一個任務結束後多久執行下一個任務,也就是fixedDelay只關心上一任務的結束時間和下一任務的開始時間。

@Scheduled(fixedDelay = 5*1_000)
public void doTask() throws InterruptedException {
    logger.info(Thread.currentThread().getName()+"===task run");
    Thread.sleep(6*1_000);
    logger.info(Thread.currentThread().getName()+"===task end");
}

@Scheduled註解的fixedDelay屬性說明

fixedRate設置的上一個任務的開始時間到下一個任務開始時間的間隔。

說明: 

  1. fixedRate配置了上一次任務的開始時間下一次任務的開始時間的間隔,每次任務都會執行;
  2. fixedDelay配置了上一次任務的結束時間下一次任務的開始時間的間隔,每次任務都會執行;
  3. cron表達式配置了在哪一刻執行任務,會在配置的任務開始時間判斷任務是否可以執行,如果能則執行,不能則會跳過本次執行;
  4. 如果是強調任務間隔的定時任務,建議使用fixedRate和fixedDelay,如果是強調任務在某時某分某刻執行的定時任務,建議使用cron表達式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章