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 和 动态加载插件完成。