Commit 0e20ca342f67b547fb181773f7008448fd248019
1 parent
c9904f09
【代码优化】IoT: 插件管理
Showing
10 changed files
with
252 additions
and
121 deletions
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> |