文章連結:liuyueyi.github.io/hexblog/201…
Quick-Task 動態指令碼支援框架之結構設計篇
相關博文:
前面兩篇博文,主要是整體介紹和如何使用;接下來開始進入正題,逐步剖析,這個專案是怎麼一步一步搭建起來的;本篇博文則主要介紹基本骨架的設計,圍繞專案的核心點,實現一個基礎的原型系統
I. 結構分析
整體設計圖如下:
對於上面的圖,得有一個基本的認知,最好是能在腦海中構想出整個框架執行的方式,在正式開始之前,先簡單的過一下這張結構圖
抓要點
1. 任務執行單元
即圖中的每個task就表示一個基本的任務,有下面幾個要求
- 統一的繼承關係(物件導向的設計理念,執行同一個角色的類由某個抽象的介面繼承而來)
- 任務的執行之間是沒有關係的(即任務在獨立的執行緒中排程執行)
2. 任務佇列
在圖中表現很明顯了,在記憶體中會儲存一個當前所有執行的任務佇列(或者其他的容器)
這個的目的是什麼?
- 任務指令碼更新時,需要解除安裝舊的任務(因此可以從佇列中找到舊的任務,並停掉)
- 任務指令碼刪除時,需要解除安裝舊的任務
3. 任務管理者
雖然圖中並沒有明確的說有這麼個東西,但也好理解,我們的系統設計目標就是支援多工的執行和熱載入,那麼肯定有個任務管理的角色,來處理這些事情
其要做的事情就一個任務熱載入
- 包括動態指令碼更新,刪除,新增的事件監聽
- 實現解除安裝記憶體中舊的任務並載入執行新的任務
4. 外掛系統
這個與核心功能關係不大,可以先不care,簡單說一下就是為task提供更好的使用的公共類
這裡不詳細展開,後面再說
II. 設計實現
有了上面的簡單認知之後,開始進入正題,編碼環節,省略掉建立工程等步驟,第一步就是設計Task的API
1. ITask設計
抽象公共的任務介面,從任務的標識區分,和業務排程執行,很容易寫出下面的實現
public interface ITask {
/**
* 預設將task的類名作為唯一標識
*
* @return
*/
default String name() {
return this.getClass().getName();
}
/**
* 開始執行任務
*/
void run();
/**
* 任務中斷
*/
default void interrupt() {}
}
複製程式碼
前面兩個好理解,中斷這個介面的目的何在?主要是出於任務結束時的收尾操作,特別是在使用到流等操作時,有這麼個回撥就比較好了
2. TaskDecorate
任務裝飾類,為什麼有這麼個東西?出於什麼考慮的?
從上面可以知道,所有的任務最終都是在獨立的執行緒中排程執行,那麼我們自己實現的Task肯定都是會封裝到執行緒中的,在Java中可以怎麼起一個執行緒執行呢?
一個順其自然的想法就是包裝一下ITask介面,讓它整合自Thread,然後就可以簡單的直接將任務丟到執行緒池中即可
@Slf4j
public class ScriptTaskDecorate extends Thread {
private ITask task;
public ScriptTaskDecorate(ITask task) {
this.task = task;
setName(task.name());
}
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
log.error("script task run error! task: {}", task.name());
}
}
@Override
public void interrupt() {
task.interrupt();
}
}
複製程式碼
說明:
上面這個並不是必須的,你也完全可以自己線上程池排程Task任務時,進行硬編碼風格的封裝呼叫,完全沒有問題(只是程式碼將不太好看而已)
3. TaskContainer
上面兩個是具體的任務相關定義介面,接下來就是維護這些任務的容器了,最簡單的就是用一個Map來儲存,uuid到task的對映關係,然後再需要解除安裝/更新任務時,停掉舊的,新增新的任務,對應的實現也比較簡單
public class TaskContainer {
/**
* key: com.git.hui.task.api.ITask#name()
*/
private static Map<String, ScriptTaskDecorate> taskCache = new ConcurrentHashMap<>();
/**
* key: absolute script path
*
* for task to delete
*/
private static Map<String, ScriptTaskDecorate> pathCache = new ConcurrentHashMap<>();
public static void registerTask(String path, ScriptTaskDecorate task) {
ScriptTaskDecorate origin = taskCache.get(task.getName());
if (origin != null) {
origin.interrupt();
}
taskCache.put(task.getName(), task);
pathCache.put(path, task);
AsynTaskManager.addTask(task);
}
public static void removeTask(String path) {
ScriptTaskDecorate task = pathCache.get(path);
if (task != null) {
task.interrupt();
taskCache.remove(task.getName());
pathCache.remove(path);
}
}
}
複製程式碼
說明
為什麼有兩個map,一個唯一標識name為key,一個是task的全路徑為key?
- 刪除任務時,是直接刪除檔案,所以需要維護一個
pathCache
- 維護name的對映,主要是基於任務的唯一標識出發的,後續可能借此做一些擴充套件(比如任務和任務之間的關聯等)
4. 任務註冊
前面介紹了任務的定義和裝載任務的容器,接下來可以想到的就是如何發現任務並註冊了,這一塊這裡不要詳細展開,後面另起一篇詳解;主要說一下思路
在設計之初,就決定任務採用Groovy指令碼來實現熱載入,所以有兩個很容易想到的功能點
- 監聽Groovy指令碼的變動(新增,更新,刪除),對應的類為
TaskChangeWatcher
- 載入Groovy指令碼到記憶體,並執行,對應的類為
GroovyCompile
5. 執行流程
有了上面四個是否可以搭建一個原型框架呢?
答案是可以的,整個框架的執行過程
- 程式啟動,註冊Groovy指令碼變動監聽器
- 載入groovy指令碼,註冊到TaskContainer
- 將groovy指令碼丟到執行緒池中排程執行
- 執行完畢後,清除和回收現場
6. 其他
當然其他一些輔助的工具類可有可無了,當然從使用的角度出發,有很多東西還是很有必要的,如
- 通用的日誌輸出元件(特別是日誌輸出,收集,檢索,經典的ELK場景)
- 報警相關元件
- 監控相關
- redis快取工具類
- dao工具類
- mq消費工具類
- http工具類
- 其他
III. 其他
0. 相關
博文:
專案:
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840