基于SpringBoot實現輕量級的動態定時任務調度

在使用SpringBoot框架進行開發時,一般都是通過@Scheduled注解進行定時任務的開發:


@Component
public class TestTask
{@Scheduled(cron="0/5 * *  * * ? ")   //每5秒執行一次public void execute(){SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); log.info("任務執行" + df.format(new Date()));}
}

但是這種方式存在一個問題,那就是任務的周期控制是死的,必須編寫在代碼中,如果遇到需要在系統運行過程中想中止、立即執行、修改執行周期等動態操作的需求時,使用注解的方式便不能滿足了,當然為了滿足此種需求可以額外再引入其他任務調度插件(例如XXL-Job等),但是引入其他組件是需要衡量成本的,額外的依賴成本、組件的維護成本、開發的復雜度等等,所以如果系統體量不是那么大,完全沒必要通過增加組件來完成,可以基于SpringBoot框架實現一套內置輕量級的任務調度。

設計思路

整體設計

這里我們把定時任務以類作為基礎單位,即一個類為一個任務,然后通過配置數據的方式,進行任務的讀取,通過反射生成任務對象,使用SpringBoot本身的線程池任務調度,完成動態的定時任務驅動,同時通過接口支撐實現相應的REST API對外暴露接口

任務模型

首先基于模板模式,設計基礎的任務執行流程抽象類,定義出一個定時任務需要執行的內容和步驟和一些通用的方法函數,后續具體的定時任務直接繼承該父類,實現該父類的before、start、after三個抽象函數即可,所有公共操作均在抽象父類完成

特殊說明:

? ? 基于此方法創建的類是不歸Spring的容器管理的,所以自定義的任務子類中是無法使用SpringBoot中的任何注解,尤其在自定義任務類中如果需要依賴其他Bean時,需要借助抽象父類AbstractBaseCronTask中已經實現的<T> T getServer(Class<T> className)來完成,getServer的實現如下:

public <T> T getServer(Class<T> className){return applicationContext.getBean(className);}

是通過SpringBoot中的ApplicationContext接口來獲取Spring的上下文,以此來滿足可以獲取Spring中其他Bean的訴求。

例如,有個定時任務TaskOne類,它需要使用UserService類中的 caculateMoney()的方法,勢必這個定時任務需要依賴UserService類,而TaskOne并非是Spring創建的對象,而是我們人為干預生成的對象,所以它是不在Spring的Bean管理范圍的,自然也就無法使用@Autowird等方式注入UserService類,此時就需要使用getServer方法來獲取UserService對象

//自定義定時任務類
public class TaskOne extends AbstractBaseCronTask {private UserService userService;public TestTask(TaskEntity taskEntity) {super(taskEntity);}@Overridepublic void beforeJob() {//任務運行第一步,先將userService進行變量注入userService = getServer(UserService.class);……}@Overridepublic void startJob() {if(XXXX){//直接調用getServer獲取需要的beanUser user = getServer(UserMapper.class).findUser("111223")userService.caluateMoney(user);//……其他代碼}}@Overridepublic void afterJob() {}
}

任務對象加載過程

?核心邏輯在于利用反射,在SpringBoot啟動后動態創建相應的定時任務類,并將其放置到SpringBoot的定時線程池中進行維護,同時將該對象同步存放至內存中一份,便于可以實時調用,當進行修改任務相關配置時,需要重新加載一次內容。

public class TaskScheduleServerImpl implements TaskScheduleServer {//正在運行的任務private static ConcurrentHashMap<String, ScheduledFuture> runningTasks = new ConcurrentHashMap<>();//線程池任務調度private ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();public boolean addTaskToScheduling(TaskEntity task) {if(!runningTasks.containsKey(task.getTaskId())){try{Class<?> clazz = Class.forName(task.getTaskClass());Constructor c = clazz.getConstructor(TaskEntity.class);AbstractBaseCronTask runnable = (AbstractBaseCronTask) c.newInstance(task);//反射方式生成對象不屬于Spring容器管控,對于Spring的bean使用需要手動注入runnable.setApplicationContext(context);CronTrigger cron = new CronTrigger(task.getTaskCron());//put到runTasksrunningTasks.put(task.getTaskId(), Objects.requireNonNull(this.threadPoolTaskScheduler.schedule(runnable, cron)));//存入內存中,便于外部調用ramTasks.put(task.getTaskId(),runnable);task.setTaskRamStatus(1);taskInfoOpMapper.updateTaskInfo(task);return true;}catch (Exception e){log.error("定時任務加載失敗..."+e);}}return false;}
}

部分源碼

這里將配置內容放入數據庫中,直接以數據庫中的表作為任務配置的基礎

/**
* 任務對象
**/
@Data
public class TaskEntity implements Serializable {//任務唯一IDprivate String taskId;//任務名稱private String taskName;//任務描述private String taskDesc;//執行周期配置private String taskCron;//任務類的全路徑private String taskClass;//任務的額外配置private String taskOutConfig;//任務創建時間private String taskCreateTime;//任務是否啟動,1啟用,0不啟用private Integer taskIsUse;//是否隨系統啟動立即執行private Integer taskBootUp;//任務上次執行狀態private Integer taskLastRun;//任務是否加載至內存中 private Integer taskRamStatus;
}

核心邏輯,加載定時任務接口及其實現類

public interface TaskScheduleServer {ConcurrentHashMap<String, AbstractBaseCronTask> getTaskSchedulingRam();/*** 初始化任務調度*/void initScheduling();/*** 添加任務至內存及容器* @param taskEntity 任務實體* @return boolean*/boolean addTaskToScheduling(TaskEntity taskEntity);/*** 從任務調度器中移除任務* @param id 任務id* @return Boolean*/boolean removeTaskFromScheduling(String id);/*** 執行指定任務* @param id 任務id* @return double 耗時*/double runTaskById(String id);/*** 清空任務*/void claearAllTask();/*** 加載所有任務*/void loadAllTask();/*** 運行開機自啟任務*/void runBootUpTask();}@Slf4j
@Component
public class TaskScheduleServerImpl implements TaskScheduleServer {…………@Overridepublic double runTaskById(String id) {TaskEntity task = taskInfoOpMapper.queryTaskInfoById(id);if(null!=task) {if (runningTasks.containsKey(task.getTaskId())){ramTasks.get(task.getTaskId()).run();return ramTasks.get(task.getTaskId()).getRunTime();}}return 0d;}@Overridepublic void claearAllTask() {ramTasks.clear();log.info("【定時任務控制器】清除內存任務 完成");runningTasks.clear();log.info("【定時任務控制器】清除線程任務 完成");threadPoolTaskScheduler.shutdown();}@Overridepublic void loadAllTask() {List<TaskEntity> allTask = taskInfoOpMapper.queryTaskInfo(null);for (TaskEntity task : allTask) {if(addTaskToScheduling(task)){log.info("【定時任務初始化】裝填任務:{} [ 任務執行周期:{} ] [ bootup:{}]",task.getTaskName(),task.getTaskCron(),task.getTaskBootUp());}}}@Overridepublic void runBootUpTask() {TaskEntity entity = new TaskEntity().taskBootUp(1);List<TaskEntity> list = taskInfoOpMapper.queryTaskInfo(entity);for(TaskEntity task:list){runTaskById(task.getTaskId());}}
}

在SpringBoot中的加載類

@Order(3)
@Component
@Slf4j
public class AfterAppStarted implements ApplicationRunner {TaskScheduleServer taskScheduleServer;@Autowiredpublic void setTaskScheduleServer(TaskScheduleServer taskScheduleServer) {this.taskScheduleServer = taskScheduleServer;}@Overridepublic void run(ApplicationArguments args) throws Exception {//運行隨系統啟動的定時任務taskScheduleServer.runBootUpTask();}}

對外暴露控制接口及其Service

@RestController
@RequestMapping("/taskScheduling/manage")
@Api(tags = "數據源管理服務")
public class TaskSchedulingController {TaskScheduleManagerService taskScheduleManagerService;@Autowiredpublic void setTaskScheduleManagerService(TaskScheduleManagerService taskScheduleManagerService) {this.taskScheduleManagerService = taskScheduleManagerService;}@PostMapping("/search")@Operation(summary = "分頁查詢任務")public Response searchData(@RequestBody SearchTaskDto param){return Response.success(taskScheduleManagerService.searchTaskForPage(param));}@GetMapping("/detail")@Operation(summary = "具體任務對象")public Response searchDetail(String taskId){return Response.success(taskScheduleManagerService.searchTaskDetail(taskId));}@GetMapping("/shutdown")@Operation(summary = "關閉指定任務")public Response shutdownTask(String taskId){return Response.success(taskScheduleManagerService.shutdownTask(taskId));}@GetMapping("/open")@Operation(summary = "開啟指定任務")public Response openTask(String taskId){return Response.success(taskScheduleManagerService.openTask(taskId));}@GetMapping("/run")@Operation(summary = "運行指定任務")public  Response runTask(String taskId){return Response.success(taskScheduleManagerService.runTask(taskId));}@PostMapping("/update")@Operation(summary = "更新指定任務")public Response updateTask(@RequestBody TaskEntity taskEntity){return Response.success(taskScheduleManagerService.updateTaskBusinessInfo(taskEntity));}}

相關接口實現類

@Service
public class TaskScheduleManagerServiceImpl implements TaskScheduleManagerService {private TaskInfoOpMapper taskInfoOpMapper;private TaskScheduleServer taskScheduleServer;@Autowiredpublic void setTaskInfoOpMapper(TaskInfoOpMapper taskInfoOpMapper) {this.taskInfoOpMapper = taskInfoOpMapper;}@Autowiredpublic void setTaskScheduleServer(TaskScheduleServer taskScheduleServer) {this.taskScheduleServer = taskScheduleServer;}@Overridepublic IPage<TaskEntity> searchTaskForPage(SearchTaskDto dto) {Page<TaskEntity> pageParam = new Page<>(1,10);pageParam.setAsc("task_id");return taskInfoOpMapper.queryTaskInfoPage(pageParam,dto.getFilterKey(),dto.getBootUp(),dto.getLastRunStatus());}@Overridepublic TaskEntity searchTaskDetail(String taskId) {if(!StringUtils.isEmpty(taskId)){return taskInfoOpMapper.queryTaskInfoById(taskId);}return null;}@Overridepublic TaskRunRetDto runTask(String taskId) {AbstractBaseCronTask task = taskScheduleServer.getTaskSchedulingRam().get(taskId);TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.run, 0);if(null != task) {double time = taskScheduleServer.runTaskById(taskId);result.setResult(1);return result.extend(time).taskInfo(task.getThisTaskInfo());} else {return result.extend("任務未啟用");}}@Overridepublic TaskRunRetDto shutdownTask(String taskId) {AbstractBaseCronTask task = taskScheduleServer.getTaskSchedulingRam().get(taskId);TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.shutdown, 0);if(null != task) {boolean flag = taskScheduleServer.removeTaskFromScheduling(taskId);if(flag) {result.setResult(1);}return result.extend("任務成功關閉").taskInfo(task.getThisTaskInfo());} else {return result.extend("任務未啟用");}}@Overridepublic TaskRunRetDto openTask(String taskId) {TaskEntity task = taskInfoOpMapper.queryTaskInfoById(taskId);TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.open, 0);if(null != task) {if (!taskScheduleServer.getTaskSchedulingRam().containsKey(taskId)) {boolean flag = taskScheduleServer.addTaskToScheduling(task);if(flag) {result.setResult(1);}return result.extend("任務開啟成功").taskInfo(task);} else {return result.extend("任務處于啟動狀態").taskInfo(task);}}else {return result.extend("任務不存在!");}}@Overridepublic TaskRunRetDto updateTaskBusinessInfo(TaskEntity entity) {TaskEntity task = searchTaskDetail(entity.getTaskId());TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.update, 0).taskInfo(entity);String config = entity.getTaskOutConfig();if(null != config && !JSONUtil.isJson(config) && !JSONUtil.isJsonArray(config)){result.setResult(0);result.extend("更新任務失敗,任務配置必須為JSON或空");result.taskInfo(entity);return result;}task.setTaskCron(entity.getTaskCron());task.setTaskOutConfig(entity.getTaskOutConfig());task.setTaskName(entity.getTaskName());task.setTaskDesc(entity.getTaskDesc());int num = taskInfoOpMapper.updateTaskInfo(task);if (num == 1) {result.setResult(1);result.extend("成功更新任務");result.taskInfo(entity);//重新刷新任務taskScheduleServer.removeTaskFromScheduling(entity.getTaskId());taskScheduleServer.addTaskToScheduling(task);}return result;}

效果

數據庫中配置任務

任務代碼

public class TestTask extends AbstractBaseCronTask {public TestTask(TaskEntity taskEntity) {super(taskEntity);}@Overridepublic void beforeJob() {log.info("測試任務開始");}@Overridepublic void startJob() {}@Overridepublic void afterJob() {}
}

任務查看

執行效果

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/43644.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/43644.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/43644.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

解決 NullReferenceException: Object reference not set to an instance of an object

在 Unity 中 利用 URDF Importer import UR5e_gripper 的 URDF file 時出現錯誤&#xff1a; NullReferenceException: Object reference not set to an instance of an object。 理論上是有個Object 是 Null&#xff0c;當我再次檢查URDF后仍覺得路徑沒有寫錯。 于是我 把Mesh…

軟件測試面試200問(含答案+文檔)

Part1 1、你的測試職業發展是什么&#xff1f; 測試經驗越多&#xff0c;測試能力越高。所以我的職業發展是需要時間積累的&#xff0c;一步步向著高級測試工程師奔去。而且我也有初步的職業規劃&#xff0c;前3年積累測試經驗&#xff0c;按如何做好測試工程師的要點去要求自…

spring的bean注冊

bean注冊 第三方jar包的類想添加到ioc中&#xff0c;加不了Component該怎么辦呢。 可以使用Bean和Import引入jar包&#xff0c;可以使用maven安裝到本地倉庫。 修改bean的名字&#xff1a;Bean("aaa")使用ioc的已經存在的bean對象&#xff0c;如Country&#xff1a;p…

in-flight 要維持在 bdp 附近嗎

試圖在 bbr 和 aimd 之間保持公平是徒勞的&#xff0c;因為它們沒有共識。bbr 認為 in-flight 超過 bdp 是擁塞&#xff0c;而 aimd 認為 buffer 溢出才擁塞&#xff0c;兼顧彼此&#xff0c;就是 bbr3&#xff0c;aimd 不會往左&#xff0c;bbr 就往右。 vegas 同理&#xff…

自定義@AnonymousAccess注解

一.目的&#xff1a; 自定義AnonymousAccess注解&#xff0c;可以直接在controller上添加該注解使請求繞過權限驗證進行匿名訪問&#xff0c;便于快速調用調試以及部分不需要進行安全驗證的接口。而不是每次都需要去SecurityConfig文件中進行修改。 二.流程&#xff1a; 三.實…

通用后臺管理(二)——項目搭建

目錄 前言 一、安裝vue-cli依賴 1、使用yarn下載vue-cli 2、使用npm下載 3、檢查一下是否下載成功 二、創建項目 1、創建項目&#xff0c;my-app是項目名稱 2、 這里選擇vue 2&#xff0c;藍色表示選中的。 3、啟動項目 三、下載項目依賴 四、配置項目 1、修改esli…

VS2019_新建窗體應用文件SOP

文章目錄 1. 打開VS2. 創建新項目3. 選擇Windows窗體應用4. 起名&#xff0c;選位置&#xff0c;框架版本選擇4.5及之后&#xff0c;這里選的4.85. 新建完成 1. 打開VS 2. 創建新項目 3. 選擇Windows窗體應用 注意要選.NET的 4. 起名&#xff0c;選位置&#xff0c;框架版本…

ps aux命令詳解

ps aux 是在 Unix 和 Linux 系統中常用的命令&#xff0c;用于顯示當前系統上運行的所有進程的信息。它結合了多個選項來提供詳細的進程列表。以下是這個命令的詳細解釋&#xff1a; 解釋 ps aux 命令 ps&#xff1a;這是顯示當前系統進程的基本命令。a&#xff1a;顯示與終端…

原來Kimi不是不作為,而是在準備大招!

月之暗面Kimi 作為一款我每天都在使用的AI智能助手&#xff0c;是真正的幫助我解決了不少工作及日常創作的一些事情。 它的表現能力也是毋庸置疑&#xff0c;不論是業內還是普通人&#xff0c;10個人當中可能就有9個人在使用Kimi。 而昨天&#xff08;7月8日&#xff09;Kimi…

Civil3D 2025:工程設計的智慧之友

初識Civil3D 2025 當我第一次打開Civil3D 2025時&#xff0c;仿佛置身于湘西的山水之間&#xff0c;那種熟悉而又新奇的感覺撲面而來。Civil3D 2025不僅僅是一款軟件&#xff0c;它更像是一位老友&#xff0c;帶著溫暖和智慧&#xff0c;陪伴我走過每一個設計的日夜。 功能豐…

侯捷C++面向對象高級編程(上)-9-擴展補充:類模板、函數模板及其他

1.static 2.把 ctors 放在 private 區 3.cout 4.class template、類模板 5.function template、函數模板 6.namespace

DDR3 SO-DIMM 內存條硬件總結(二)

上一節我們大概了解了下DDR3的大概使用情況&#xff0c;下來我們再順藤摸瓜再深入學習下ddr3的使用&#xff0c;我們從下面這張內存條開始&#xff1a; 我們看到第二行2GB 1RX8 PC3-10600S-9-11-B2 具體含義上一節已經講過了&#xff0c;我們深挖下1R*8&#xff0c;他的含義是…

資料分析筆記整理

提升技巧多做題、少動筆、多分析 資料分析認識 國考一般20題(24~28分鐘) 統計材料的類型包括單純的文字、表格、圖形以及由這些元素組成的復合類型材料 文字性材料:(30~60秒) 多段落型文字材料(時間、關鍵詞、結構) 孤立段落文字材料(時間、關鍵詞、標點[。;]) 表…

Java語言程序設計——篇二(2)

Java語言基礎 運算符與表達式運算符1、算術運算符2、關系運算符3、邏輯運算符&#xff08; &&、||、 !、&、| 、^&#xff09;4、位運算符&#xff08; >>、<<、>>>、&、|、^、~&#xff09;5、賦值運算符6、條件運算符7、字符串運算符8、…

數字化時代下,財務共享數據分析建設之路

隨著人工智能、云計算、大數據、區塊鏈等技術&#xff0c;以及衍生出的各種產品的大發展&#xff0c;使得數字化發展的速度再一次加快&#xff0c;也讓數字經濟和數字化轉型得到了更多人的關注和認可。 在傳統經濟增長逐漸放緩&#xff0c;市場競爭愈發激烈的局面下&#xff0…

GOJS去除水印

GOJS gojs 去除水印 **查找go.js庫搜索下面這段文本 String.fromCharCode(a.charCodeAt(g)^b[(b[c]b[d])%256]) 加入這段文本 if(f.indexOf(GoJS 2.1 evaluation)>-1|| f.indexOf(© 1998-2021 Northwoods Software)>-1|| f.indexOf(Not for distribution or produ…

splice方法的使用#Vue3

splice方法的使用 splice(index,len,item)是vue中對數組進行操作的方法之一&#xff0c;可以用來刪除&#xff0c;更新&#xff0c;和增加數組內容 參數: index:數組下標 len&#xff1a;為1或0 item:更新或增加的內容 使用方法&#xff1a; 刪除&#xff0c;當參數形式為spli…

Linux內核網絡性能問題的追蹤(工具篇)

在現代計算環境中&#xff0c;網絡性能對于系統的整體性能至關重要。網絡瓶頸不僅會影響數據傳輸速度&#xff0c;還會影響應用程序的響應時間&#xff0c;最終影響用戶體驗。為了有效地解決網絡性能問題&#xff0c;了解和使用各種追蹤工具至關重要。另外一方面&#xff0c;造…

MySQL批量插入數據的幾種方式

最近在項目中遇到了批量插入數據的一些問題&#xff0c;一般圖省事都是利用mybatis或mybatis-plus的saveBatch方法&#xff0c;但這次因為數據過多&#xff0c;出現了一系列的問題&#xff0c;只能手動處理&#xff0c;接下來分享下對于數據量比較大的批量插入&#xff0c;可能…

python的魔法方法

python類中的self是什么&#xff1f; 對象的方法都會有一個self參數&#xff0c;類比于c&#xff0c;self就相當于c的this指針。 由一個類可以生成無數個對象&#xff0c;當一個對象的方法被調用時&#xff0c;對象會講自身的引用作為第一個參數傳給該方法&#xff0c;那么pyt…