8.完成任务实现的SDK封装及插件式加载

2023-12-18 06:26:34

1.设计

任务的实现目前完成了Modbus RTU、Modbus TCP、Virtule。任务实现应该是任意的,比如打印一段话,执行一句SQL等,所以系统内部的必然要做到可扩展。

要做到可扩展,首先第一步就是定义标准,所以我们首先需要封装任务实现的SDK(第一件事)。

还要考虑到,用户使用我们的框架,但是并不想修改我们框架内部的代码,而是自己建立仓库完成实现,动态加载到我们的系统中,所以我们还要提供插件式集成任务实现的能力(第二件事)。

所以下面就对这两个需求展开设计与实现。

1.1 封装任务实现的SDK

此任务较易实现,因为前面我们就对任务有了一定的封装,所以只需要将定义部分,摘抄出去形成独立的jar就可以了,说干就干

1.1.1 建立一个新的工程:dttask-protocol-sdk

dttask-protocol-sdk 就是一个简单的java jar

移动文件,注意更改import

1.1.2 建立一个新的工程:dttask-protocol-simulator

这是一个依赖dttask-protocol-sdk的任务实现工程

它没有什么特殊,和dttask-server里的Virtual里的实现基本一样,就是类名不一样。建这个类也是为了后续测试动态加载插件做准备

至此工程依赖结构如下图:

1.2 实现动态加载插件

上面已经完成了插件的编写,因为我们整体使用的spring,所以我们在实现插件时就需要完成spring的bean注册。

1.2.1 ProtocolController

提供一个flushProtocol的接口,里面完成对插件jar包的动态加载,原理就是:

  • 先使用classloader将jar包里的class都加载进来
  • 然后检测所有class有没有是spring的bean,是的话,就向spring容器注册它对应的BeanDefinition
  • 最后使用spring容器查找对应bean,完成bean的实例化以及任务的实现放进ProtocolManager中集中管理
@RestController
@Slf4j
public class ProtocolController {

    @Autowired
    private DefaultListableBeanFactory defaultListableBeanFactory;
    @Autowired
    private ProtocolManager protocolManager;

    @GetMapping("/flushProtocol")
    public void flushProtocol(@RequestParam(value = "jarAddress", required = false) String jarAddress) {
        if (jarAddress == null) {
            jarAddress = "D:\\workspaces\\dttask\\protocolJar\\dttask-protocol-simulator-1.0-SNAPSHOT.jar";
        }
        String jarPath = "file:/" + jarAddress;
        hotDeployWithSpring(jarAddress, jarPath);
    }

    /**
     * 加入jar包后 动态注册bean到spring容器,包括bean的依赖
     */
    public void hotDeployWithSpring(String jarAddress, String jarPath) {
        Set<String> classNameSet = readJarFile(jarAddress);
        try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(jarPath)}, Thread.currentThread().getContextClassLoader())) {
            for (String className : classNameSet) {
                Class<?> clazz = urlClassLoader.loadClass(className);
                if (isSpringBeanClass(clazz)) {
                    BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                    String beanName = transformName(className);
                    defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
                }
            }
        } catch (ClassNotFoundException e) {
            throw new BusinessException("hotDeployWithSpring ClassNotFoundException", e);
        } catch (MalformedURLException e) {
            throw new BusinessException("hotDeployWithSpring MalformedURLException", e);
        } catch (IOException e) {
            throw new BusinessException("hotDeployWithSpring IOException", e);
        } finally {
            protocolManager.refreshMap();
        }


    }

    /**
     * 读取jar包中所有类文件
     */
    public static Set<String> readJarFile(String jarAddress) {
        Set<String> classNameSet = new HashSet<>();
        Enumeration<JarEntry> entries;
        try (JarFile jarFile = new JarFile(jarAddress)) {
            //遍历整个jar文件
            entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String name = jarEntry.getName();
                if (name.endsWith(".class")) {
                    String className = name.replace(".class", "").replace("/", ".");
                    classNameSet.add(className);
                }
            }
        } catch (IOException e) {
            log.error("readJarFile exception:", e);
            throw new BusinessException("readJarFile exception", e);
        }
        return classNameSet;
    }

    /**
     * 方法描述 判断class对象是否带有spring的注解
     */
    public static boolean isSpringBeanClass(Class<?> cla) {
        if (cla == null) {
            return false;
        }
        // 不是抽象类 接口 且 没有以下注解
        return (cla.getAnnotation(Component.class) != null
                || cla.getAnnotation(Repository.class) != null
                || cla.getAnnotation(Service.class) != null)
                && !Modifier.isAbstract(cla.getModifiers())
                && !cla.isInterface();
    }

    /**
     * 类名首字母小写 作为spring容器beanMap的key
     */
    public static String transformName(String className) {
        String tmpstr = className.substring(className.lastIndexOf(".") + 1);
        return tmpstr.substring(0, 1).toLowerCase() + tmpstr.substring(1);
    }


    /**
     * 删除jar包时 需要在spring容器删除注入
     */
    public void delete(String jarAddress, String jarPath) {
        Set<String> classNameSet = readJarFile(jarAddress);
        try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(jarPath)},
                Thread.currentThread().getContextClassLoader())) {
            for (String className : classNameSet) {
                Class<?> clazz = urlClassLoader.loadClass(className);
                if (isSpringBeanClass(clazz)) {
                    defaultListableBeanFactory.removeBeanDefinition(transformName(className));
                }
            }
        } catch (MalformedURLException e) {
            throw new BusinessException("delete MalformedURLException", e);
        } catch (IOException | ClassNotFoundException e) {
            throw new BusinessException("delete IOException or ClassNotFoundException", e);
        } 
    }
}

1.2.2 JobController

完成对Job的启动和停止

@RestController
@Slf4j
public class JobController {
    
    @Autowired
    private NetworkService networkService;
    
    @GetMapping("/stopDttaskJob")
    public void stopJobId(@RequestParam("dttaskJobId") long dttaskJobId) {
        Set<Long> dttaskJobIds = new HashSet<>();
        dttaskJobIds.add(dttaskJobId);
        networkService.stopCollect(dttaskJobIds);
    }

    @GetMapping("/startDttaskJob")
    public void startJobId(@RequestParam("dttaskJobId") long dttaskJobId) {
        Set<Long> dttaskJobIds = new HashSet<>();
        dttaskJobIds.add(dttaskJobId);
        networkService.startCollect(dttaskJobIds);
    }
    
}

2. 测试

  • 使用ApiPost建立3个请求

  • 启动三个节点,三个节点完成选举,并各自执行2个任务

  • dttask-protocol-simulator工程打一个jar包,放到 D:\workspaces\dttask\protocolJar 目录下

  • 对1号controller节点,发送停止任务请求

  • 将停止的任务的link_type字段改为-2并保存(-2是simulator实现的任务类型)

  • 发送协议实现接口调用

  • dttask-protocol-simulator加上一个断点,方便后续的查看

  • 发送任务启动接口

进入断点,表示 外部的插件已经动态加载进来了

至此 封装SDK 和 动态加载插件完成。

文章来源:https://blog.csdn.net/u010142228/article/details/135050494
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。