Commit 0e20ca342f67b547fb181773f7008448fd248019

Authored by 安浩浩
1 parent c9904f09

【代码优化】IoT: 插件管理

plugins/enabled.txt
  1 +http-plugin@0.0.1
plugins/yudao-module-iot-http-plugin-2.2.0-snapshot.jar
No preview for this file type
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/PluginInfoController.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.plugininfo; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.plugininfo;
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoImportReqVO;
6 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO; 7 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
7 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoRespVO; 8 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoRespVO;
8 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO; 9 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
@@ -16,11 +17,8 @@ import jakarta.validation.Valid; @@ -16,11 +17,8 @@ import jakarta.validation.Valid;
16 import org.springframework.security.access.prepost.PreAuthorize; 17 import org.springframework.security.access.prepost.PreAuthorize;
17 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
18 import org.springframework.web.bind.annotation.*; 19 import org.springframework.web.bind.annotation.*;
19 -import org.springframework.web.multipart.MultipartFile;  
20 20
21 -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;  
22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 21 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
23 -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;  
24 22
25 @Tag(name = "管理后台 - IoT 插件信息") 23 @Tag(name = "管理后台 - IoT 插件信息")
26 @RestController 24 @RestController
@@ -72,16 +70,10 @@ public class PluginInfoController { @@ -72,16 +70,10 @@ public class PluginInfoController {
72 return success(BeanUtils.toBean(pageResult, PluginInfoRespVO.class)); 70 return success(BeanUtils.toBean(pageResult, PluginInfoRespVO.class));
73 } 71 }
74 72
75 - @RequestMapping(value = "/update-jar",  
76 - method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题  
77 - @Operation(summary = "上传Jar包")  
78 - public CommonResult<Boolean> uploadJar(  
79 - @RequestParam("id") Long id,  
80 - @RequestParam("jar") MultipartFile file) throws Exception {  
81 - if (file.isEmpty()) {  
82 - throw exception(FILE_IS_EMPTY);  
83 - }  
84 - pluginInfoService.uploadJar(id, file); 73 + @PostMapping("/upload-file")
  74 + @Operation(summary = "上传插件文件")
  75 + public CommonResult<Boolean> uploadFile(@Valid PluginInfoImportReqVO reqVO) {
  76 + pluginInfoService.uploadFile(reqVO.getId(), reqVO.getFile());
85 return success(true); 77 return success(true);
86 } 78 }
87 79
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugininfo/vo/PluginInfoImportReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +import org.springframework.web.multipart.MultipartFile;
  7 +
  8 +@Schema(description = "管理后台 - IoT 插件上传 Request VO")
  9 +@Data
  10 +public class PluginInfoImportReqVO {
  11 +
  12 + @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
  13 + private Long id;
  14 +
  15 + @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED)
  16 + @NotNull(message = "插件文件不能为空")
  17 + private MultipartFile file;
  18 +
  19 +}
0 \ 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/framework/plugin/SpringConfiguration.java deleted
1 -package cn.iocoder.yudao.module.iot.framework.plugin;  
2 -  
3 -import org.pf4j.spring.SpringPluginManager;  
4 -import org.springframework.context.annotation.Bean;  
5 -import org.springframework.context.annotation.Configuration;  
6 -import org.springframework.context.annotation.DependsOn;  
7 -  
8 -@Configuration  
9 -public class SpringConfiguration {  
10 -  
11 - @Bean  
12 - @DependsOn("serviceRegistryInitializedMarker")  
13 - public SpringPluginManager pluginManager() {  
14 - return new SpringPluginManager();  
15 - }  
16 -  
17 -}  
18 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/ServiceRegistryConfiguration.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/UnifiedConfiguration.java
1 package cn.iocoder.yudao.module.iot.framework.plugin; 1 package cn.iocoder.yudao.module.iot.framework.plugin;
2 2
3 -import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;  
4 import cn.iocoder.yudao.module.iot.api.ServiceRegistry; 3 import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
  4 +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
5 import lombok.extern.slf4j.Slf4j; 5 import lombok.extern.slf4j.Slf4j;
  6 +import org.pf4j.spring.SpringPluginManager;
6 import org.springframework.context.annotation.Bean; 7 import org.springframework.context.annotation.Bean;
7 import org.springframework.context.annotation.Configuration; 8 import org.springframework.context.annotation.Configuration;
  9 +import org.springframework.context.annotation.DependsOn;
8 10
9 -import javax.annotation.PostConstruct;  
10 import javax.annotation.Resource; 11 import javax.annotation.Resource;
11 12
12 @Slf4j 13 @Slf4j
13 @Configuration 14 @Configuration
14 -public class ServiceRegistryConfiguration { 15 +public class UnifiedConfiguration {
  16 +
  17 + private static final String SERVICE_REGISTRY_INITIALIZED_MARKER = "serviceRegistryInitializedMarker";
15 18
16 @Resource 19 @Resource
17 private DeviceDataApi deviceDataApi; 20 private DeviceDataApi deviceDataApi;
18 21
19 - @PostConstruct  
20 - public void init() {  
21 - // 将主程序中的 DeviceDataApi 实例注册到 ServiceRegistry 22 + @Bean(SERVICE_REGISTRY_INITIALIZED_MARKER)
  23 + public Object serviceRegistryInitializedMarker() {
22 ServiceRegistry.registerService(DeviceDataApi.class, deviceDataApi); 24 ServiceRegistry.registerService(DeviceDataApi.class, deviceDataApi);
23 log.info("[init][将 DeviceDataApi 实例注册到 ServiceRegistry 中]"); 25 log.info("[init][将 DeviceDataApi 实例注册到 ServiceRegistry 中]");
  26 + return new Object();
24 } 27 }
25 28
26 - /**  
27 - * 定义一个标记用的 Bean,用于表示 ServiceRegistry 已初始化完成  
28 - */  
29 - @Bean("serviceRegistryInitializedMarker") // TODO @haohao:1)这个名字,可以搞个 public static final 常量;2)是不是 conditionBefore 啥  
30 - public Object serviceRegistryInitializedMarker() {  
31 - // 返回任意对象即可,这里返回 null 都可以,但最好返回个实际对象  
32 - return new Object(); 29 + @Bean
  30 + @DependsOn(SERVICE_REGISTRY_INITIALIZED_MARKER)
  31 + public SpringPluginManager pluginManager() {
  32 + log.info("[init][实例化 SpringPluginManager]");
  33 + return new SpringPluginManager();
33 } 34 }
34 35
35 } 36 }
36 \ No newline at end of file 37 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoService.java
@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO; @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
7 import jakarta.validation.Valid; 7 import jakarta.validation.Valid;
8 import org.springframework.web.multipart.MultipartFile; 8 import org.springframework.web.multipart.MultipartFile;
9 9
  10 +import java.util.List;
  11 +
10 /** 12 /**
11 * IoT 插件信息 Service 接口 13 * IoT 插件信息 Service 接口
12 * 14 *
@@ -58,7 +60,7 @@ public interface PluginInfoService { @@ -58,7 +60,7 @@ public interface PluginInfoService {
58 * @param id 插件id 60 * @param id 插件id
59 * @param file 文件 61 * @param file 文件
60 */ 62 */
61 - void uploadJar(Long id, MultipartFile file); 63 + void uploadFile(Long id, MultipartFile file);
62 64
63 /** 65 /**
64 * 更新插件的状态 66 * 更新插件的状态
@@ -67,4 +69,11 @@ public interface PluginInfoService { @@ -67,4 +69,11 @@ public interface PluginInfoService {
67 * @param status 状态 69 * @param status 状态
68 */ 70 */
69 void updatePluginStatus(Long id, Integer status); 71 void updatePluginStatus(Long id, Integer status);
  72 +
  73 + /**
  74 + * 获得启用的插件列表
  75 + *
  76 + * @return 插件列表-插件id
  77 + */
  78 + List<String> getEnabledPlugins();
70 } 79 }
71 \ No newline at end of file 80 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugininfo/PluginInfoServiceImpl.java
1 package cn.iocoder.yudao.module.iot.service.plugininfo; 1 package cn.iocoder.yudao.module.iot.service.plugininfo;
2 2
3 -import cn.hutool.core.io.IoUtil;  
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 -import cn.iocoder.yudao.module.infra.api.file.FileApi;  
7 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO; 5 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoPageReqVO;
8 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO; 6 import cn.iocoder.yudao.module.iot.controller.admin.plugininfo.vo.PluginInfoSaveReqVO;
9 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO; 7 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
10 import cn.iocoder.yudao.module.iot.dal.mysql.plugininfo.PluginInfoMapper; 8 import cn.iocoder.yudao.module.iot.dal.mysql.plugininfo.PluginInfoMapper;
11 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; 9 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
12 -import jakarta.annotation.PostConstruct;  
13 import jakarta.annotation.Resource; 10 import jakarta.annotation.Resource;
14 -import lombok.SneakyThrows;  
15 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
16 import org.pf4j.PluginDescriptor; 12 import org.pf4j.PluginDescriptor;
17 import org.pf4j.PluginState; 13 import org.pf4j.PluginState;
@@ -22,11 +18,13 @@ import org.springframework.stereotype.Service; @@ -22,11 +18,13 @@ import org.springframework.stereotype.Service;
22 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
23 import org.springframework.web.multipart.MultipartFile; 19 import org.springframework.web.multipart.MultipartFile;
24 20
25 -import java.nio.file.Path; 21 +import java.io.File;
  22 +import java.io.IOException;
  23 +import java.nio.file.*;
  24 +import java.util.ArrayList;
  25 +import java.util.List;
26 import java.util.concurrent.Executors; 26 import java.util.concurrent.Executors;
27 import java.util.concurrent.TimeUnit; 27 import java.util.concurrent.TimeUnit;
28 -import java.util.jar.JarEntry;  
29 -import java.util.jar.JarFile;  
30 28
31 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
32 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; 30 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
@@ -47,9 +45,6 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -47,9 +45,6 @@ public class PluginInfoServiceImpl implements PluginInfoService {
47 @Resource 45 @Resource
48 private SpringPluginManager pluginManager; 46 private SpringPluginManager pluginManager;
49 47
50 - @Resource  
51 - private FileApi fileApi;  
52 -  
53 @Value("${pf4j.pluginsDir}") 48 @Value("${pf4j.pluginsDir}")
54 private String pluginsDir; 49 private String pluginsDir;
55 50
@@ -95,6 +90,19 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -95,6 +90,19 @@ public class PluginInfoServiceImpl implements PluginInfoService {
95 90
96 // 删除 91 // 删除
97 pluginInfoMapper.deleteById(id); 92 pluginInfoMapper.deleteById(id);
  93 + // 删除插件文件
  94 + Executors.newSingleThreadExecutor().submit(() -> {
  95 + try {
  96 + TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕
  97 + File file = new File(pluginsDir, pluginInfoDO.getFile());
  98 + if (file.exists() && !file.delete()) {
  99 + log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFile());
  100 + }
  101 + } catch (InterruptedException e) {
  102 + log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFile(), e);
  103 + }
  104 + });
  105 +
98 } 106 }
99 107
100 private PluginInfoDO validatePluginInfoExists(Long id) { 108 private PluginInfoDO validatePluginInfoExists(Long id) {
@@ -116,73 +124,100 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -116,73 +124,100 @@ public class PluginInfoServiceImpl implements PluginInfoService {
116 } 124 }
117 125
118 @Override 126 @Override
119 - public void uploadJar(Long id, MultipartFile file) {  
120 - // 1. 校验存在 127 + public void uploadFile(Long id, MultipartFile file) {
  128 + // 1. 校验插件信息是否存在
121 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id); 129 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
122 130
123 - // 2. 判断文件名称与插件 ID 是否匹配 131 + // 2. 获取插件 ID
124 String pluginId = pluginInfoDo.getPluginId(); 132 String pluginId = pluginInfoDo.getPluginId();
125 133
126 - // 3. 停止卸载旧的插件  
127 - // 3.1. 获取插件信息 134 + // 3. 停止并卸载旧的插件
  135 + stopAndUnloadPlugin(pluginId);
  136 +
  137 + // 4. 上传新的插件文件
  138 + String pluginIdNew = uploadAndLoadNewPlugin(file);
  139 +
  140 + // 5. 更新插件启用状态文件
  141 + updatePluginStatusFile(pluginIdNew, false);
  142 +
  143 + // 6. 更新插件信息
  144 + updatePluginInfo(pluginInfoDo, pluginIdNew, file);
  145 + }
  146 +
  147 + // 停止并卸载旧的插件
  148 + private void stopAndUnloadPlugin(String pluginId) {
128 PluginWrapper plugin = pluginManager.getPlugin(pluginId); 149 PluginWrapper plugin = pluginManager.getPlugin(pluginId);
129 if (plugin != null) { 150 if (plugin != null) {
130 - // 3.2. 如果插件状态是启动的,停止插件  
131 if (plugin.getPluginState().equals(PluginState.STARTED)) { 151 if (plugin.getPluginState().equals(PluginState.STARTED)) {
132 - pluginManager.stopPlugin(pluginId); 152 + pluginManager.stopPlugin(pluginId); // 停止插件
133 } 153 }
134 - // 3.3. 卸载插件  
135 - pluginManager.unloadPlugin(pluginId); 154 + pluginManager.unloadPlugin(pluginId); // 卸载插件
136 } 155 }
  156 + }
137 157
138 - // 4. 上传插件  
139 - String pluginIdNew; 158 + // 上传并加载新的插件文件
  159 + private String uploadAndLoadNewPlugin(MultipartFile file) {
  160 + Path pluginsPath = Paths.get(pluginsDir);
140 try { 161 try {
141 - String path = fileApi.createFile(pluginsDir, IoUtil.readBytes(file.getInputStream()));  
142 - Path pluginPath = Path.of(path);  
143 - pluginIdNew = pluginManager.loadPlugin(pluginPath); 162 + if (!Files.exists(pluginsPath)) {
  163 + Files.createDirectories(pluginsPath); // 创建插件目录
  164 + }
  165 + String filename = file.getOriginalFilename();
  166 + if (filename != null) {
  167 + Path jarPath = pluginsPath.resolve(filename);
  168 + Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
  169 + return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
  170 + } else {
  171 + throw exception(PLUGIN_INSTALL_FAILED);
  172 + }
144 } catch (Exception e) { 173 } catch (Exception e) {
145 throw exception(PLUGIN_INSTALL_FAILED); 174 throw exception(PLUGIN_INSTALL_FAILED);
146 } 175 }
  176 + }
147 177
148 - PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginIdNew);  
149 - if (pluginWrapper == null) {  
150 - throw exception(PLUGIN_INSTALL_FAILED);  
151 - } 178 + // 更新插件状态文件
  179 + private void updatePluginStatusFile(String pluginIdNew, boolean isEnabled) {
  180 + Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");
  181 + Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");
  182 + Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;
  183 + Path oppositeFilePath = isEnabled ? disabledFilePath : enabledFilePath;
152 184
153 - // 5. 读取配置文件和脚本  
154 - String configJson = "";  
155 - String script = "";  
156 - try (JarFile jarFile = new JarFile(pluginWrapper.getPluginPath().toFile())) {  
157 - // 5.1 获取config文件在jar包中的路径  
158 - String configFile = "classes/config.json";  
159 - JarEntry configEntry = jarFile.getJarEntry(configFile);  
160 -  
161 - if (configEntry != null) {  
162 - // 5.2 读取配置文件  
163 - configJson = IoUtil.readUtf8(jarFile.getInputStream(configEntry));  
164 - log.info("configJson:{}", configJson); 185 + try {
  186 + PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginIdNew);
  187 + if (pluginWrapper == null) {
  188 + throw exception(PLUGIN_INSTALL_FAILED);
  189 + }
  190 + String pluginInfo = pluginIdNew + "@" + pluginWrapper.getDescriptor().getVersion();
  191 + List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath)
  192 + : new ArrayList<>();
  193 + List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath)
  194 + : new ArrayList<>();
  195 +
  196 + if (!targetLines.contains(pluginInfo)) {
  197 + targetLines.add(pluginInfo);
  198 + Files.write(targetFilePath, targetLines, StandardOpenOption.CREATE,
  199 + StandardOpenOption.TRUNCATE_EXISTING);
165 } 200 }
166 201
167 - // 5.3 读取script.js脚本  
168 - String scriptFile = "classes/script.js";  
169 - JarEntry scriptEntity = jarFile.getJarEntry(scriptFile);  
170 - if (scriptEntity != null) {  
171 - // 5.4 读取脚本文件  
172 - script = IoUtil.readUtf8(jarFile.getInputStream(scriptEntity));  
173 - log.info("script:{}", script); 202 + if (oppositeLines.contains(pluginInfo)) {
  203 + oppositeLines.remove(pluginInfo);
  204 + Files.write(oppositeFilePath, oppositeLines, StandardOpenOption.CREATE,
  205 + StandardOpenOption.TRUNCATE_EXISTING);
174 } 206 }
175 - } catch (Exception e) { 207 + } catch (IOException e) {
176 throw exception(PLUGIN_INSTALL_FAILED); 208 throw exception(PLUGIN_INSTALL_FAILED);
177 } 209 }
  210 + }
178 211
  212 + // 更新插件信息
  213 + private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginIdNew, MultipartFile file) {
179 pluginInfoDo.setPluginId(pluginIdNew); 214 pluginInfoDo.setPluginId(pluginIdNew);
180 pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus()); 215 pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
181 pluginInfoDo.setFile(file.getOriginalFilename()); 216 pluginInfoDo.setFile(file.getOriginalFilename());
182 - pluginInfoDo.setConfigSchema(configJson);  
183 - pluginInfoDo.setScript(script); 217 + pluginInfoDo.setScript("");
184 218
185 - PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor(); 219 + PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginIdNew).getDescriptor();
  220 + pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription());
186 pluginInfoDo.setVersion(pluginDescriptor.getVersion()); 221 pluginInfoDo.setVersion(pluginDescriptor.getVersion());
187 pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription()); 222 pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
188 pluginInfoMapper.updateById(pluginInfoDo); 223 pluginInfoMapper.updateById(pluginInfoDo);
@@ -190,52 +225,50 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -190,52 +225,50 @@ public class PluginInfoServiceImpl implements PluginInfoService {
190 225
191 @Override 226 @Override
192 public void updatePluginStatus(Long id, Integer status) { 227 public void updatePluginStatus(Long id, Integer status) {
193 - // 1. 校验存在 228 + // 1. 校验插件信息是否存在
194 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id); 229 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
195 230
196 - // 插件状态无效 231 + // 2. 校验插件状态是否有效
197 if (!IotPluginStatusEnum.contains(status)) { 232 if (!IotPluginStatusEnum.contains(status)) {
198 throw exception(PLUGIN_STATUS_INVALID); 233 throw exception(PLUGIN_STATUS_INVALID);
199 } 234 }
200 235
  236 + // 3. 获取插件ID和插件实例
201 String pluginId = pluginInfoDo.getPluginId(); 237 String pluginId = pluginInfoDo.getPluginId();
202 PluginWrapper plugin = pluginManager.getPlugin(pluginId); 238 PluginWrapper plugin = pluginManager.getPlugin(pluginId);
  239 +
  240 + // 4. 根据状态更新插件
203 if (plugin != null) { 241 if (plugin != null) {
204 - if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) && plugin.getPluginState() != PluginState.STARTED) {  
205 - // 启动插件 242 + // 4.1 如果目标状态是运行且插件未启动,则启动插件
  243 + if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
  244 + && plugin.getPluginState() != PluginState.STARTED) {
206 pluginManager.startPlugin(pluginId); 245 pluginManager.startPlugin(pluginId);
207 - } else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) && plugin.getPluginState() == PluginState.STARTED) {  
208 - // 停止插件 246 + updatePluginStatusFile(pluginId, true); // 更新插件状态文件为启用
  247 + }
  248 + // 4.2 如果目标状态是停止且插件已启动,则停止插件
  249 + else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
  250 + && plugin.getPluginState() == PluginState.STARTED) {
209 pluginManager.stopPlugin(pluginId); 251 pluginManager.stopPlugin(pluginId);
  252 + updatePluginStatusFile(pluginId, false); // 更新插件状态文件为禁用
210 } 253 }
211 } else { 254 } else {
212 - // 已经停止,未获取到插件 255 + // 5. 插件不存在且状态为停止,抛出异常
213 if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) { 256 if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
214 throw exception(PLUGIN_STATUS_INVALID); 257 throw exception(PLUGIN_STATUS_INVALID);
215 } 258 }
216 } 259 }
  260 +
  261 + // 6. 更新数据库中的插件状态
217 pluginInfoDo.setStatus(status); 262 pluginInfoDo.setStatus(status);
218 pluginInfoMapper.updateById(pluginInfoDo); 263 pluginInfoMapper.updateById(pluginInfoDo);
219 } 264 }
220 265
221 -// @PostConstruct  
222 -// public void init() {  
223 -// Executors.newSingleThreadScheduledExecutor().schedule(this::startPlugins, 3, TimeUnit.SECONDS);  
224 -// }  
225 -//  
226 -// @SneakyThrows  
227 -// private void startPlugins() {  
228 -// for (PluginInfoDO pluginInfoDO : pluginInfoMapper.selectList()) {  
229 -// if (!IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {  
230 -// continue;  
231 -// }  
232 -// log.info("start plugin:{}", pluginInfoDO.getPluginId());  
233 -// try {  
234 -// pluginManager.startPlugin(pluginInfoDO.getPluginId());  
235 -// } catch (Exception e) {  
236 -// log.error("start plugin error", e);  
237 -// }  
238 -// }  
239 -// } 266 + @Override
  267 + public List<String> getEnabledPlugins() {
  268 + return pluginInfoMapper.selectList().stream()
  269 + .filter(pluginInfoDO -> IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus()))
  270 + .map(PluginInfoDO::getPluginId)
  271 + .toList();
  272 + }
240 273
241 } 274 }
242 \ No newline at end of file 275 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/WelcomePlugin.java 0 → 100644
  1 +/*
  2 + * Copyright (C) 2012-present the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package cn.iocoder.yudao.module.iot.plugin;
  17 +
  18 +import cn.iocoder.yudao.module.iot.api.Greeting;
  19 +import org.apache.commons.lang.StringUtils;
  20 +import org.pf4j.Extension;
  21 +import org.pf4j.Plugin;
  22 +import org.pf4j.PluginWrapper;
  23 +import org.pf4j.RuntimeMode;
  24 +import com.sun.net.httpserver.HttpServer;
  25 +import java.io.IOException;
  26 +import java.io.OutputStream;
  27 +import java.net.InetSocketAddress;
  28 +
  29 +/**
  30 + * 打招呼 测试用例
  31 + */
  32 +public class WelcomePlugin extends Plugin {
  33 +
  34 + private HttpServer server;
  35 +
  36 + public WelcomePlugin(PluginWrapper wrapper) {
  37 + super(wrapper);
  38 + }
  39 +
  40 + @Override
  41 + public void start() {
  42 + System.out.println("WelcomePlugin.start()");
  43 + // for testing the development mode
  44 + if (RuntimeMode.DEVELOPMENT.equals(wrapper.getRuntimeMode())) {
  45 + System.out.println(StringUtils.upperCase("WelcomePlugin"));
  46 + }
  47 + startHttpServer();
  48 + }
  49 +
  50 + @Override
  51 + public void stop() {
  52 + System.out.println("WelcomePlugin.stop()");
  53 + stopHttpServer();
  54 + }
  55 +
  56 + private void startHttpServer() {
  57 + try {
  58 + server = HttpServer.create(new InetSocketAddress(9081), 0);
  59 + server.createContext("/", exchange -> {
  60 + String response = "Welcome to PF4J HTTP Server";
  61 + exchange.sendResponseHeaders(200, response.getBytes().length);
  62 + OutputStream os = exchange.getResponseBody();
  63 + os.write(response.getBytes());
  64 + os.close();
  65 + });
  66 + server.setExecutor(null);
  67 + server.start();
  68 + System.out.println("HTTP server started on port 9081");
  69 + } catch (IOException e) {
  70 + e.printStackTrace();
  71 + }
  72 + }
  73 +
  74 + private void stopHttpServer() {
  75 + if (server != null) {
  76 + server.stop(0);
  77 + System.out.println("HTTP server stopped");
  78 + }
  79 + }
  80 +
  81 + @Extension
  82 + public static class WelcomeGreeting implements Greeting {
  83 +
  84 + @Override
  85 + public String getGreeting() {
  86 + return "Welcome to PF4J";
  87 + }
  88 +
  89 + }
  90 +
  91 +}
0 \ No newline at end of file 92 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml
@@ -24,6 +24,7 @@ @@ -24,6 +24,7 @@
24 <plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpPlugin</plugin.class> 24 <plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpPlugin</plugin.class>
25 <plugin.version>0.0.1</plugin.version> 25 <plugin.version>0.0.1</plugin.version>
26 <plugin.provider>ahh</plugin.provider> 26 <plugin.provider>ahh</plugin.provider>
  27 + <plugin.description>http-plugin-0.0.1</plugin.description>
27 <plugin.dependencies/> 28 <plugin.dependencies/>
28 </properties> 29 </properties>
29 30
@@ -104,6 +105,7 @@ @@ -104,6 +105,7 @@
104 <Plugin-Class>${plugin.class}</Plugin-Class> 105 <Plugin-Class>${plugin.class}</Plugin-Class>
105 <Plugin-Version>${plugin.version}</Plugin-Version> 106 <Plugin-Version>${plugin.version}</Plugin-Version>
106 <Plugin-Provider>${plugin.provider}</Plugin-Provider> 107 <Plugin-Provider>${plugin.provider}</Plugin-Provider>
  108 + <Plugin-Description>${plugin.description}</Plugin-Description>
107 <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies> 109 <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
108 </manifestEntries> 110 </manifestEntries>
109 </archive> 111 </archive>