Quick-Task 動態指令碼支援框架之結構設計篇

一灰灰發表於2018-07-23
logo

文章連結:liuyueyi.github.io/hexblog/201…

Quick-Task 動態指令碼支援框架之結構設計篇

相關博文:

前面兩篇博文,主要是整體介紹和如何使用;接下來開始進入正題,逐步剖析,這個專案是怎麼一步一步搭建起來的;本篇博文則主要介紹基本骨架的設計,圍繞專案的核心點,實現一個基礎的原型系統

I. 結構分析

整體設計圖如下:

指令碼框架.png

對於上面的圖,得有一個基本的認知,最好是能在腦海中構想出整個框架執行的方式,在正式開始之前,先簡單的過一下這張結構圖

抓要點

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或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

blogInfoV2.png

相關文章