Commit b5856c4cfc3a55e89928cb313670210af64a5f7c

Authored by YunaiV
1 parent 603649d2

【代码评审】IoT:插件机制

Showing 15 changed files with 86 additions and 75 deletions
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/ServiceRegistry.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.api; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.api;
3 import java.util.HashMap; 3 import java.util.HashMap;
4 import java.util.Map; 4 import java.util.Map;
5 5
  6 +// TODO 芋艿:纠结下
6 /** 7 /**
7 * 服务注册表 - 插架模块使用,无法使用 Spring 注入 8 * 服务注册表 - 插架模块使用,无法使用 Spring 注入
8 */ 9 */
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/DeviceDataApi.java
@@ -2,9 +2,12 @@ package cn.iocoder.yudao.module.iot.api.device; @@ -2,9 +2,12 @@ package cn.iocoder.yudao.module.iot.api.device;
2 2
3 /** 3 /**
4 * 设备数据 API 4 * 设备数据 API
  5 + *
  6 + * @author haohao
5 */ 7 */
6 public interface DeviceDataApi { 8 public interface DeviceDataApi {
7 9
  10 + // TODO @haohao:最好搞成 dto 哈!
8 /** 11 /**
9 * 保存设备数据 12 * 保存设备数据
10 * 13 *
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcRequest.java
@@ -5,9 +5,9 @@ import lombok.Builder; @@ -5,9 +5,9 @@ import lombok.Builder;
5 import lombok.Data; 5 import lombok.Data;
6 import lombok.NoArgsConstructor; 6 import lombok.NoArgsConstructor;
7 7
  8 +// TODO @芋艿:要不要加个 mqtt 值了的前缀
8 /** 9 /**
9 * MQTT RPC 请求 10 * MQTT RPC 请求
10 - *  
11 */ 11 */
12 @Data 12 @Data
13 @Builder 13 @Builder
@@ -23,6 +23,7 @@ public class RpcRequest { @@ -23,6 +23,7 @@ public class RpcRequest {
23 /** 23 /**
24 * 参数 24 * 参数
25 */ 25 */
  26 + // TODO @haohao:object 对象会不会不好序列化?
26 private Object[] params; 27 private Object[] params;
27 28
28 /** 29 /**
@@ -34,4 +35,5 @@ public class RpcRequest { @@ -34,4 +35,5 @@ public class RpcRequest {
34 * 回复地址 35 * 回复地址
35 */ 36 */
36 private String replyTo; 37 private String replyTo;
  38 +
37 } 39 }
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcResponse.java
@@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor;
7 7
8 /** 8 /**
9 * MQTT RPC 响应 9 * MQTT RPC 响应
10 - *  
11 */ 10 */
12 @Data 11 @Data
13 @Builder 12 @Builder
@@ -23,10 +22,12 @@ public class RpcResponse { @@ -23,10 +22,12 @@ public class RpcResponse {
23 /** 22 /**
24 * 结果 23 * 结果
25 */ 24 */
  25 + // TODO @haohao:object 对象会不会不好反序列化?
26 private Object result; 26 private Object result;
27 27
28 /** 28 /**
29 * 错误 29 * 错误
30 */ 30 */
31 private String error; 31 private String error;
  32 +
32 } 33 }
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/SerializationUtils.java
@@ -15,4 +15,5 @@ public class SerializationUtils { @@ -15,4 +15,5 @@ public class SerializationUtils {
15 public static <T> T deserialize(String json, Class<T> clazz) { 15 public static <T> T deserialize(String json, Class<T> clazz) {
16 return JSONUtil.toBean(json, clazz); 16 return JSONUtil.toBean(json, clazz);
17 } 17 }
  18 +
18 } 19 }
19 \ No newline at end of file 20 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoImportReqVO.java
@@ -9,7 +9,7 @@ import org.springframework.web.multipart.MultipartFile; @@ -9,7 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
9 @Data 9 @Data
10 public class PluginInfoImportReqVO { 10 public class PluginInfoImportReqVO {
11 11
12 - @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") 12 + @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
13 private Long id; 13 private Long id;
14 14
15 @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED) 15 @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED)
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoRespVO.java
1 package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo; 1 package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
2 2
3 -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;  
4 -import com.alibaba.excel.annotation.ExcelProperty;  
5 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
6 import lombok.Data; 4 import lombok.Data;
7 5
@@ -9,63 +7,48 @@ import java.time.LocalDateTime; @@ -9,63 +7,48 @@ import java.time.LocalDateTime;
9 7
10 @Schema(description = "管理后台 - IoT 插件信息 Response VO") 8 @Schema(description = "管理后台 - IoT 插件信息 Response VO")
11 @Data 9 @Data
12 -@ExcelIgnoreUnannotated  
13 public class PluginInfoRespVO { 10 public class PluginInfoRespVO {
14 11
15 @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") 12 @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
16 - @ExcelProperty("主键 ID")  
17 private Long id; 13 private Long id;
18 14
19 @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") 15 @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
20 - @ExcelProperty("插件包标识符")  
21 private String pluginKey; 16 private String pluginKey;
22 17
23 @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") 18 @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
24 - @ExcelProperty("插件名称")  
25 private String name; 19 private String name;
26 20
27 @Schema(description = "描述", example = "你猜") 21 @Schema(description = "描述", example = "你猜")
28 - @ExcelProperty("描述")  
29 private String description; 22 private String description;
30 23
31 @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") 24 @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
32 - @ExcelProperty("部署方式")  
33 private Integer deployType; 25 private Integer deployType;
34 26
35 @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) 27 @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED)
36 - @ExcelProperty("插件包文件名")  
37 private String fileName; 28 private String fileName;
38 29
39 @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) 30 @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED)
40 - @ExcelProperty("插件版本")  
41 private String version; 31 private String version;
42 32
43 @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") 33 @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
44 - @ExcelProperty("插件类型")  
45 private Integer type; 34 private Integer type;
46 35
47 @Schema(description = "设备插件协议类型") 36 @Schema(description = "设备插件协议类型")
48 - @ExcelProperty("设备插件协议类型")  
49 private String protocol; 37 private String protocol;
50 38
51 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) 39 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
52 - @ExcelProperty("状态")  
53 private Integer status; 40 private Integer status;
54 41
55 @Schema(description = "插件配置项描述信息") 42 @Schema(description = "插件配置项描述信息")
56 - @ExcelProperty("插件配置项描述信息")  
57 private String configSchema; 43 private String configSchema;
58 44
59 @Schema(description = "插件配置信息") 45 @Schema(description = "插件配置信息")
60 - @ExcelProperty("插件配置信息")  
61 private String config; 46 private String config;
62 47
63 @Schema(description = "插件脚本") 48 @Schema(description = "插件脚本")
64 - @ExcelProperty("插件脚本")  
65 private String script; 49 private String script;
66 50
67 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) 51 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
68 - @ExcelProperty("创建时间")  
69 private LocalDateTime createTime; 52 private LocalDateTime createTime;
70 53
71 } 54 }
72 \ No newline at end of file 55 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoSaveReqVO.java
@@ -7,6 +7,10 @@ import lombok.*; @@ -7,6 +7,10 @@ import lombok.*;
7 @Data 7 @Data
8 public class PluginInfoSaveReqVO { 8 public class PluginInfoSaveReqVO {
9 9
  10 + // TODO @haohao:新增的字段有点多,每个都需要哇?
  11 +
  12 + // TODO @haohao:一些枚举字段,需要加枚举校验。例如说,deployType、status、type 等
  13 +
10 @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") 14 @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
11 private Long id; 15 private Long id;
12 16
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/PluginInstanceMapper.java
@@ -21,6 +21,7 @@ public interface PluginInstanceMapper extends BaseMapperX&lt;PluginInstanceDO&gt; { @@ -21,6 +21,7 @@ public interface PluginInstanceMapper extends BaseMapperX&lt;PluginInstanceDO&gt; {
21 .eq(PluginInstanceDO::getPluginId, pluginId)); 21 .eq(PluginInstanceDO::getPluginId, pluginId));
22 } 22 }
23 23
  24 + // TODO @haohao:这个还需要么?相关不用的 VO 可以删除
24 default PageResult<PluginInstanceDO> selectPage(PluginInstancePageReqVO reqVO) { 25 default PageResult<PluginInstanceDO> selectPage(PluginInstancePageReqVO reqVO) {
25 return selectPage(reqVO, new LambdaQueryWrapperX<PluginInstanceDO>() 26 return selectPage(reqVO, new LambdaQueryWrapperX<PluginInstanceDO>()
26 .eqIfPresent(PluginInstanceDO::getMainId, reqVO.getMainId()) 27 .eqIfPresent(PluginInstanceDO::getMainId, reqVO.getMainId())
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/PluginInstancesJob.java
1 package cn.iocoder.yudao.module.iot.job.plugin; 1 package cn.iocoder.yudao.module.iot.job.plugin;
2 2
3 -  
4 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; 3 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
5 import cn.iocoder.yudao.module.iot.service.plugin.PluginInstanceService; 4 import cn.iocoder.yudao.module.iot.service.plugin.PluginInstanceService;
6 import org.springframework.scheduling.annotation.Scheduled; 5 import org.springframework.scheduling.annotation.Scheduled;
@@ -26,4 +25,5 @@ public class PluginInstancesJob { @@ -26,4 +25,5 @@ public class PluginInstancesJob {
26 pluginInstanceService.updatePluginInstances(); 25 pluginInstanceService.updatePluginInstances();
27 }); 26 });
28 } 27 }
  28 +
29 } 29 }
30 \ No newline at end of file 30 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/server/RpcServer.java
@@ -15,6 +15,8 @@ import javax.annotation.PreDestroy; @@ -15,6 +15,8 @@ import javax.annotation.PreDestroy;
15 import java.util.HashMap; 15 import java.util.HashMap;
16 import java.util.Map; 16 import java.util.Map;
17 17
  18 +// TODO @芋艿:server 逻辑,再瞅瞅;
  19 +// TODO @haohao:如果只写在 iot biz 里,那么后续 server => client 貌似不方便?微信再讨论下~;
18 @Service 20 @Service
19 @Slf4j 21 @Slf4j
20 public class RpcServer { 22 public class RpcServer {
@@ -90,6 +92,9 @@ public class RpcServer { @@ -90,6 +92,9 @@ public class RpcServer {
90 */ 92 */
91 @FunctionalInterface 93 @FunctionalInterface
92 public interface MethodInvoker { 94 public interface MethodInvoker {
  95 +
93 Object invoke(Object[] params) throws Exception; 96 Object invoke(Object[] params) throws Exception;
  97 +
94 } 98 }
  99 +
95 } 100 }
96 \ No newline at end of file 101 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInfoServiceImpl.java
@@ -41,18 +41,18 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -41,18 +41,18 @@ public class PluginInfoServiceImpl implements PluginInfoService {
41 41
42 @Resource 42 @Resource
43 private PluginInfoMapper pluginInfoMapper; 43 private PluginInfoMapper pluginInfoMapper;
  44 +
44 @Resource 45 @Resource
45 private SpringPluginManager pluginManager; 46 private SpringPluginManager pluginManager;
46 47
  48 + // TODO @芋艿:要不要换位置
47 @Value("${pf4j.pluginsDir}") 49 @Value("${pf4j.pluginsDir}")
48 private String pluginsDir; 50 private String pluginsDir;
49 51
50 @Override 52 @Override
51 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) { 53 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
52 - // 插入  
53 PluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, PluginInfoDO.class); 54 PluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, PluginInfoDO.class);
54 pluginInfoMapper.insert(pluginInfo); 55 pluginInfoMapper.insert(pluginInfo);
55 - // 返回  
56 return pluginInfo.getId(); 56 return pluginInfo.getId();
57 } 57 }
58 58
@@ -67,29 +67,29 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -67,29 +67,29 @@ public class PluginInfoServiceImpl implements PluginInfoService {
67 67
68 @Override 68 @Override
69 public void deletePluginInfo(Long id) { 69 public void deletePluginInfo(Long id) {
70 - // 校验存在 70 + // 1.1 校验存在
71 PluginInfoDO pluginInfoDO = validatePluginInfoExists(id); 71 PluginInfoDO pluginInfoDO = validatePluginInfoExists(id);
72 -  
73 - // 停止插件 72 + // 1.2 停止插件
74 if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) { 73 if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
75 throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING); 74 throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING);
76 } 75 }
77 76
78 - // 卸载插件 77 + // 2. 卸载插件
  78 + // TODO @haohao:可以复用 stopAndUnloadPlugin
79 PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginKey()); 79 PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginKey());
80 if (plugin != null) { 80 if (plugin != null) {
81 - // 查询插件是否是启动状态 81 + // 停止插件
82 if (plugin.getPluginState().equals(PluginState.STARTED)) { 82 if (plugin.getPluginState().equals(PluginState.STARTED)) {
83 - // 停止插件  
84 pluginManager.stopPlugin(plugin.getPluginId()); 83 pluginManager.stopPlugin(plugin.getPluginId());
85 } 84 }
86 // 卸载插件 85 // 卸载插件
87 pluginManager.unloadPlugin(plugin.getPluginId()); 86 pluginManager.unloadPlugin(plugin.getPluginId());
88 } 87 }
89 88
90 - // 删除 89 + // 3.1 删除
91 pluginInfoMapper.deleteById(id); 90 pluginInfoMapper.deleteById(id);
92 - // 删除插件文件 91 + // 3.2 删除插件文件
  92 + // TODO @haohao:这个直接主线程 sleep 就好了,不用单独开线程池哈。原因是,低频操作;另外,只有存在的时候,才 sleep + 删除;
93 Executors.newSingleThreadExecutor().submit(() -> { 93 Executors.newSingleThreadExecutor().submit(() -> {
94 try { 94 try {
95 TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕 95 TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕
@@ -101,7 +101,6 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -101,7 +101,6 @@ public class PluginInfoServiceImpl implements PluginInfoService {
101 log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e); 101 log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e);
102 } 102 }
103 }); 103 });
104 -  
105 } 104 }
106 105
107 private PluginInfoDO validatePluginInfoExists(Long id) { 106 private PluginInfoDO validatePluginInfoExists(Long id) {
@@ -127,22 +126,19 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -127,22 +126,19 @@ public class PluginInfoServiceImpl implements PluginInfoService {
127 // 1. 校验插件信息是否存在 126 // 1. 校验插件信息是否存在
128 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id); 127 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
129 128
130 - // 2. 获取插件标识  
131 - String pluginKey = pluginInfoDo.getPluginKey();  
132 -  
133 - // 3. 停止并卸载旧的插件  
134 - stopAndUnloadPlugin(pluginKey); 129 + // 2. 停止并卸载旧的插件
  130 + stopAndUnloadPlugin(pluginInfoDo.getPluginKey());
135 131
136 - // 4. 上传新的插件文件 132 + // 3.1 上传新的插件文件
137 String pluginKeyNew = uploadAndLoadNewPlugin(file); 133 String pluginKeyNew = uploadAndLoadNewPlugin(file);
138 -  
139 - // 5. 更新插件启用状态文件 134 + // 3.2 更新插件启用状态文件
140 updatePluginStatusFile(pluginKeyNew, false); 135 updatePluginStatusFile(pluginKeyNew, false);
141 136
142 - // 6. 更新插件信息 137 + // 4. 更新插件信息
143 updatePluginInfo(pluginInfoDo, pluginKeyNew, file); 138 updatePluginInfo(pluginInfoDo, pluginKeyNew, file);
144 } 139 }
145 140
  141 + // TODO @haohao:注释的格式
146 // 停止并卸载旧的插件 142 // 停止并卸载旧的插件
147 private void stopAndUnloadPlugin(String pluginKey) { 143 private void stopAndUnloadPlugin(String pluginKey) {
148 PluginWrapper plugin = pluginManager.getPlugin(pluginKey); 144 PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
@@ -154,10 +150,13 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -154,10 +150,13 @@ public class PluginInfoServiceImpl implements PluginInfoService {
154 } 150 }
155 } 151 }
156 152
  153 + // TODO @haohao:注释的格式
157 // 上传并加载新的插件文件 154 // 上传并加载新的插件文件
158 private String uploadAndLoadNewPlugin(MultipartFile file) { 155 private String uploadAndLoadNewPlugin(MultipartFile file) {
  156 + // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载
159 Path pluginsPath = Paths.get(pluginsDir); 157 Path pluginsPath = Paths.get(pluginsDir);
160 try { 158 try {
  159 + // TODO @haohao:可以使用 FileUtil 简化?
161 if (!Files.exists(pluginsPath)) { 160 if (!Files.exists(pluginsPath)) {
162 Files.createDirectories(pluginsPath); // 创建插件目录 161 Files.createDirectories(pluginsPath); // 创建插件目录
163 } 162 }
@@ -166,16 +165,18 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -166,16 +165,18 @@ public class PluginInfoServiceImpl implements PluginInfoService {
166 Path jarPath = pluginsPath.resolve(filename); 165 Path jarPath = pluginsPath.resolve(filename);
167 Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件 166 Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
168 return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件 167 return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
169 - } else {  
170 - throw exception(PLUGIN_INSTALL_FAILED);  
171 } 168 }
  169 + throw exception(PLUGIN_INSTALL_FAILED); // TODO @haohao:这么抛的话,貌似会被 catch (Exception e) {
172 } catch (Exception e) { 170 } catch (Exception e) {
  171 + // TODO @haohao:打个 error log,方便排查
173 throw exception(PLUGIN_INSTALL_FAILED); 172 throw exception(PLUGIN_INSTALL_FAILED);
174 } 173 }
175 } 174 }
176 175
  176 + // TODO @haohao:注释的格式
177 // 更新插件状态文件 177 // 更新插件状态文件
178 private void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled) { 178 private void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled) {
  179 + // TODO @haohao:疑问,这里写 enabled.txt 和 disabled.txt 的目的是啥哈?
179 Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt"); 180 Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");
180 Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt"); 181 Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");
181 Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath; 182 Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;
@@ -186,10 +187,8 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -186,10 +187,8 @@ public class PluginInfoServiceImpl implements PluginInfoService {
186 if (pluginWrapper == null) { 187 if (pluginWrapper == null) {
187 throw exception(PLUGIN_INSTALL_FAILED); 188 throw exception(PLUGIN_INSTALL_FAILED);
188 } 189 }
189 - List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath)  
190 - : new ArrayList<>();  
191 - List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath)  
192 - : new ArrayList<>(); 190 + List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath) : new ArrayList<>();
  191 + List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath) : new ArrayList<>();
193 192
194 if (!targetLines.contains(pluginKeyNew)) { 193 if (!targetLines.contains(pluginKeyNew)) {
195 targetLines.add(pluginKeyNew); 194 targetLines.add(pluginKeyNew);
@@ -207,26 +206,33 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -207,26 +206,33 @@ public class PluginInfoServiceImpl implements PluginInfoService {
207 } 206 }
208 } 207 }
209 208
  209 + // TODO @haohao:注释的格式
210 // 更新插件信息 210 // 更新插件信息
211 private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) { 211 private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) {
  212 + // TODO @haohao:更新实体的时候,最好 new 一个新的!
  213 + // TODO @haohao:可以链式调用,简化下代码;
212 pluginInfoDo.setPluginKey(pluginKeyNew); 214 pluginInfoDo.setPluginKey(pluginKeyNew);
213 pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus()); 215 pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
214 pluginInfoDo.setFileName(file.getOriginalFilename()); 216 pluginInfoDo.setFileName(file.getOriginalFilename());
215 pluginInfoDo.setScript(""); 217 pluginInfoDo.setScript("");
216 - 218 + // 解析 pf4j 插件
217 PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginKeyNew).getDescriptor(); 219 PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginKeyNew).getDescriptor();
218 pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription()); 220 pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription());
219 pluginInfoDo.setVersion(pluginDescriptor.getVersion()); 221 pluginInfoDo.setVersion(pluginDescriptor.getVersion());
220 pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription()); 222 pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
  223 +
  224 + // 执行更新
221 pluginInfoMapper.updateById(pluginInfoDo); 225 pluginInfoMapper.updateById(pluginInfoDo);
222 } 226 }
223 227
  228 + // TODO @haohao:status、state 字段命名,要统一下~
224 @Override 229 @Override
225 public void updatePluginStatus(Long id, Integer status) { 230 public void updatePluginStatus(Long id, Integer status) {
226 // 1. 校验插件信息是否存在 231 // 1. 校验插件信息是否存在
227 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id); 232 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
228 233
229 // 2. 校验插件状态是否有效 234 // 2. 校验插件状态是否有效
  235 + // TODO @haohao:直接参数校验掉。通过 @InEnum
230 if (!IotPluginStatusEnum.contains(status)) { 236 if (!IotPluginStatusEnum.contains(status)) {
231 throw exception(PLUGIN_STATUS_INVALID); 237 throw exception(PLUGIN_STATUS_INVALID);
232 } 238 }
@@ -237,17 +243,16 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -237,17 +243,16 @@ public class PluginInfoServiceImpl implements PluginInfoService {
237 243
238 // 4. 根据状态更新插件 244 // 4. 根据状态更新插件
239 if (plugin != null) { 245 if (plugin != null) {
240 - // 4.1 如果目标状态是运行且插件未启动,则启动插件 246 + // 4.1 启动:如果目标状态是运行且插件未启动,则启动插件
241 if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) 247 if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
242 && plugin.getPluginState() != PluginState.STARTED) { 248 && plugin.getPluginState() != PluginState.STARTED) {
243 pluginManager.startPlugin(pluginKey); 249 pluginManager.startPlugin(pluginKey);
244 - updatePluginStatusFile(pluginKey, true); // 更新插件状态文件为启用  
245 - }  
246 - // 4.2 如果目标状态是停止且插件已启动,则停止插件  
247 - else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) 250 + updatePluginStatusFile(pluginKey, true);
  251 + // 4.2 停止:如果目标状态是停止且插件已启动,则停止插件
  252 + } else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
248 && plugin.getPluginState() == PluginState.STARTED) { 253 && plugin.getPluginState() == PluginState.STARTED) {
249 pluginManager.stopPlugin(pluginKey); 254 pluginManager.stopPlugin(pluginKey);
250 - updatePluginStatusFile(pluginKey, false); // 更新插件状态文件为禁用 255 + updatePluginStatusFile(pluginKey, false);
251 } 256 }
252 } else { 257 } else {
253 // 5. 插件不存在且状态为停止,抛出异常 258 // 5. 插件不存在且状态为停止,抛出异常
@@ -257,17 +262,20 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -257,17 +262,20 @@ public class PluginInfoServiceImpl implements PluginInfoService {
257 } 262 }
258 263
259 // 6. 更新数据库中的插件状态 264 // 6. 更新数据库中的插件状态
  265 + // TODO @haohao:新建新建 pluginInfoDo 哈!
260 pluginInfoDo.setStatus(status); 266 pluginInfoDo.setStatus(status);
261 pluginInfoMapper.updateById(pluginInfoDo); 267 pluginInfoMapper.updateById(pluginInfoDo);
262 } 268 }
263 269
264 @Override 270 @Override
265 public List<PluginInfoDO> getPluginInfoList() { 271 public List<PluginInfoDO> getPluginInfoList() {
266 - return pluginInfoMapper.selectList(null); 272 + return pluginInfoMapper.selectList();
267 } 273 }
268 274
  275 + // TODO @haohao:可以改成 getPluginInfoListByStatus 更通用哈。
269 @Override 276 @Override
270 public List<PluginInfoDO> getRunningPluginInfoList() { 277 public List<PluginInfoDO> getRunningPluginInfoList() {
271 return pluginInfoMapper.selectListByStatus(IotPluginStatusEnum.RUNNING.getStatus()); 278 return pluginInfoMapper.selectListByStatus(IotPluginStatusEnum.RUNNING.getStatus());
272 } 279 }
  280 +
273 } 281 }
274 \ No newline at end of file 282 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInstanceServiceImpl.java
@@ -26,8 +26,9 @@ import java.util.List; @@ -26,8 +26,9 @@ import java.util.List;
26 public class PluginInstanceServiceImpl implements PluginInstanceService { 26 public class PluginInstanceServiceImpl implements PluginInstanceService {
27 27
28 /** 28 /**
29 - * 主程序id 29 + * 主程序 ID
30 */ 30 */
  31 + // TODO @haohao:这个可以后续确认下,有没更合适的标识。例如说 mac 地址之类的
31 public static final String MAIN_ID = IdUtil.fastSimpleUUID(); 32 public static final String MAIN_ID = IdUtil.fastSimpleUUID();
32 33
33 @Resource 34 @Resource
@@ -40,36 +41,37 @@ public class PluginInstanceServiceImpl implements PluginInstanceService { @@ -40,36 +41,37 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
40 @Value("${server.port:48080}") 41 @Value("${server.port:48080}")
41 private int port; 42 private int port;
42 43
  44 + // TODO @haohao:建议把 PluginInfoServiceImpl 里面,和 instance 相关的逻辑拿过来,可能会更好。info 处理信息,instance 处理实例
43 45
  46 + // TODO @haohao:这个改成 reportPluginInstance 会不会更合适哈。
44 @Override 47 @Override
45 public void updatePluginInstances() { 48 public void updatePluginInstances() {
46 - // 1. 查询 pf4j 插件列表 49 + // 1.1 查询 pf4j 插件列表
47 List<PluginWrapper> plugins = pluginManager.getPlugins(); 50 List<PluginWrapper> plugins = pluginManager.getPlugins();
48 -  
49 - // 2. 查询插件信息列表 51 + // 1.2 查询插件信息列表
50 List<PluginInfoDO> pluginInfos = pluginInfoService.getPluginInfoList(); 52 List<PluginInfoDO> pluginInfos = pluginInfoService.getPluginInfoList();
51 -  
52 - // 动态获取主程序的 IP 和端口 53 + // 1.3 动态获取主程序的 IP 和端口
53 String mainIp = getLocalIpAddress(); 54 String mainIp = getLocalIpAddress();
54 55
55 - // 3. 遍历插件列表,并保存为插件实例 56 + // 2. 遍历插件列表,并保存为插件实例
56 for (PluginWrapper plugin : plugins) { 57 for (PluginWrapper plugin : plugins) {
  58 + // 2.1 查找插件信息
57 String pluginKey = plugin.getPluginId(); 59 String pluginKey = plugin.getPluginId();
  60 + // TODO @haohao:CollUtil.findOne() 简化
58 PluginInfoDO pluginInfo = pluginInfos.stream() 61 PluginInfoDO pluginInfo = pluginInfos.stream()
59 .filter(pluginInfoDO -> pluginInfoDO.getPluginKey().equals(pluginKey)) 62 .filter(pluginInfoDO -> pluginInfoDO.getPluginKey().equals(pluginKey))
60 .findFirst() 63 .findFirst()
61 .orElse(null); 64 .orElse(null);
62 -  
63 - // 4. 如果插件信息不存在,则跳过  
64 if (pluginInfo == null) { 65 if (pluginInfo == null) {
  66 + // TODO @haohao:建议打个 error log
65 continue; 67 continue;
66 } 68 }
67 69
68 - // 5. 查询插件实例 70 + // 2.2 查询插件实例
69 PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(MAIN_ID, pluginInfo.getId()); 71 PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(MAIN_ID, pluginInfo.getId());
70 -  
71 - // 6. 如果插件实例不存在,则创建 72 + // 2.3.1 如果插件实例不存在,则创建
72 if (pluginInstance == null) { 73 if (pluginInstance == null) {
  74 + // TODO @haohao:可以链式调用;建议新建一个!
73 pluginInstance = new PluginInstanceDO(); 75 pluginInstance = new PluginInstanceDO();
74 pluginInstance.setPluginId(pluginInfo.getId()); 76 pluginInstance.setPluginId(pluginInfo.getId());
75 pluginInstance.setMainId(MAIN_ID); 77 pluginInstance.setMainId(MAIN_ID);
@@ -78,13 +80,14 @@ public class PluginInstanceServiceImpl implements PluginInstanceService { @@ -78,13 +80,14 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
78 pluginInstance.setHeartbeatAt(System.currentTimeMillis()); 80 pluginInstance.setHeartbeatAt(System.currentTimeMillis());
79 pluginInstanceMapper.insert(pluginInstance); 81 pluginInstanceMapper.insert(pluginInstance);
80 } else { 82 } else {
81 - // 7. 如果插件实例存在,则更新 83 + // 2.3.2 如果插件实例存在,则更新
82 pluginInstance.setHeartbeatAt(System.currentTimeMillis()); 84 pluginInstance.setHeartbeatAt(System.currentTimeMillis());
83 pluginInstanceMapper.updateById(pluginInstance); 85 pluginInstanceMapper.updateById(pluginInstance);
84 } 86 }
85 } 87 }
86 } 88 }
87 89
  90 + // TODO @haohao:这个目的是,获取到第一个有效 ip 是哇?
88 private String getLocalIpAddress() { 91 private String getLocalIpAddress() {
89 try { 92 try {
90 List<String> ipList = NetUtil.localIpv4s().stream() 93 List<String> ipList = NetUtil.localIpv4s().stream()
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/controller/RpcController.java
@@ -28,4 +28,5 @@ public class RpcController { @@ -28,4 +28,5 @@ public class RpcController {
28 public CompletableFuture<Object> concat(@RequestParam String str1, @RequestParam String str2) throws Exception { 28 public CompletableFuture<Object> concat(@RequestParam String str1, @RequestParam String str2) throws Exception {
29 return rpcClient.call("concat", new Object[]{str1, str2}, 10); 29 return rpcClient.call("concat", new Object[]{str1, str2}, 10);
30 } 30 }
  31 +
31 } 32 }
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/client/RpcClient.java
1 package cn.iocoder.yudao.module.iot.mqttrpc.client; 1 package cn.iocoder.yudao.module.iot.mqttrpc.client;
2 2
3 -import java.util.concurrent.CompletableFuture;  
4 -import java.util.concurrent.ConcurrentHashMap;  
5 -import java.util.concurrent.ConcurrentMap;  
6 -import java.util.concurrent.Executors;  
7 -import java.util.concurrent.ScheduledExecutorService;  
8 -import java.util.concurrent.TimeUnit;  
9 import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest; 3 import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest;
10 import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse; 4 import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;
11 import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils; 5 import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;
12 import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig; 6 import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;
13 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
14 -import org.eclipse.paho.client.mqttv3.*; 8 +import org.eclipse.paho.client.mqttv3.MqttClient;
  9 +import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
  10 +import org.eclipse.paho.client.mqttv3.MqttException;
  11 +import org.eclipse.paho.client.mqttv3.MqttMessage;
15 import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; 12 import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
16 import org.springframework.stereotype.Service; 13 import org.springframework.stereotype.Service;
17 14
@@ -20,6 +17,7 @@ import javax.annotation.PreDestroy; @@ -20,6 +17,7 @@ import javax.annotation.PreDestroy;
20 import java.util.UUID; 17 import java.util.UUID;
21 import java.util.concurrent.*; 18 import java.util.concurrent.*;
22 19
  20 +// TODO @芋艿:需要考虑,怎么公用!
23 @Service 21 @Service
24 @Slf4j 22 @Slf4j
25 public class RpcClient { 23 public class RpcClient {