• 180723-Quick-Task 动态脚本支持框架之结构设计篇


    logo

    文章链接:https://liuyueyi.github.io/hexblog/2018/07/23/180723-Quick-Task-动态脚本支持框架之结构设计篇/

    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. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

    一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

    2. 声明

    尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

    3. 扫描关注

    blogInfoV2.png

  • 相关阅读:
    sigaction函数解析
    实战Nginx与PHP(FastCGI)的安装、配置与优化
    Linux下Nginx+PHP 简单安装配置
    Nginx安装配置PHP(FastCGI)环境的教程
    Linux上配置Nginx+PHP5(FastCGI)
    @JoinTable和@JoinColumn
    Spring Data JPA 之 一对一,一对多,多对多 关系映射
    MyChrome制作Chrome浏览器便携版
    注解@CrossOrigin解决跨域问题
    MySQL查看表结构及查看建表语句
  • 原文地址:https://www.cnblogs.com/yihuihui/p/9357041.html
一二三 - 开发者的网上家园