【代码评审】IoT:plugin 相关的实现

This commit is contained in:
YunaiV 2025-02-04 17:34:04 +08:00
parent d23be86164
commit b46e630912
11 changed files with 120 additions and 111 deletions

View File

@ -78,6 +78,7 @@ public class PluginInfoController {
return success(true);
}
// TODO @haohao要不独立一个 VO不用 PluginInfoSaveReqVO
@PutMapping("/update-status")
@Operation(summary = "修改插件状态")
@PreAuthorize("@ss.hasPermission('iot:plugin-info:update')")

View File

@ -13,7 +13,7 @@ public class PluginInfoSaveReqVO {
// TODO @haohao一些枚举字段需要加枚举校验例如说deployTypestatustype
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
@Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
private Long id;
@Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")

View File

@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
// TODO @haohao建议 IotPluginInfoDO 改成 IotPluginConfigDO插件配置项目里暂时没有用 info 作为配置的哈
/**
* IoT 插件信息 DO
*
@ -38,7 +39,7 @@ public class IotPluginInfoDO extends BaseDO {
*/
private String name;
/**
* 描述
* 插件描述
*/
private String description;
/**
@ -68,12 +69,14 @@ public class IotPluginInfoDO extends BaseDO {
*/
// TODO @芋艿枚举字段
private String protocol;
// TODO @haohao这个字段是不是直接用 CommonStatus开启禁用然后插件实例那online 是否在线
/**
* 状态
* <p>
* 枚举 {@link IotPluginStatusEnum}
*/
private Integer status;
// TODO @芋艿configSchemaconfig 示例字段
/**
* 插件配置项描述信息
@ -83,6 +86,7 @@ public class IotPluginInfoDO extends BaseDO {
* 插件配置信息
*/
private String config;
// TODO @芋艿script 后续的使用
/**
* 插件脚本

View File

@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.iot.framework.plugin.config;
import cn.iocoder.yudao.module.iot.framework.plugin.core.CustomPluginStateListener;
import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStartRunner;
import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStateListener;
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInfoService;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Value;
@ -9,16 +11,24 @@ import org.springframework.context.annotation.Configuration;
import java.nio.file.Paths;
// TODO @芋艿需要 review
@Slf4j
/**
* IoT 插件配置类
*
* @author haohao
*/
@Configuration
public class UnifiedConfiguration {
@Value("${pf4j.pluginsDir:pluginsDir}")
private String pluginsDir;
@Slf4j
public class IotPluginConfiguration {
@Bean
public SpringPluginManager pluginManager() {
public IotPluginStartRunner pluginStartRunner(SpringPluginManager pluginManager,
IotPluginInfoService pluginInfoService) {
return new IotPluginStartRunner(pluginManager, pluginInfoService);
}
// TODO @芋艿需要 review
@Bean
public SpringPluginManager pluginManager(@Value("${pf4j.pluginsDir:pluginsDir}") String pluginsDir) {
log.info("[init][实例化 SpringPluginManager]");
SpringPluginManager springPluginManager = new SpringPluginManager(Paths.get(pluginsDir)) {
// SpringPluginManager springPluginManager = new SpringPluginManager() {
@ -30,7 +40,7 @@ public class UnifiedConfiguration {
}
};
springPluginManager.addPluginStateListener(new CustomPluginStateListener());
springPluginManager.addPluginStateListener(new IotPluginStateListener());
return springPluginManager;
}

View File

@ -1,25 +0,0 @@
package cn.iocoder.yudao.module.iot.framework.plugin.core;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginStateListener;
import org.springframework.stereotype.Component;
// TODO @芋艿需要 review
@Component
@Slf4j
public class CustomPluginStateListener implements PluginStateListener {
@Override
public void pluginStateChanged(PluginStateEvent event) {
// 1. 获取插件ID
String pluginId = event.getPlugin().getPluginId();
// 2. 获取插件旧状态
String oldState = event.getOldState().toString();
// 3. 获取插件新状态
String newState = event.getPluginState().toString();
// 4. 打印日志信息
log.info("插件的状态 '{}' 已更改为 '{}' 至 '{}'", pluginId, oldState, newState);
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.iot.framework.plugin.core;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import java.util.List;
/**
* IoT 插件启动 Runner
*
* 用于 Spring Boot 启动时启动 {@link IotPluginDeployTypeEnum#JAR} 部署类型的插件
*/
@RequiredArgsConstructor
@Slf4j
public class IotPluginStartRunner implements ApplicationRunner {
private final SpringPluginManager springPluginManager;
private final IotPluginInfoService pluginInfoService;
@Override
public void run(ApplicationArguments args) {
List<IotPluginInfoDO> pluginInfoList = TenantUtils.executeIgnore(
// TODO @haohao需要查询部署类型哈
() -> pluginInfoService.getPluginInfoListByStatus(IotPluginStatusEnum.RUNNING.getStatus()));
if (CollUtil.isEmpty(pluginInfoList)) {
log.info("[run][没有需要启动的插件]");
return;
}
// 遍历插件列表逐个启动
pluginInfoList.forEach(pluginInfo -> {
try {
log.info("[run][插件({}) 启动开始]", pluginInfo.getPluginKey());
springPluginManager.startPlugin(pluginInfo.getPluginKey());
log.info("[run][插件({}) 启动完成]", pluginInfo.getPluginKey());
} catch (Exception e) {
log.error("[run][插件({}) 启动异常]", pluginInfo.getPluginKey(), e);
}
});
}
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.iot.framework.plugin.core;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginStateListener;
/**
* IoT 插件状态监听器用于 log 插件的状态变化
*
* @author haohao
*/
@Slf4j
public class IotPluginStateListener implements PluginStateListener {
@Override
public void pluginStateChanged(PluginStateEvent event) {
log.info("[pluginStateChanged][插件({}) 状态变化,从 {} 变为 {}]", event.getPlugin().getPluginId(),
event.getOldState().toString(), event.getPluginState().toString());
}
}

View File

@ -1,49 +0,0 @@
package cn.iocoder.yudao.module.iot.framework.plugin.core;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInfoService;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
// TODO @芋艿需要 review
@Component
@Slf4j
public class PluginStart implements ApplicationRunner {
@Resource
private IotPluginInfoService pluginInfoService;
@Resource
private SpringPluginManager pluginManager;
@Override
public void run(ApplicationArguments args) {
TenantUtils.executeIgnore(() -> { // 1. 忽略租户上下文执行
List<IotPluginInfoDO> pluginInfoList = pluginInfoService
.getPluginInfoListByStatus(IotPluginStatusEnum.RUNNING.getStatus()); // 2. 获取运行中的插件列表
if (CollUtil.isEmpty(pluginInfoList)) { // 3. 检查插件列表是否为空
log.info("[run] 没有需要启动的插件"); // 4. 日志记录没有插件需要启动
return;
}
pluginInfoList.forEach(pluginInfo -> { // 5. 使用lambda表达式遍历插件列表
try {
log.info("[run][启动插件] pluginKey = {}", pluginInfo.getPluginKey()); // 6. 日志记录插件启动信息
pluginManager.startPlugin(pluginInfo.getPluginKey()); // 7. 启动插件
} catch (Exception e) {
log.error("[run][启动插件失败] pluginKey = {}", pluginInfo.getPluginKey(), e); // 8. 记录启动失败的日志
}
});
});
}
}

View File

@ -14,7 +14,7 @@ import java.util.List;
/**
* IoT 插件信息 Service 接口
*
* @author 芋道源码
* @author haohao
*/
public interface IotPluginInfoService {

View File

@ -23,7 +23,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PLUGIN_INFO_N
/**
* IoT 插件信息 Service 实现类
*
* @author 芋道源码
* @author haohao
*/
@Service
@Validated
@ -37,7 +37,7 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
private IotPluginInstanceService pluginInstanceService;
@Resource
private SpringPluginManager pluginManager;
private SpringPluginManager springPluginManager;
@Override
public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
@ -60,18 +60,17 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
public void deletePluginInfo(Long id) {
// 1.1 校验存在
IotPluginInfoDO pluginInfoDO = validatePluginInfoExists(id);
// 1.2 停止插件
// 1.2 未开启状态才允许删除
if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING);
}
// 2. 卸载插件
// 2.1 卸载插件
pluginInstanceService.stopAndUnloadPlugin(pluginInfoDO.getPluginKey());
// 3. 删除插件文件
// 2.2 删除插件文件
pluginInstanceService.deletePluginFile(pluginInfoDO);
// 4. 删除插件信息
// 3. 删除插件信息
pluginInfoMapper.deleteById(id);
}
@ -97,17 +96,18 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
public void uploadFile(Long id, MultipartFile file) {
// 1. 校验插件信息是否存在
IotPluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
// TODO @haohao最好校验下 file 相关参数是否完整类似version 之类是不是可以解析到
// 2. 停止并卸载旧的插件
// 2.1 停止并卸载旧的插件
pluginInstanceService.stopAndUnloadPlugin(pluginInfoDo.getPluginKey());
// 3 上传新的插件文件更新插件启用状态文件
// 2.2 上传新的插件文件更新插件启用状态文件
String pluginKeyNew = pluginInstanceService.uploadAndLoadNewPlugin(file);
// 4. 更新插件信息
// 3. 更新插件信息
updatePluginInfo(pluginInfoDo, pluginKeyNew, file);
}
// TODO @haohao这个方法要不合并到 uploadFile
/**
* 更新插件信息
*
@ -116,18 +116,15 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
* @param file 文件
*/
private void updatePluginInfo(IotPluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) {
// 创建新的插件信息对象并链式设置属性
IotPluginInfoDO updatedPluginInfo = new IotPluginInfoDO()
.setId(pluginInfoDo.getId())
.setPluginKey(pluginKeyNew)
.setStatus(IotPluginStatusEnum.STOPPED.getStatus())
.setStatus(IotPluginStatusEnum.STOPPED.getStatus()) // TODO @haohao这个状态是不是非 stop
.setFileName(file.getOriginalFilename())
.setScript("")
.setConfigSchema(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription())
.setVersion(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion())
.setDescription(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription());
// 执行更新
.setScript("") // TODO @haohao这个设置为 "" 会不会覆盖数据里的哈应该从插件里读取未来
.setConfigSchema(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription())
.setVersion(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion())
.setDescription(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription());
pluginInfoMapper.updateById(updatedPluginInfo);
}
@ -140,10 +137,7 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
pluginInstanceService.updatePluginStatus(pluginInfoDo, status);
// 3. 更新数据库中的插件状态
IotPluginInfoDO updatedPluginInfo = new IotPluginInfoDO();
updatedPluginInfo.setId(id);
updatedPluginInfo.setStatus(status);
pluginInfoMapper.updateById(updatedPluginInfo);
pluginInfoMapper.updateById(new IotPluginInfoDO().setId(id).setStatus(status));
}
@Override

View File

@ -404,7 +404,7 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService {
// + 禁用开启1存在删除TODO 测试直接删除结论可以deleteJob
//
if (true) {
if (false) {
Long id = 1L;
Map<String, Object> jobDataMap = IotRuleSceneJob.buildJobDataMap(id);
schedulerManager.addOrUpdateJob(IotRuleSceneJob.class,
@ -417,7 +417,8 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService {
schedulerManager.pauseJob(IotRuleSceneJob.buildJobName(id));
}
if (true) {
Long id = 1L;
schedulerManager.deleteJob(IotRuleSceneJob.buildJobName(id));
}
}