【代码优化】重构 HTTP插件并添加自动配置

This commit is contained in:
安浩浩 2025-01-26 17:29:03 +08:00
parent 269dec1b2e
commit 7bfa830628
10 changed files with 204 additions and 109 deletions

View File

@ -40,9 +40,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
@Slf4j
public class PluginInstanceServiceImpl implements PluginInstanceService {
// TODO @haohao这个可以后续确认下有没更合适的标识例如说 mac 地址之类的
// 简化的 UUID + mac 地址 会不会好一些一台机子有可能会部署多个插件
// 那就 mac@uuid
// TODO @haohaomac@uuid
public static final String MAIN_ID = IdUtil.fastSimpleUUID();
@Resource
@ -60,32 +58,31 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
@Override
public void stopAndUnloadPlugin(String pluginKey) {
PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
// TODO @haohao改成 if return 会更简洁一点
if (plugin != null) {
if (plugin.getPluginState().equals(PluginState.STARTED)) {
pluginManager.stopPlugin(pluginKey); // 停止插件
log.info("已停止插件: {}", pluginKey);
}
pluginManager.unloadPlugin(pluginKey); // 卸载插件
log.info("已卸载插件: {}", pluginKey);
} else {
if (plugin == null) {
log.warn("插件不存在或已卸载: {}", pluginKey);
return;
}
if (plugin.getPluginState().equals(PluginState.STARTED)) {
pluginManager.stopPlugin(pluginKey); // 停止插件
log.info("已停止插件: {}", pluginKey);
}
pluginManager.unloadPlugin(pluginKey); // 卸载插件
log.info("已卸载插件: {}", pluginKey);
}
@Override
public void deletePluginFile(PluginInfoDO pluginInfoDO) {
File file = new File(pluginsDir, pluginInfoDO.getFileName());
// TODO @haohao改成 if return 会更简洁一点
if (file.exists()) {
try {
TimeUnit.SECONDS.sleep(1); // 等待 1 避免插件未卸载完毕
if (!file.delete()) {
log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName());
}
} catch (InterruptedException e) {
log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e);
if (!file.exists()) {
return;
}
try {
TimeUnit.SECONDS.sleep(1); // 等待 1 避免插件未卸载完毕
if (!file.delete()) {
log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName());
}
} catch (InterruptedException e) {
log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e);
}
}
@ -120,25 +117,25 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
String pluginKey = pluginInfoDo.getPluginKey();
PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
// TODO @haohao改成 if return 会更简洁一点
if (plugin != null) {
// 启动插件
if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
&& plugin.getPluginState() != PluginState.STARTED) {
pluginManager.startPlugin(pluginKey);
log.info("已启动插件: {}", pluginKey);
}
// 停止插件
else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
&& plugin.getPluginState() == PluginState.STARTED) {
pluginManager.stopPlugin(pluginKey);
log.info("已停止插件: {}", pluginKey);
}
} else {
if (plugin == null) {
// 插件不存在且状态为停止抛出异常
if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID);
}
return;
}
// 启动插件
if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
&& plugin.getPluginState() != PluginState.STARTED) {
pluginManager.startPlugin(pluginKey);
log.info("已启动插件: {}", pluginKey);
}
// 停止插件
else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
&& plugin.getPluginState() == PluginState.STARTED) {
pluginManager.stopPlugin(pluginKey);
log.info("已停止插件: {}", pluginKey);
}
}
@ -152,10 +149,10 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
Map<String, PluginInfoDO> pluginInfoMap = pluginInfos.stream()
.collect(Collectors.toMap(PluginInfoDO::getPluginKey, Function.identity()));
// 1.3 获取本机 IP MAC 地址
// 1.3 获取本机 IP MAC 地址,mac@uuid
String ip = NetUtil.getLocalhostStr();
String mac = NetUtil.getLocalMacAddress();
String mainId = MAIN_ID + "-" + mac;
String mainId = mac + "@" + MAIN_ID;
// 2. 遍历插件列表并保存为插件实例
for (PluginWrapper plugin : plugins) {
@ -173,14 +170,21 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
pluginInfo.getId());
if (pluginInstance == null) {
// 4.4 如果插件实例不存在则创建
pluginInstance = PluginInstanceDO.builder().pluginId(pluginInfo.getId()).mainId(MAIN_ID + "-" + mac)
.ip(ip).port(port).heartbeatAt(System.currentTimeMillis()).build();
pluginInstance = PluginInstanceDO.builder()
.pluginId(pluginInfo.getId())
.mainId(MAIN_ID + "-" + mac)
.ip(ip)
.port(port)
.heartbeatAt(System.currentTimeMillis())
.build();
pluginInstanceMapper.insert(pluginInstance);
} else {
// 2.2 情况二如果存在则更新 heartbeatAt
// TODO @haohao这里最好 new update避免并发更新虽然目前没有
pluginInstance.setHeartbeatAt(System.currentTimeMillis());
pluginInstanceMapper.updateById(pluginInstance);
PluginInstanceDO updatePluginInstance = PluginInstanceDO.builder()
.id(pluginInstance.getId())
.heartbeatAt(System.currentTimeMillis())
.build();
pluginInstanceMapper.updateById(updatePluginInstance);
}
}
}

View File

@ -5,24 +5,30 @@ import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import cn.iocoder.yudao.module.iot.api.device.dto.IotDeviceEventReportReqDTO;
import cn.iocoder.yudao.module.iot.api.device.dto.IotDevicePropertyReportReqDTO;
import cn.iocoder.yudao.module.iot.api.device.dto.IotDeviceStatusUpdateReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.client.RestTemplate;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
// TODO @haohao类注释写一下比较好
/**
* 用于通过 {@link RestTemplate} 向远程 IoT 服务发送设备数据相关的请求
* 包括设备状态更新事件数据上报属性数据上报等操作
*/
@Slf4j
@RequiredArgsConstructor
public class DeviceDataApiClient implements DeviceDataApi {
/**
* 用于发送 HTTP 请求的工具
*/
private final RestTemplate restTemplate;
private final String deviceDataUrl;
// 可以通过构造器把 RestTemplate baseUrl 注入进来
// TODO @haohao可以用 lombok 简化
public DeviceDataApiClient(RestTemplate restTemplate, String deviceDataUrl) {
this.restTemplate = restTemplate;
this.deviceDataUrl = deviceDataUrl;
}
/**
* 远程 IoT 服务的基础 URL
* 例如http://127.0.0.1:8080
*/
private final String deviceDataUrl;
// TODO @haohao返回结果不用 CommonResult
@Override
@ -43,17 +49,51 @@ public class DeviceDataApiClient implements DeviceDataApi {
return doPost(url, reportReqDTO, "reportDevicePropertyData");
}
// TODO @haohao未来可能有 get 类型哈
/**
* 将与远程服务交互的通用逻辑抽取成一个私有方法
* 发送 GET 请求
*
* @param <T> 请求体类型
* @param url 请求 URL
* @param requestBody 请求体
* @param actionName 操作名称
* @return 响应结果
*/
private <T> CommonResult<Boolean> doGet(String url, T requestBody, String actionName) {
log.info("[{}] Sending request to URL: {}", actionName, url);
try {
CommonResult<?> response = restTemplate.getForObject(url, CommonResult.class);
if (response != null && response.isSuccess()) {
return success(true);
} else {
log.warn("[{}] Request to URL: {} failed with response: {}", actionName, url, response);
return CommonResult.error(500, "Request failed");
}
} catch (Exception e) {
log.error("[{}] Error sending request to URL: {}", actionName, url, e);
return CommonResult.error(400, "Request error: " + e.getMessage());
}
}
/**
* 发送 POST 请求
*
* @param <T> 请求体类型
* @param url 请求 URL
* @param requestBody 请求体
* @param actionName 操作名称
* @return 响应结果
*/
private <T> CommonResult<Boolean> doPost(String url, T requestBody, String actionName) {
log.info("[{}] Sending request to URL: {}", actionName, url);
try {
// 这里指定返回类型为 CommonResult<?>根据后台服务返回的实际结构做调整
restTemplate.postForObject(url, requestBody, CommonResult.class);
// TODO @haohaocheck 结果是否成功
return success(true);
CommonResult<?> response = restTemplate.postForObject(url, requestBody, CommonResult.class);
if (response != null && response.isSuccess()) {
return success(true);
} else {
log.warn("[{}] Request to URL: {} failed with response: {}", actionName, url, response);
return CommonResult.error(500, "Request failed");
}
} catch (Exception e) {
log.error("[{}] Error sending request to URL: {}", actionName, url, e);
return CommonResult.error(400, "Request error: " + e.getMessage());

View File

@ -1,31 +0,0 @@
package cn.iocoder.yudao.module.iot.plugin.common.config;
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import cn.iocoder.yudao.module.iot.plugin.common.api.DeviceDataApiClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
// TODO @haohao这个最好是 autoconfiguration
@Configuration
public class DeviceDataApiInitializer {
// TODO @haohao这个要不搞个配置类哈
@Value("${iot.device-data.url}")
private String deviceDataUrl;
@Bean
public RestTemplate restTemplate() {
// TODO haohao如果你有更多的自定义需求比如连接池超时时间等可以在这里设置
return new RestTemplateBuilder().build();
}
// TODO @haohao不存在时才构建
@Bean
public DeviceDataApi deviceDataApi(RestTemplate restTemplate) {
return new DeviceDataApiClient(restTemplate, deviceDataUrl);
}
}

View File

@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.iot.plugin.common.config;
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
import cn.iocoder.yudao.module.iot.plugin.common.api.DeviceDataApiClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
/**
* 设备数据 API 初始化器
*
* @author haohao
*/
@AutoConfiguration
public class YudaoDeviceDataApiAutoConfiguration {
// TODO @haohao这个要不搞个配置类哈
@Value("${iot.device-data.url}")
private String deviceDataUrl;
/**
* 创建 RestTemplate 实例
*
* @return RestTemplate 实例
*/
@Bean
public RestTemplate restTemplate() {
// 如果你有更多的自定义需求比如连接池超时时间等可以在这里设置
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofMillis(5000)) // 设置连接超时时间
.setReadTimeout(Duration.ofMillis(5000)) // 设置读取超时时间
.build();
}
/**
* 创建 DeviceDataApi 实例
*
* @param restTemplate RestTemplate 实例
* @return DeviceDataApi 实例
*/
@Bean
public DeviceDataApi deviceDataApi(RestTemplate restTemplate) {
return new DeviceDataApiClient(restTemplate, deviceDataUrl);
}
}

View File

@ -0,0 +1 @@
cn.iocoder.yudao.module.iot.plugin.common.config.YudaoDeviceDataApiAutoConfiguration

View File

@ -11,7 +11,7 @@ import org.springframework.context.ConfigurableApplicationContext;
* 独立运行入口
*/
@Slf4j
@SpringBootApplication(scanBasePackages = "cn.iocoder.yudao.module.iot.plugin") // TODO @haohao建议不扫描 cn.iocoder.yudao.module.iot.plugin而是通过自动配置初始化 common
@SpringBootApplication
public class HttpPluginSpringbootApplication {
public static void main(String[] args) {
@ -21,6 +21,7 @@ public class HttpPluginSpringbootApplication {
// 手动获取 VertxService 并启动
// TODO @haohao可以放在 bean init 里么
// 会和插件模式冲突
VertxService vertxService = context.getBean(VertxService.class);
vertxService.startServer();

View File

@ -21,30 +21,41 @@ public class HttpVertxPlugin extends SpringPlugin {
@Override
public void start() {
// TODO @haohao这种最好启动中启动完成成对打印日志方便定位问题
log.info("[HttpVertxPlugin][start ...]");
log.info("[HttpVertxPlugin][start][begin] 开始启动 HttpVertxPlugin 插件...");
// 1. 获取插件上下文
ApplicationContext pluginContext = getApplicationContext();
if (pluginContext == null) {
log.error("[HttpVertxPlugin] pluginContext is null, start failed.");
return;
try {
// 1. 获取插件上下文
ApplicationContext pluginContext = getApplicationContext();
if (pluginContext == null) {
log.error("[HttpVertxPlugin][start][fail] pluginContext is null, 启动失败!");
return;
}
// 2. 启动 Vert.x
VertxService vertxService = pluginContext.getBean(VertxService.class);
vertxService.startServer();
log.info("[HttpVertxPlugin][start][end] 启动完成");
} catch (Exception e) {
log.error("[HttpVertxPlugin][start][exception] 启动过程出现异常!", e);
}
// 2. 启动 Vertx
VertxService vertxService = pluginContext.getBean(VertxService.class);
vertxService.startServer();
}
@Override
public void stop() {
log.info("[HttpVertxPlugin][stop ...]");
ApplicationContext pluginContext = getApplicationContext();
if (pluginContext != null) {
// 停止服务器
VertxService vertxService = pluginContext.getBean(VertxService.class);
vertxService.stopServer();
log.info("[HttpVertxPlugin][stop][begin] 开始停止 HttpVertxPlugin 插件...");
try {
ApplicationContext pluginContext = getApplicationContext();
if (pluginContext != null) {
// 停止服务器
VertxService vertxService = pluginContext.getBean(VertxService.class);
vertxService.stopServer();
}
log.info("[HttpVertxPlugin][stop][end] 停止完成");
} catch (Exception e) {
log.error("[HttpVertxPlugin][stop][exception] 停止过程出现异常!", e);
}
}
@ -68,5 +79,4 @@ public class HttpVertxPlugin extends SpringPlugin {
return pluginContext;
}
}
}

View File

@ -34,6 +34,10 @@ public class HttpVertxPluginConfiguration {
/**
* 创建路由
*
* @param vertx Vertx 实例
* @param httpVertxHandler HttpVertxHandler 实例
* @return Router 实例
*/
@Bean
public Router router(Vertx vertx, HttpVertxHandler httpVertxHandler) {
@ -50,6 +54,12 @@ public class HttpVertxPluginConfiguration {
return router;
}
/**
* 创建 HttpVertxHandler 实例
*
* @param deviceDataApi DeviceDataApi 实例
* @return HttpVertxHandler 实例
*/
@Bean
public HttpVertxHandler httpVertxHandler(DeviceDataApi deviceDataApi) {
return new HttpVertxHandler(deviceDataApi);
@ -58,6 +68,10 @@ public class HttpVertxPluginConfiguration {
/**
* 定义一个 VertxService 来管理服务器启动逻辑
* 无论是独立运行还是插件方式都可以共用此类
*
* @param vertx Vertx 实例
* @param router Router 实例
* @return VertxService 实例
*/
@Bean
public VertxService vertxService(Vertx vertx, Router router) {

View File

@ -5,3 +5,8 @@ spring:
iot:
device-data:
url: http://127.0.0.1:48080
plugin:
http:
server:
port: 8092