Commit 603649d2485ec36f30b77ae2a17957fb4bf28100

Authored by 安浩浩
1 parent dc1f9338

【功能完善】IoT: 新增 MQTT RPC 支持,包含请求和响应模型、序列化工具、MQTT 配置及客户端/服务器实现,提供示例服务和控制器接口,优化插件结构以支持 HTTP 插件的集成。

Showing 15 changed files with 471 additions and 12 deletions
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcRequest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.common;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * MQTT RPC 请求
  10 + *
  11 + */
  12 +@Data
  13 +@Builder
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
  16 +public class RpcRequest {
  17 +
  18 + /**
  19 + * 方法名
  20 + */
  21 + private String method;
  22 +
  23 + /**
  24 + * 参数
  25 + */
  26 + private Object[] params;
  27 +
  28 + /**
  29 + * 关联 ID
  30 + */
  31 + private String correlationId;
  32 +
  33 + /**
  34 + * 回复地址
  35 + */
  36 + private String replyTo;
  37 +}
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcResponse.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.common;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * MQTT RPC 响应
  10 + *
  11 + */
  12 +@Data
  13 +@Builder
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
  16 +public class RpcResponse {
  17 +
  18 + /**
  19 + * 关联 ID
  20 + */
  21 + private String correlationId;
  22 +
  23 + /**
  24 + * 结果
  25 + */
  26 + private Object result;
  27 +
  28 + /**
  29 + * 错误
  30 + */
  31 + private String error;
  32 +}
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/SerializationUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.common;
  2 +
  3 +import cn.hutool.json.JSONUtil;
  4 +
  5 +/**
  6 + * 序列化工具类
  7 + *
  8 + */
  9 +public class SerializationUtils {
  10 +
  11 + public static String serialize(Object obj) {
  12 + return JSONUtil.toJsonStr(obj);
  13 + }
  14 +
  15 + public static <T> T deserialize(String json, Class<T> clazz) {
  16 + return JSONUtil.toBean(json, clazz);
  17 + }
  18 +}
0 19 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.context.annotation.Configuration;
  6 +
  7 +@Data
  8 +@Configuration
  9 +@ConfigurationProperties(prefix = "mqtt")
  10 +public class MqttConfig {
  11 + /**
  12 + * MQTT 代理地址
  13 + */
  14 + private String broker;
  15 +
  16 + /**
  17 + * MQTT 用户名
  18 + */
  19 + private String username;
  20 +
  21 + /**
  22 + * MQTT 密码
  23 + */
  24 + private String password;
  25 +
  26 + /**
  27 + * MQTT 客户端 ID
  28 + */
  29 + private String clientId;
  30 +
  31 + /**
  32 + * MQTT 请求主题
  33 + */
  34 + private String requestTopic;
  35 +
  36 + /**
  37 + * MQTT 响应主题前缀
  38 + */
  39 + private String responseTopicPrefix;
  40 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/server/RpcServer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.server;
  2 +
  3 +import cn.hutool.core.lang.UUID;
  4 +import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest;
  5 +import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;
  6 +import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;
  7 +import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +import org.eclipse.paho.client.mqttv3.*;
  10 +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
  11 +import org.springframework.stereotype.Service;
  12 +
  13 +import javax.annotation.PostConstruct;
  14 +import javax.annotation.PreDestroy;
  15 +import java.util.HashMap;
  16 +import java.util.Map;
  17 +
  18 +@Service
  19 +@Slf4j
  20 +public class RpcServer {
  21 +
  22 + private final MqttConfig mqttConfig;
  23 + private final MqttClient mqttClient;
  24 + private final Map<String, MethodInvoker> methodRegistry = new HashMap<>();
  25 +
  26 + public RpcServer(MqttConfig mqttConfig) throws MqttException {
  27 + this.mqttConfig = mqttConfig;
  28 + this.mqttClient = new MqttClient(mqttConfig.getBroker(), "rpc-server-" + UUID.randomUUID(), new MemoryPersistence());
  29 + MqttConnectOptions options = new MqttConnectOptions();
  30 + options.setAutomaticReconnect(true);
  31 + options.setCleanSession(true);
  32 + options.setUserName(mqttConfig.getUsername());
  33 + options.setPassword(mqttConfig.getPassword().toCharArray());
  34 + this.mqttClient.connect(options);
  35 + }
  36 +
  37 + @PostConstruct
  38 + public void init() throws MqttException {
  39 + mqttClient.subscribe(mqttConfig.getRequestTopic(), this::handleRequest);
  40 + log.info("RPC Server subscribed to topic: {}", mqttConfig.getRequestTopic());
  41 + }
  42 +
  43 + private void handleRequest(String topic, MqttMessage message) {
  44 + RpcRequest request = SerializationUtils.deserialize(new String(message.getPayload()), RpcRequest.class);
  45 + RpcResponse response = new RpcResponse();
  46 + response.setCorrelationId(request.getCorrelationId());
  47 +
  48 + try {
  49 + MethodInvoker invoker = methodRegistry.get(request.getMethod());
  50 + if (invoker == null) {
  51 + throw new NoSuchMethodException("Unknown method: " + request.getMethod());
  52 + }
  53 + Object result = invoker.invoke(request.getParams());
  54 + response.setResult(result);
  55 + } catch (Exception e) {
  56 + response.setError(e.getMessage());
  57 + log.error("Error processing RPC request: {}", e.getMessage(), e);
  58 + }
  59 +
  60 + String replyPayload = SerializationUtils.serialize(response);
  61 + MqttMessage replyMessage = new MqttMessage(replyPayload.getBytes());
  62 + replyMessage.setQos(1);
  63 + try {
  64 + mqttClient.publish(request.getReplyTo(), replyMessage);
  65 + log.info("Published response to {}", request.getReplyTo());
  66 + } catch (MqttException e) {
  67 + log.error("Failed to publish response: {}", e.getMessage(), e);
  68 + }
  69 + }
  70 +
  71 + /**
  72 + * 注册可调用的方法
  73 + *
  74 + * @param methodName 方法名称
  75 + * @param invoker 方法调用器
  76 + */
  77 + public void registerMethod(String methodName, MethodInvoker invoker) {
  78 + methodRegistry.put(methodName, invoker);
  79 + log.info("Registered method: {}", methodName);
  80 + }
  81 +
  82 + @PreDestroy
  83 + public void cleanup() throws MqttException {
  84 + mqttClient.disconnect();
  85 + log.info("RPC Server disconnected");
  86 + }
  87 +
  88 + /**
  89 + * 方法调用器接口
  90 + */
  91 + @FunctionalInterface
  92 + public interface MethodInvoker {
  93 + Object invoke(Object[] params) throws Exception;
  94 + }
  95 +}
0 96 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/ExampleService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.plugin;
  2 +
  3 +import cn.iocoder.yudao.module.iot.mqttrpc.server.RpcServer;
  4 +import lombok.RequiredArgsConstructor;
  5 +import org.springframework.stereotype.Service;
  6 +
  7 +import javax.annotation.PostConstruct;
  8 +
  9 +@Service
  10 +@RequiredArgsConstructor
  11 +public class ExampleService {
  12 +
  13 + private final RpcServer rpcServer;
  14 +
  15 + @PostConstruct
  16 + public void registerMethods() {
  17 + rpcServer.registerMethod("add", params -> {
  18 + if (params.length != 2) {
  19 + throw new IllegalArgumentException("add方法需要两个参数");
  20 + }
  21 + int a = ((Number) params[0]).intValue();
  22 + int b = ((Number) params[1]).intValue();
  23 + return add(a, b);
  24 + });
  25 +
  26 + rpcServer.registerMethod("concat", params -> {
  27 + if (params.length != 2) {
  28 + throw new IllegalArgumentException("concat方法需要两个参数");
  29 + }
  30 + String str1 = params[0].toString();
  31 + String str2 = params[1].toString();
  32 + return concat(str1, str2);
  33 + });
  34 + }
  35 +
  36 + private int add(int a, int b) {
  37 + return a + b;
  38 + }
  39 +
  40 + private String concat(String a, String b) {
  41 + return a + b;
  42 + }
  43 +}
0 44 \ 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
... ... @@ -18,7 +18,6 @@ import org.springframework.stereotype.Service;
18 18 import org.springframework.validation.annotation.Validated;
19 19 import org.springframework.web.multipart.MultipartFile;
20 20  
21   -import javax.annotation.PostConstruct;
22 21 import java.io.File;
23 22 import java.io.IOException;
24 23 import java.nio.file.*;
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml
... ... @@ -150,5 +150,10 @@
150 150 <artifactId>netty-all</artifactId>
151 151 <version>4.1.63.Final</version> <!-- 版本可根据需要调整 -->
152 152 </dependency>
  153 + <!-- MQTT -->
  154 + <dependency>
  155 + <groupId>org.eclipse.paho</groupId>
  156 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  157 + </dependency>
153 158 </dependencies>
154 159 </project>
155 160 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/HttpPluginSpringbootApplication.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot;
  2 +
  3 +import org.springframework.boot.SpringApplication;
  4 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  5 +
  6 +@SpringBootApplication
  7 +public class HttpPluginSpringbootApplication {
  8 + public static void main(String[] args) {
  9 + SpringApplication.run(HttpPluginSpringbootApplication.class, args);
  10 + }
  11 +}
0 12 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/controller/RpcController.java 0 → 100644
  1 +
  2 +package cn.iocoder.yudao.module.iot.controller;
  3 +
  4 +import cn.iocoder.yudao.module.iot.mqttrpc.client.RpcClient;
  5 +import lombok.RequiredArgsConstructor;
  6 +import org.springframework.web.bind.annotation.PostMapping;
  7 +import org.springframework.web.bind.annotation.RequestMapping;
  8 +import org.springframework.web.bind.annotation.RequestParam;
  9 +import org.springframework.web.bind.annotation.RestController;
  10 +
  11 +import javax.annotation.Resource;
  12 +import java.util.concurrent.CompletableFuture;
  13 +
  14 +@RestController
  15 +@RequestMapping("/rpc")
  16 +@RequiredArgsConstructor
  17 +public class RpcController {
  18 +
  19 + @Resource
  20 + private RpcClient rpcClient;
  21 +
  22 + @PostMapping("/add")
  23 + public CompletableFuture<Object> add(@RequestParam int a, @RequestParam int b) throws Exception {
  24 + return rpcClient.call("add", new Object[]{a, b}, 10);
  25 + }
  26 +
  27 + @PostMapping("/concat")
  28 + public CompletableFuture<Object> concat(@RequestParam String str1, @RequestParam String str2) throws Exception {
  29 + return rpcClient.call("concat", new Object[]{str1, str2}, 10);
  30 + }
  31 +}
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/client/RpcClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.client;
  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;
  10 +import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;
  11 +import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;
  12 +import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.eclipse.paho.client.mqttv3.*;
  15 +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
  16 +import org.springframework.stereotype.Service;
  17 +
  18 +import javax.annotation.PostConstruct;
  19 +import javax.annotation.PreDestroy;
  20 +import java.util.UUID;
  21 +import java.util.concurrent.*;
  22 +
  23 +@Service
  24 +@Slf4j
  25 +public class RpcClient {
  26 +
  27 + private final MqttConfig mqttConfig;
  28 + private final MqttClient mqttClient;
  29 + private final ConcurrentMap<String, CompletableFuture<RpcResponse>> pendingRequests = new ConcurrentHashMap<>();
  30 + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
  31 +
  32 + public RpcClient(MqttConfig mqttConfig) throws MqttException {
  33 + this.mqttConfig = mqttConfig;
  34 + this.mqttClient = new MqttClient(mqttConfig.getBroker(), mqttConfig.getClientId(), new MemoryPersistence());
  35 + MqttConnectOptions options = new MqttConnectOptions();
  36 + options.setAutomaticReconnect(true);
  37 + options.setCleanSession(true);
  38 + options.setUserName(mqttConfig.getUsername());
  39 + options.setPassword(mqttConfig.getPassword().toCharArray());
  40 + this.mqttClient.connect(options);
  41 + }
  42 +
  43 + @PostConstruct
  44 + public void init() throws MqttException {
  45 + mqttClient.subscribe(mqttConfig.getResponseTopicPrefix() + "#", this::handleResponse);
  46 + log.info("RPC Client subscribed to topics: {}", mqttConfig.getResponseTopicPrefix() + "#");
  47 + }
  48 +
  49 + private void handleResponse(String topic, MqttMessage message) {
  50 + String correlationId = topic.substring(mqttConfig.getResponseTopicPrefix().length());
  51 + RpcResponse response = SerializationUtils.deserialize(new String(message.getPayload()), RpcResponse.class);
  52 + CompletableFuture<RpcResponse> future = pendingRequests.remove(correlationId);
  53 + if (future != null) {
  54 + if (response.getError() != null) {
  55 + future.completeExceptionally(new RuntimeException(response.getError()));
  56 + } else {
  57 + future.complete(response);
  58 + }
  59 + } else {
  60 + log.warn("Received response for unknown correlationId: {}", correlationId);
  61 + }
  62 + }
  63 +
  64 + public CompletableFuture<Object> call(String method, Object[] params, int timeoutSeconds) throws MqttException {
  65 + String correlationId = UUID.randomUUID().toString();
  66 + String replyTo = mqttConfig.getResponseTopicPrefix() + correlationId;
  67 +
  68 + RpcRequest request = new RpcRequest(method, params, correlationId, replyTo);
  69 + String payload = SerializationUtils.serialize(request);
  70 + MqttMessage message = new MqttMessage(payload.getBytes());
  71 + message.setQos(1);
  72 + mqttClient.publish(mqttConfig.getRequestTopic(), message);
  73 +
  74 + CompletableFuture<RpcResponse> futureResponse = new CompletableFuture<>();
  75 + pendingRequests.put(correlationId, futureResponse);
  76 +
  77 + // 设置超时
  78 + scheduler.schedule(() -> {
  79 + CompletableFuture<RpcResponse> removed = pendingRequests.remove(correlationId);
  80 + if (removed != null) {
  81 + removed.completeExceptionally(new TimeoutException("RPC call timed out"));
  82 + }
  83 + }, timeoutSeconds, TimeUnit.SECONDS);
  84 +
  85 + // 返回最终的结果
  86 + return futureResponse.thenApply(RpcResponse::getResult);
  87 + }
  88 +
  89 + @PreDestroy
  90 + public void cleanup() throws MqttException {
  91 + mqttClient.disconnect();
  92 + scheduler.shutdown();
  93 + log.info("RPC Client disconnected");
  94 + }
  95 +}
0 96 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mqttrpc.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.context.annotation.Configuration;
  6 +
  7 +@Data
  8 +@Configuration
  9 +@ConfigurationProperties(prefix = "mqtt")
  10 +public class MqttConfig {
  11 +
  12 + /**
  13 + * MQTT 代理地址
  14 + */
  15 + private String broker;
  16 +
  17 + /**
  18 + * MQTT 用户名
  19 + */
  20 + private String username;
  21 +
  22 + /**
  23 + * MQTT 密码
  24 + */
  25 + private String password;
  26 +
  27 + /**
  28 + * MQTT 客户端 ID
  29 + */
  30 + private String clientId;
  31 +
  32 + /**
  33 + * MQTT 请求主题
  34 + */
  35 + private String requestTopic;
  36 +
  37 + /**
  38 + * MQTT 响应主题前缀
  39 + */
  40 + private String responseTopicPrefix;
  41 +}
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/resources/application.yml 0 → 100644
  1 +server:
  2 + port: 8092
  3 +
  4 +spring:
  5 + application:
  6 + name: yudao-module-iot-http-plugin
  7 +
  8 +# MQTT-RPC 配置
  9 +mqtt:
  10 + broker: tcp://chaojiniu.top:1883
  11 + username: haohao
  12 + password: ahh@123456
  13 + clientId: mqtt-rpc-client-${random.int}
  14 + requestTopic: rpc/request
  15 + responseTopicPrefix: rpc/response/
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/pom.xml
... ... @@ -145,10 +145,10 @@
145 145 <version>${lombok.version}</version>
146 146 <scope>provided</scope>
147 147 </dependency>
  148 + <!-- MQTT -->
148 149 <dependency>
149   - <groupId>io.netty</groupId>
150   - <artifactId>netty-all</artifactId>
151   - <version>4.1.63.Final</version> <!-- 版本可根据需要调整 -->
  150 + <groupId>org.eclipse.paho</groupId>
  151 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
152 152 </dependency>
153 153 </dependencies>
154 154 </project>
155 155 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java
1 1 package cn.iocoder.yudao.module.iot.plugin;
2 2  
3   -import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
4 3 import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
  4 +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
5 5 import lombok.extern.slf4j.Slf4j;
6   -import org.pf4j.PluginWrapper;
7 6 import org.pf4j.Plugin;
  7 +import org.pf4j.PluginWrapper;
8 8  
  9 +import javax.annotation.Resource;
9 10 import java.util.concurrent.ExecutorService;
10 11 import java.util.concurrent.Executors;
11 12  
... ... @@ -13,11 +14,11 @@ import java.util.concurrent.Executors;
13 14 public class MqttPlugin extends Plugin {
14 15  
15 16 private ExecutorService executorService;
  17 + @Resource
16 18 private DeviceDataApi deviceDataApi;
17 19  
18 20 public MqttPlugin(PluginWrapper wrapper) {
19 21 super(wrapper);
20   - // 初始化线程池
21 22 this.executorService = Executors.newSingleThreadExecutor();
22 23 }
23 24  
... ... @@ -25,24 +26,20 @@ public class MqttPlugin extends Plugin {
25 26 public void start() {
26 27 log.info("MqttPlugin.start()");
27 28  
28   - // 重新初始化线程池,确保它是活跃的
29 29 if (executorService.isShutdown() || executorService.isTerminated()) {
30 30 executorService = Executors.newSingleThreadExecutor();
31 31 }
32 32  
33   - // 从 ServiceRegistry 中获取主程序暴露的 DeviceDataApi 接口实例
34 33 deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
35 34 if (deviceDataApi == null) {
36 35 log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
37 36 return;
38 37 }
  38 +
39 39 }
40 40  
41 41 @Override
42 42 public void stop() {
43 43 log.info("MqttPlugin.stop()");
44   - // 停止线程池
45   - executorService.shutdownNow();
46 44 }
47   -
48 45 }
49 46 \ No newline at end of file
... ...