SpringBoot 動態可配定時任務(動態定時任務)


本文主要介紹了SpringBoot架構下動態定時任務的使用,定時任務表達式配置在數據庫中,其它項目架構也可以借鑒,實現思路是一樣的。
支持查看任務狀態,動態修改任務時間,停止任務等;

1.前置准備

1.1創建任務表

CREATE TABLE `scheduled_task` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `task_key` varchar(128) NOT NULL COMMENT '任務key值(使用bean名稱)',
  `task_desc` varchar(128) DEFAULT NULL COMMENT '任務描述',
  `task_cron` varchar(128) NOT NULL COMMENT '任務表達式',
  `init_start_flag` int(2) NOT NULL DEFAULT '1' COMMENT '程序初始化是否啟動 1 是 0 否',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniqu_task_key` (`task_key`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

1.2添加初始化測試數據

INSERT INTO `cron`.`scheduled_task`(`id`, `task_key`, `task_desc`, `task_cron`, `init_start_flag`, `create_time`, `update_time`) VALUES (1, 'scheduledTask01', '定時任務01', '0/5 * * * * ?', 1, NOW(), NOW());
INSERT INTO `cron`.`scheduled_task`(`id`, `task_key`, `task_desc`, `task_cron`, `init_start_flag`, `create_time`, `update_time`) VALUES (2, 'scheduledTask02', '定時任務02', '0/2 * * * * ?', 0, NOW(), NOW());
INSERT INTO `cron`.`scheduled_task`(`id`, `task_key`, `task_desc`, `task_cron`, `init_start_flag`, `create_time`, `update_time`) VALUES (3, 'scheduledTask03', '定時任務03', '0/2 * * * * ?', 1, NOW(), NOW());

1.3創建Maven項目並添加Maven依賴

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.12.RELEASE</version>
    </parent>

    <!--版本號 -->
    <properties>
        <fastjson.version>1.2.47</fastjson.version>
        <mybatis-spring-boot.version>1.3.1</mybatis-spring-boot.version>
        <mybatis-spring-boot-starter.version>1.3.0</mybatis-spring-boot-starter.version>
        <mybatis-plus.version>2.1-gamma</mybatis-plus.version>
        <druid.version>1.1.2</druid.version>
    </properties>
    <dependencies>
        <!--依賴管理 -->
        <dependency><!--添加Web依賴 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency><!--添加Mybatis依賴 -->
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot.version}</version>
        </dependency>
        <dependency><!--添加MySql依賴 -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency><!--添加Test依賴 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- json 工具 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
    </dependencies>

1.4添加配置文件application.properties

server.port=8081
spring.datasource.url:jdbc:mysql://localhost:3306/cron?useSSL=false
spring.datasource.username:root
spring.datasource.password:root

1.5添加SpringBoot啟動類

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

2.任務調度

2.1創建定時任務線程池,初始化任務Map

       注:任務map,后續使用具體講解

import com.scheduled.dynamic.enums.ScheduledTaskEnum;
import com.scheduled.dynamic.service.ScheduledTaskJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.util.Map;
@Configuration
public class ScheduledTaskConfig {

    /** * 日志 */
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTaskConfig.class);

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        LOGGER.info("創建定時任務調度線程池 start");
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(20);
        threadPoolTaskScheduler.setThreadNamePrefix("taskExecutor-");
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        LOGGER.info("創建定時任務調度線程池 end");
        return threadPoolTaskScheduler;
    }

    /** * 初始化定時任務Map * key :任務key * value : 執行接口實現 */
    @Bean(name = "scheduledTaskJobMap")
    public Map<String, ScheduledTaskJob> scheduledTaskJobMap() {
        return ScheduledTaskEnum.initScheduledTask();
    }

}

2.2創建任務實體Bean

public class ScheduledTaskBean {

    /** * 任務key值 唯一 */
    private String taskKey;
    /** * 任務描述 */
    private String taskDesc;
    /** * 任務表達式 */
    private String taskCron;

    /** * 程序初始化是否啟動 1 是 0 否 */
    private Integer initStartFlag;

    /** * 當前是否已啟動 */
    private boolean startFlag;
    //省略 get/set

2.3Mapper接口

/** * 定時任務表 mapper * demo 項目未使用 xml方式,使用注解方式查詢數據以便演示 */
@Mapper
public interface ScheduledTaskMapper {

    /** * 根據key 獲取 任務信息 */
    @Select("select task_key as taskKey,task_desc as taskDesc,task_cron as taskCron,init_start_flag as initStartFlag from scheduled_task where task_key = '${taskKey}' ")
    ScheduledTaskBean getByKey(@Param("taskKey") String taskKey);

    /** * 獲取程序初始化需要自啟的任務信息 */
    @Select("select task_key as taskKey,task_desc as taskDesc,task_cron as taskCron,init_start_flag as initStartFlag from scheduled_task where init_start_flag=1 ")
    List<ScheduledTaskBean> getAllNeedStartTask();

    /** * 獲取所有任務 */
    @Select("select task_key as taskKey,task_desc as taskDesc,task_cron as taskCron,init_start_flag as initStartFlag from scheduled_task ")
    List<ScheduledTaskBean> getAllTask();
}

2.4創建調度任務公共父接口

/** * 調度任務公共父接口 */
public interface ScheduledTaskJob extends Runnable {
}

2.5實現父接口,重寫run方法

注:ScheduledTask02與ScheduledTask03與當前ScheduledTask01 一致,不再重復寫,復制幾個即可

import com.scheduled.dynamic.service.ScheduledTaskJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** * 任務 01 */
public class ScheduledTask01 implements ScheduledTaskJob {
    /** * 日志 */
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask01.class);

    @Override
    public void run() {
        LOGGER.info("ScheduledTask => 01 run 當前線程名稱 {} ", Thread.currentThread().getName());
    }
}

2.6創建定時任務枚舉類

注:方便管理任務

import com.scheduled.dynamic.service.ScheduledTaskJob;
import com.scheduled.dynamic.task.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/** * 定時任務枚舉值 * 注:key 需要與數據庫保持一致 */
public enum ScheduledTaskEnum {

    /** * 任務1 */
    TASK_01("scheduledTask01", new ScheduledTask01()),
    /** * 任務2 */
    TASK_02("scheduledTask02", new ScheduledTask02()),
    /** * 任務3 */
    TASK_03("scheduledTask03", new ScheduledTask03()),
    ;
    /** * 定時任務key */
    private String taskKey;
    /** * 定時任務 執行實現類 */
    private ScheduledTaskJob scheduledTaskJob;

    ScheduledTaskEnum(String taskKey, ScheduledTaskJob scheduledTaskJob) {
        this.taskKey = taskKey;
        this.scheduledTaskJob = scheduledTaskJob;
    }

    /** * 初始化 所有任務 */
    public static Map<String, ScheduledTaskJob> initScheduledTask() {
        if (ScheduledTaskEnum.values().length < 0) {
            return new ConcurrentHashMap<>();
        }
        Map<String, ScheduledTaskJob> scheduledTaskJobMap = new ConcurrentHashMap<>();
        for (ScheduledTaskEnum taskEnum : ScheduledTaskEnum.values()) {
            scheduledTaskJobMap.put(taskEnum.taskKey, taskEnum.scheduledTaskJob);
        }
        return scheduledTaskJobMap;
    }
}

2.7功能核心接口

/** * 定時任務接口 */
public interface ScheduledTaskService {

    /** * 所有任務列表 */
    List<ScheduledTaskBean> taskList();

    /** * 根據任務key 啟動任務 */
    Boolean start(String taskKey);

    /** * 根據任務key 停止任務 */
    Boolean stop(String taskKey);

    /** * 根據任務key 重啟任務 */
    Boolean restart(String taskKey);


    /** * 程序啟動時初始化 ==> 啟動所有正常狀態的任務 */
    void initAllTask(List<ScheduledTaskBean> scheduledTaskBeanList);

}
/** * 定時任務實現 */
@Service
public class ScheduledTaskServiceImpl implements ScheduledTaskService {
    /** * 日志 */
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTaskServiceImpl.class);

    @Autowired
    private ScheduledTaskMapper taskMapper;
    /** * 可重入鎖 */
    private ReentrantLock lock = new ReentrantLock();
    /** * 定時任務線程池 */
    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    /** * 所有定時任務存放Map * key :任務 key * value:任務實現 */
    @Autowired
    @Qualifier(value = "scheduledTaskJobMap")
    private Map<String, ScheduledTaskJob> scheduledTaskJobMap;

    /** * 存放已經啟動的任務map */
    private Map<String, ScheduledFuture> scheduledFutureMap = new ConcurrentHashMap<>();

    /** * 所有任務列表 */
    @Override
    public List<ScheduledTaskBean> taskList() {
        LOGGER.info(">>>>>> 獲取任務列表開始 >>>>>> ");
        //數據庫查詢所有任務 => 未做分頁
        List<ScheduledTaskBean> taskBeanList = taskMapper.getAllTask();
        if (CollectionUtils.isEmpty(taskBeanList)) {
            return new ArrayList<>();
        }

        for (ScheduledTaskBean taskBean : taskBeanList) {
            String taskKey = taskBean.getTaskKey();
            //是否啟動標記處理
            taskBean.setStartFlag(this.isStart(taskKey));
        }
        LOGGER.info(">>>>>> 獲取任務列表結束 >>>>>> ");
        return taskBeanList;
    }


    /** * 根據任務key 啟動任務 */
    @Override
    public Boolean start(String taskKey) {
        LOGGER.info(">>>>>> 啟動任務 {} 開始 >>>>>>", taskKey);
        //添加鎖放一個線程啟動,防止多人啟動多次
        lock.lock();
        LOGGER.info(">>>>>> 添加任務啟動鎖完畢");
        try {
            //校驗是否已經啟動
            if (this.isStart(taskKey)) {
                LOGGER.info(">>>>>> 當前任務已經啟動,無需重復啟動!");
                return false;
            }
            //校驗任務是否存在
            if (!scheduledTaskJobMap.containsKey(taskKey)) {
                return false;
            }
            //根據key數據庫獲取任務配置信息
            ScheduledTaskBean scheduledTask = taskMapper.getByKey(taskKey);
            //啟動任務
            this.doStartTask(scheduledTask);
        } finally {
            // 釋放鎖
            lock.unlock();
            LOGGER.info(">>>>>> 釋放任務啟動鎖完畢");
        }
        LOGGER.info(">>>>>> 啟動任務 {} 結束 >>>>>>", taskKey);
        return true;
    }

    /** * 根據 key 停止任務 */
    @Override
    public Boolean stop(String taskKey) {
        LOGGER.info(">>>>>> 進入停止任務 {} >>>>>>", taskKey);
        //當前任務實例是否存在
        boolean taskStartFlag = scheduledFutureMap.containsKey(taskKey);
        LOGGER.info(">>>>>> 當前任務實例是否存在 {}", taskStartFlag);
        if (taskStartFlag) {
            //獲取任務實例
            ScheduledFuture scheduledFuture = scheduledFutureMap.get(taskKey);
            //關閉實例
            scheduledFuture.cancel(true);
        }
        LOGGER.info(">>>>>> 結束停止任務 {} >>>>>>", taskKey);
        return taskStartFlag;
    }

    /** * 根據任務key 重啟任務 */
    @Override
    public Boolean restart(String taskKey) {
        LOGGER.info(">>>>>> 進入重啟任務 {} >>>>>>", taskKey);
        //先停止
        this.stop(taskKey);
        //再啟動
        return this.start(taskKey);
    }

    /** * 程序啟動時初始化 ==> 啟動所有正常狀態的任務 */
    @Override
    public void initAllTask(List<ScheduledTaskBean> scheduledTaskBeanList) {
        LOGGER.info("程序啟動 ==> 初始化所有任務開始 !size={}", scheduledTaskBeanList.size());
        if (CollectionUtils.isEmpty(scheduledTaskBeanList)) {
            return;
        }
        for (ScheduledTaskBean scheduledTask : scheduledTaskBeanList) {
            //任務 key
            String taskKey = scheduledTask.getTaskKey();
            //校驗是否已經啟動
            if (this.isStart(taskKey)) {
                continue;
            }
            //啟動任務
            this.doStartTask(scheduledTask);
        }
        LOGGER.info("程序啟動 ==> 初始化所有任務結束 !size={}", scheduledTaskBeanList.size());
    }

    /** * 執行啟動任務 */
    private void doStartTask(ScheduledTaskBean scheduledTask) {
        //任務key
        String taskKey = scheduledTask.getTaskKey();
        //定時表達式
        String taskCron = scheduledTask.getTaskCron();
        //獲取需要定時調度的接口
        ScheduledTaskJob scheduledTaskJob = scheduledTaskJobMap.get(taskKey);
        LOGGER.info(">>>>>> 任務 [ {} ] ,cron={}", scheduledTask.getTaskDesc(), taskCron);
        ScheduledFuture scheduledFuture = threadPoolTaskScheduler.schedule(scheduledTaskJob,
                new Trigger() {
                    @Override
                    public Date nextExecutionTime(TriggerContext triggerContext) {
                        CronTrigger cronTrigger = new CronTrigger(taskCron);
                        return cronTrigger.nextExecutionTime(triggerContext);
                    }
                });
        //將啟動的任務放入 map
        scheduledFutureMap.put(taskKey, scheduledFuture);
    }

    /** * 任務是否已經啟動 */
    private Boolean isStart(String taskKey) {
        //校驗是否已經啟動
        if (scheduledFutureMap.containsKey(taskKey)) {
            if (!scheduledFutureMap.get(taskKey).isCancelled()) {
                return true;
            }
        }
        return false;
    }

}

2.8 定時任務Controller

/** * 定時任務 controller */
@RestController
@RequestMapping("/scheduled")
public class ScheduledController {
    @Autowired
    private ScheduledTaskService scheduledTaskService;
    /** * 所有任務列表 */
    @RequestMapping("/taskList")
    public List<ScheduledTaskBean> taskList() {
        return scheduledTaskService.taskList();
    }

    /** * 根據任務key => 啟動任務 */
    @RequestMapping("/start")
    public String start(@RequestParam("taskKey") String taskKey) {
        scheduledTaskService.start(taskKey);
        return "start success";
    }

    /** * 根據任務key => 停止任務 */
    @RequestMapping("/stop")
    public String stop(@RequestParam("taskKey") String taskKey) {
        scheduledTaskService.stop(taskKey);
        return "stop success";
    }

    /** * 根據任務key => 重啟任務 */
    @RequestMapping("/restart")
    public String restart(@RequestParam("taskKey") String taskKey) {
        scheduledTaskService.restart(taskKey);
        return "restart success";
    }

}

效果圖示
在這里插入圖片描述
在這里插入圖片描述

截止到這來,已經可以實現,動態管理任務了,包括動態修改啟動表達式,停止任務,啟動任務,重啟任務等。
但是,還有一個問題,項目重啟后,需要人工來啟動需要啟動的任務。
。。。

2.9實現項目啟動時自啟動

注:SpringBoot給我們提供了兩個接口來幫助我們實現這種需求。這兩個接口分別為CommandLineRunner和ApplicationRunner。他們的執行時機為容器啟動完成的時候。

/** * @see @Order注解的執行優先級是按value值從小到大順序。 * 項目啟動完畢后開啟需要自啟的任務 */
@Component
@Order(value = 1)
public class ScheduledTaskRunner implements ApplicationRunner {
    /** * 日志 */
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTaskRunner.class);

    @Autowired
    private ScheduledTaskMapper taskMapper;

    @Autowired
    private ScheduledTaskService scheduledTaskService;
    
    /** * 程序啟動完畢后,需要自啟的任務 */
    @Override
    public void run(ApplicationArguments applicationArguments) throws Exception {
        LOGGER.info(" >>>>>> 項目啟動完畢, 開啟 => 需要自啟的任務 開始!");
        List<ScheduledTaskBean> scheduledTaskBeanList = taskMapper.getAllNeedStartTask();
        scheduledTaskService.initAllTask(scheduledTaskBeanList);
        LOGGER.info(" >>>>>> 項目啟動完畢, 開啟 => 需要自啟的任務 結束!");
    }
}

到這里,動態定時任務已經很完美了

總結

優點:動態啟動任務,修改任務間隔時間
缺點:分布式多機部署,也只能實現對某一台機器的任務做動態處理,因為啟動的定時任務,放在當前機器內存中了,歡迎大家一起優化!
在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM