diff --git a/admin-ui/package.json b/admin-ui/package.json index 2c8191c..6048826 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -17,7 +17,8 @@ "@microsoft/fetch-event-source": "2.0.1", "@element-plus/icons-vue": "2.3.1", "@vueup/vue-quill": "1.1.0", - "@vueuse/core": "9.5.0", + "@vueuse/components": "^12.7.0", + "@vueuse/core": "^12.7.0", "axios": "0.27.2", "crypto-js": "4.2.0", "echarts": "5.4.0", @@ -25,9 +26,11 @@ "file-saver": "2.0.5", "fuse.js": "6.6.2", "js-cookie": "3.0.1", + "highlight.js": "^11.11.1", "jsencrypt": "3.3.1", "mescroll.js": "1.4.2", "mitt": "3.0.1", + "normalize.css": "^8.0.1", "nprogress": "0.2.0", "pinia": "2.0.22", "pinia-plugin-persist": "1.0.0", diff --git a/admin-ui/pnpm-lock.yaml b/admin-ui/pnpm-lock.yaml index 382e07c..7155052 100644 --- a/admin-ui/pnpm-lock.yaml +++ b/admin-ui/pnpm-lock.yaml @@ -17,9 +17,12 @@ importers: '@vueup/vue-quill': specifier: 1.1.0 version: 1.1.0(vue@3.5.13) + '@vueuse/components': + specifier: ^12.7.0 + version: 12.8.2 '@vueuse/core': - specifier: 9.5.0 - version: 9.5.0(vue@3.5.13) + specifier: ^12.7.0 + version: 12.8.2 axios: specifier: 0.27.2 version: 0.27.2 @@ -38,6 +41,9 @@ importers: fuse.js: specifier: 6.6.2 version: 6.6.2 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 js-cookie: specifier: 3.0.1 version: 3.0.1 @@ -50,6 +56,9 @@ importers: mitt: specifier: 3.0.1 version: 3.0.1 + normalize.css: + specifier: ^8.0.1 + version: 8.0.1 nprogress: specifier: 0.2.0 version: 0.2.0 @@ -86,7 +95,7 @@ importers: version: 1.56.1 unplugin-auto-import: specifier: 0.11.4 - version: 0.11.4(@vueuse/core@9.5.0(vue@3.5.13))(rollup@4.30.0)(webpack-sources@3.2.3) + version: 0.11.4(@vueuse/core@12.8.2)(rollup@4.30.0)(webpack-sources@3.2.3) vite: specifier: 5.4.11 version: 5.4.11(@types/node@22.7.5)(sass@1.56.1) @@ -339,61 +348,51 @@ packages: resolution: {integrity: sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.30.0': resolution: {integrity: sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.30.0': resolution: {integrity: sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.30.0': resolution: {integrity: sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.30.0': resolution: {integrity: sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.30.0': resolution: {integrity: sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.30.0': resolution: {integrity: sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.30.0': resolution: {integrity: sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.30.0': resolution: {integrity: sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.30.0': resolution: {integrity: sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.30.0': resolution: {integrity: sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==} @@ -435,6 +434,9 @@ packages: '@types/web-bluetooth@0.0.16': resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@vitejs/plugin-vue@4.6.2': resolution: {integrity: sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -497,12 +499,24 @@ packages: peerDependencies: vue: ^3.2.41 + '@vueuse/components@12.8.2': + resolution: {integrity: sha512-Nj27u1KsDWzoTthlChzVndJ9g0sW5APCXO3EJkSxlG11nN/ANTUlPPeoJOFvtbdDRnvsMJalboJyE0rRyg7yNg==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + '@vueuse/core@9.5.0': resolution: {integrity: sha512-6GsWBsJHEb3sYw15mbLrcbslAVY45pkzjJYTKYKCXv88z7srAF0VEW0q+oXKsl58tCbqooplInahXFg8Yo1m4w==} + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + '@vueuse/metadata@9.5.0': resolution: {integrity: sha512-4M1AyPZmIv41pym+K5+4wup3bKuYebbH8w8BROY1hmT7rIwcyS4tEL+UsGz0Hiu1FCOxcoBrwtAizc0YmBJjyQ==} + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + '@vueuse/shared@9.5.0': resolution: {integrity: sha512-HnnCWU1Vg9CVWRCcI8ohDKDRB2Sc4bTgT1XAIaoLSfVHHn+TKbrox6pd3klCSw4UDxkhDfOk8cAdcK+Z5KleCA==} @@ -1011,6 +1025,10 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + htmlparser2@3.10.1: resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} @@ -1306,6 +1324,9 @@ packages: normalize-wheel-es@1.2.0: resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + normalize.css@8.0.1: + resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==} + nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} @@ -2050,6 +2071,8 @@ snapshots: '@types/web-bluetooth@0.0.16': {} + '@types/web-bluetooth@0.0.21': {} + '@vitejs/plugin-vue@4.6.2(vite@5.4.11(@types/node@22.7.5)(sass@1.56.1))(vue@3.5.13)': dependencies: vite: 5.4.11(@types/node@22.7.5)(sass@1.56.1) @@ -2157,6 +2180,23 @@ snapshots: quill-delta: 4.2.2 vue: 3.5.13 + '@vueuse/components@12.8.2': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.13 + transitivePeerDependencies: + - typescript + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.13 + transitivePeerDependencies: + - typescript + '@vueuse/core@9.5.0(vue@3.5.13)': dependencies: '@types/web-bluetooth': 0.0.16 @@ -2167,8 +2207,16 @@ snapshots: - '@vue/composition-api' - vue + '@vueuse/metadata@12.8.2': {} + '@vueuse/metadata@9.5.0': {} + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.13 + transitivePeerDependencies: + - typescript + '@vueuse/shared@9.5.0(vue@3.5.13)': dependencies: vue-demi: 0.14.10(vue@3.5.13) @@ -2802,6 +2850,8 @@ snapshots: he@1.2.0: {} + highlight.js@11.11.1: {} + htmlparser2@3.10.1: dependencies: domelementtype: 1.3.1 @@ -3094,6 +3144,8 @@ snapshots: normalize-wheel-es@1.2.0: {} + normalize.css@8.0.1: {} + nprogress@0.2.0: {} nth-check@2.1.1: @@ -3597,7 +3649,7 @@ snapshots: universalify@2.0.1: {} - unplugin-auto-import@0.11.4(@vueuse/core@9.5.0(vue@3.5.13))(rollup@4.30.0)(webpack-sources@3.2.3): + unplugin-auto-import@0.11.4(@vueuse/core@12.8.2)(rollup@4.30.0)(webpack-sources@3.2.3): dependencies: '@antfu/utils': 0.6.3 '@rollup/pluginutils': 5.1.2(rollup@4.30.0) @@ -3606,7 +3658,7 @@ snapshots: unimport: 0.7.1(rollup@4.30.0)(webpack-sources@3.2.3) unplugin: 0.10.2 optionalDependencies: - '@vueuse/core': 9.5.0(vue@3.5.13) + '@vueuse/core': 12.8.2 transitivePeerDependencies: - rollup - webpack-sources diff --git a/admin-ui/src/main.js b/admin-ui/src/main.js index 9a86094..73fbfc1 100644 --- a/admin-ui/src/main.js +++ b/admin-ui/src/main.js @@ -6,7 +6,7 @@ import ElementPlus from 'element-plus' // import locale from 'element-plus/lib/locale/lang/zh-cn' // 中文语言 import zhCn from 'element-plus/dist/locale/zh-cn.mjs' - +import 'normalize.css' // CSS重置样式 import '@/assets/styles/index.scss' // global css import App from './App' import { store, useSettingsStore } from './store' diff --git a/auth-inner-server/.gitignore b/auth-inner-server/.gitignore new file mode 100644 index 0000000..48e8504 --- /dev/null +++ b/auth-inner-server/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Logs +logs/ +*.log + +# Environment files +.env* + +# IDE files +.vscode/ +.idea/ + +# OS generated files +.DS_Store +Thumbs.db + +# Build outputs +dist/ +build/ + +# PM2 logs +.pm2/ + +# Docker +.dockerignore + +# pnpm +pnpm-lock.yaml + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/auth-inner-server/Dockerfile b/auth-inner-server/Dockerfile new file mode 100644 index 0000000..62096a5 --- /dev/null +++ b/auth-inner-server/Dockerfile @@ -0,0 +1,35 @@ +# Use the official Node.js image as the base image +FROM node:22.16.0 AS builder + +# Set the working directory inside the container +WORKDIR /app + +# Copy package.json and package-lock.json (if available) to the working directory +COPY package*.json ./ + +# Install the application dependencies +RUN npm install + +# Copy the rest of the application code to the working directory +COPY . . + +# Production stage +FROM node:22.16.0-alpine AS production + +# Set the working directory inside the container +WORKDIR /app + +# Copy dependencies from builder stage +COPY --from=builder /app/node_modules ./node_modules + +# Copy application code from builder stage +COPY --from=builder /app/. . + +# Install pm2 globally +RUN npm install -g pm2 + +# Expose the port that the application listens on +EXPOSE 3000 + +# Define the command to run the application in production mode with pm2.json config +CMD ["pm2", "start", "pm2.json", "--no-daemon"] \ No newline at end of file diff --git a/auth-inner-server/README.md b/auth-inner-server/README.md new file mode 100644 index 0000000..f31f43e --- /dev/null +++ b/auth-inner-server/README.md @@ -0,0 +1,3 @@ +# 内部授权服务器 + +为内部第三方提供授权restful-api,如:emqx,zlmediakit diff --git a/auth-inner-server/index.js b/auth-inner-server/index.js new file mode 100644 index 0000000..5b0fe3d --- /dev/null +++ b/auth-inner-server/index.js @@ -0,0 +1,27 @@ +const express = require('express'); +const app = express(); +const port = process.env.PORT || 3000; +const username = process.env.AUTH_USERNAME || 'admin'; +const password = process.env.AUTH_PASSWORD || '3.1415926' +const authUrl = process.env.AUTH_URL || 'http://127.0.0.1:8080' + +// Middleware to parse JSON bodies +app.use(express.json()); +app.use(express.urlencoded({extended: true})); + +// RESTful routes +app.get('/', (req, res) => { + res.json({message: 'Welcome to the auth-inner-server API'}); +}); + +app.post('/emqx-login', (req, res) => { + if (req.body.username === username && req.body.password === password) { + res.json({is_superuser: true, result: 'allow'}); + } else { + res.json({is_superuser: false, result: 'deny'}); + } +}) + +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); diff --git a/auth-inner-server/package.json b/auth-inner-server/package.json new file mode 100644 index 0000000..5729168 --- /dev/null +++ b/auth-inner-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "auth-inner-server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js", + "prod": "pm2 start index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.21.2", + "pm2": "^6.0.8" + }, + "devDependencies": { + "nodemon": "^3.1.10" + }, + "engines": { + "node": "22.16.0" + } +} diff --git a/auth-inner-server/pm2.json b/auth-inner-server/pm2.json new file mode 100644 index 0000000..d8af602 --- /dev/null +++ b/auth-inner-server/pm2.json @@ -0,0 +1,25 @@ +{ + "apps": [ + { + "name": "auth-inner-server", + "script": "index.js", + "watch": [ + "app" + ], + "ignore_watch": [ + "app/public" + ], + "log_date_format": "YYYY-MM-DD HH:mm Z", + "error_file": "./logs/pm2-err.log", + "out_file": "./logs/pm2-out.log", + "merge_logs": true, + "exec_mode": "fork", + "max_memory_restart": "200M", + "autorestart": true, + "env": { + "NODE_ENV": "prd" + }, + "instances": 1 + } + ] +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 179cd96..7995e26 100644 --- a/pom.xml +++ b/pom.xml @@ -370,6 +370,7 @@ ruoyi-common ruoyi-demo ruoyi-sms + ruoyi-mqtt pom diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index d469040..51a5226 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -73,6 +73,15 @@ ruoyi-system-file + + com.ruoyi + ruoyi-mqtt + + + cn.hutool + hutool-cache + + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index 5d21009..f24e08e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -91,7 +91,7 @@ public class SysLoginController { Map ret = MapUtil.newHashMap(); ret.put("is_superuser",false); ret.put("result","deny"); - if(loginBody.getUsername().equals("energy2") && loginBody.getPassword().equals("energy21415926")){ + if(loginBody.getUsername().equals(config.getName()) && loginBody.getPassword().equals(config.getName()+"1415926")){ ret.put("is_superuser",true); ret.put("result","allow"); return ret; diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/event/QaEvent.java b/ruoyi-common/src/main/java/com/ruoyi/common/event/QaEvent.java index 2303ebf..84c6e72 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/event/QaEvent.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/event/QaEvent.java @@ -3,7 +3,7 @@ package com.ruoyi.common.event; import org.springframework.context.ApplicationEvent; /** - * 需要触发energy2知识库的增删改查事件 + * 需要触发base2024知识库的增删改查事件 */ public class QaEvent extends ApplicationEvent { diff --git a/ruoyi-mqtt/README.md b/ruoyi-mqtt/README.md new file mode 100644 index 0000000..3d0bd0a --- /dev/null +++ b/ruoyi-mqtt/README.md @@ -0,0 +1,332 @@ +在物联网(IoT)和分布式系统中,MQTT(Message Queuing Telemetry Transport)是一种轻量级、基于发布/订阅模式的消息传输协议,特别适合带宽有限、网络不稳定的场景。Spring Boot作为主流的Java开发框架,通过集成MQTT客户端库(如Eclipse Paho)可以快速实现MQTT通信功能。 + + +### 一、MQTT核心概念 +在详解Spring Boot集成MQTT前,需先了解几个核心概念: +- **Broker**:MQTT服务器(如Eclipse Mosquitto、EMQX),负责接收客户端发送的消息并转发给订阅者。 +- **客户端(Client)**:分为发布者(Publisher)和订阅者(Subscriber),同一客户端可同时扮演两种角色。 +- **主题(Topic)**:消息的分类标识(如`device/temp`),支持层级结构和通配符(`+`匹配单级、`#`匹配多级)。 +- **QoS(Quality of Service)**:消息传输质量等级,分3级: + - QoS 0:最多一次(消息可能丢失,不确认)。 + - QoS 1:至少一次(确保消息到达,可能重复)。 + - QoS 2:刚好一次(确保消息唯一到达,最可靠但开销大)。 +- **遗嘱消息(Last Will and Testament)**:客户端异常断开时,Broker自动向指定主题发送的消息。 + + +### 二、Spring Boot集成MQTT的核心依赖 +Spring Boot本身不直接提供MQTT Starter,通常通过集成**Eclipse Paho MQTT客户端**实现。需在`pom.xml`中添加依赖: + +```xml + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + + + + + org.springframework.integration + spring-integration-mqtt + 5.5.15 + +``` + + +### 三、基础实现:基于Paho客户端的MQTT通信 +#### 1. 配置MQTT连接参数 +在`application.yml`中配置Broker地址、客户端ID等信息: + +```yaml +mqtt: + broker-url: tcp://localhost:1883 # MQTT Broker地址(TCP协议) + client-id: springboot-mqtt-client # 客户端唯一ID(避免重复,可加随机数) + username: admin # 可选:Broker认证用户名 + password: 123456 # 可选:Broker认证密码 + keep-alive: 60 # 心跳间隔(秒) + default-topic: device/data # 默认主题 + qos: 1 # 默认QoS等级 +``` + + +#### 2. 配置MQTT客户端工厂 +通过`@Configuration`创建MQTT客户端工厂和连接选项: + +```java +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MqttConfig { + + @Value("${mqtt.broker-url}") + private String brokerUrl; + + @Value("${mqtt.client-id}") + private String clientId; + + @Value("${mqtt.username}") + private String username; + + @Value("${mqtt.password}") + private String password; + + @Value("${mqtt.keep-alive}") + private int keepAlive; + + // 配置连接选项 + @Bean + public MqttConnectOptions mqttConnectOptions() { + MqttConnectOptions options = new MqttConnectOptions(); + options.setServerURIs(new String[]{brokerUrl}); // 支持多个Broker地址 + options.setUserName(username); + options.setPassword(password.toCharArray()); + options.setKeepAliveInterval(keepAlive); + options.setCleanSession(false); // 不清除会话(重连后保留订阅关系) + + // 配置遗嘱消息(可选) + options.setWill("device/offline", "客户端断开连接".getBytes(), 1, false); + return options; + } + + // 创建MQTT客户端实例(异步客户端,推荐使用) + @Bean + public MqttAsyncClient mqttAsyncClient() throws Exception { + // MemoryPersistence:消息临时存储在内存(可选:FilePersistence持久化到文件) + MqttAsyncClient client = new MqttAsyncClient(brokerUrl, clientId, new MemoryPersistence()); + // 连接Broker + client.connect(mqttConnectOptions()).waitForCompletion(); + return client; + } +} +``` + + +#### 3. 实现消息发送(Publisher) +创建消息发送服务,封装发送逻辑: + +```java +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class MqttPublisher { + + @Autowired + private MqttAsyncClient mqttClient; + + @Value("${mqtt.default-topic}") + private String defaultTopic; + + @Value("${mqtt.qos}") + private int defaultQos; + + /** + * 发送消息到默认主题 + */ + public void send(String payload) throws Exception { + send(defaultTopic, payload, defaultQos, false); + } + + /** + * 发送消息到指定主题 + * @param topic 主题 + * @param payload 消息内容 + * @param qos QoS等级 + * @param retained 是否保留消息(Broker存储最后一条保留消息,新订阅者会立即收到) + */ + public void send(String topic, String payload, int qos, boolean retained) throws Exception { + MqttMessage message = new MqttMessage(payload.getBytes()); + message.setQos(qos); + message.setRetained(retained); + // 异步发送,waitForCompletion()等待发送完成 + mqttClient.publish(topic, message).waitForCompletion(); + } +} +``` + + +#### 4. 实现消息接收(Subscriber) +通过`MqttCallback`接口监听消息和连接状态: + +```java +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +@Service +public class MqttSubscriber { + + @Autowired + private MqttAsyncClient mqttClient; + + @Value("${mqtt.default-topic}") + private String defaultTopic; + + @Value("${mqtt.qos}") + private int defaultQos; + + // 初始化时订阅主题 + @PostConstruct + public void subscribe() throws Exception { + // 订阅默认主题,指定QoS和消息监听器 + mqttClient.subscribe(defaultTopic, defaultQos, new IMqttMessageListener() { + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + String payload = new String(message.getPayload()); + System.out.println("收到消息:主题=" + topic + ",内容=" + payload); + // 处理消息逻辑(如存入数据库、触发业务操作等) + } + }).waitForCompletion(); + } +} +``` + + +### 四、高级用法:基于Spring Integration MQTT +Spring Integration提供了更符合Spring风格的MQTT集成方式,通过消息通道(MessageChannel)和注解简化开发。 + +#### 1. 配置Integration MQTT +```java +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.core.MessageProducer; +import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; +import org.springframework.integration.mqtt.core.MqttPahoClientFactory; +import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; +import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler; +import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; + +@Configuration +public class MqttIntegrationConfig { + + @Value("${mqtt.broker-url}") + private String brokerUrl; + + @Value("${mqtt.client-id}") + private String clientId; + + @Value("${mqtt.username}") + private String username; + + @Value("${mqtt.password}") + private String password; + + // 客户端工厂 + @Bean + public MqttPahoClientFactory mqttClientFactory() { + DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); + MqttConnectOptions options = new MqttConnectOptions(); + options.setServerURIs(new String[]{brokerUrl}); + options.setUserName(username); + options.setPassword(password.toCharArray()); + factory.setConnectionOptions(options); + return factory; + } + + // 接收消息的通道 + @Bean + public MessageChannel mqttInputChannel() { + return new DirectChannel(); + } + + // 入站适配器(接收消息) + @Bean + public MessageProducer inbound() { + // 订阅主题:device/data,客户端ID加后缀避免与发送端冲突 + MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter( + clientId + "-inbound", mqttClientFactory(), "device/data"); + adapter.setConverter(new DefaultPahoMessageConverter()); // 消息转换器 + adapter.setQos(1); // QoS等级 + adapter.setOutputChannel(mqttInputChannel()); // 绑定到接收通道 + return adapter; + } + + // 处理接收的消息(使用@ServiceActivator) + @Bean + @ServiceActivator(inputChannel = "mqttInputChannel") + public MessageHandler handler() { + return message -> { + String topic = (String) message.getHeaders().get("mqtt_receivedTopic"); + String payload = message.getPayload().toString(); + System.out.println("Integration接收消息:主题=" + topic + ",内容=" + payload); + }; + } + + // 发送消息的通道 + @Bean + public MessageChannel mqttOutputChannel() { + return new DirectChannel(); + } + + // 出站适配器(发送消息) + @Bean + @ServiceActivator(inputChannel = "mqttOutputChannel") + public MessageHandler outbound() { + MqttPahoMessageHandler handler = new MqttPahoMessageHandler( + clientId + "-outbound", mqttClientFactory()); + handler.setAsync(true); // 异步发送 + handler.setDefaultTopic("device/control"); // 默认发送主题 + handler.setDefaultQos(1); // 默认QoS + return handler; + } +} +``` + + +#### 2. 使用Integration发送消息 +通过`MessageChannel`发送消息: + +```java +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.MessageChannel; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class IntegrationMqttPublisher { + + // 注入发送通道 + @Resource(name = "mqttOutputChannel") + private MessageChannel mqttOutputChannel; + + public void send(String payload) { + // 发送到默认主题 + mqttOutputChannel.send(MessageBuilder.withPayload(payload).build()); + } + + public void sendToTopic(String topic, String payload) { + // 发送到指定主题(通过header指定) + mqttOutputChannel.send(MessageBuilder.withPayload(payload) + .setHeader("mqtt_topic", topic) + .build()); + } +} +``` + + +### 五、注意事项 +1. **客户端ID唯一性**:同一Broker下客户端ID不可重复,否则会导致连接被强制断开(可通过`clientId + 随机数`避免)。 +2. **QoS选择**:根据业务可靠性要求选择QoS,物联网场景常用QoS 1(平衡可靠性和性能)。 +3. **断线重连**:Paho客户端默认支持重连,可通过`MqttConnectOptions.setAutomaticReconnect(true)`增强重连逻辑。 +4. **消息持久化**:若需避免消息丢失,可使用`FilePersistence`替代`MemoryPersistence`,并设置`cleanSession=false`。 +5. **主题设计**:合理规划主题层级(如`device/{设备ID}/temp`),便于管理和订阅。 + + +通过以上方式,Spring Boot可快速集成MQTT实现消息的发布与订阅,适用于物联网设备通信、分布式系统通知等场景。根据业务复杂度,可选择基础Paho客户端或Spring Integration简化开发。 \ No newline at end of file diff --git a/ruoyi-mqtt/docker/docker-compose.yml b/ruoyi-mqtt/docker/docker-compose.yml new file mode 100644 index 0000000..7abb90e --- /dev/null +++ b/ruoyi-mqtt/docker/docker-compose.yml @@ -0,0 +1,35 @@ +# 网络设置 +networks: + base2024-network: + name: base2024-network + driver: bridge + ipam: + config: + - subnet: 192.168.222.0/24 + gateway: 192.168.222.1 + +services: + + + # mqtt服务 + mqtt: + container_name: mqtt + restart: always + image: registry.cn-hangzhou.aliyuncs.com/awl/emqx:5.6.1 + ports: + - "1883:1883" + - "8083:8083" + # - "8084:8084" + # - "8883:8883" + - "18083:18083" + volumes: + - ./mqtt/configs/:/opt/emqx/data/configs/:rw + - ./mqtt/log/:/opt/emqx/log/:rw + environment: + TZ: Asia/Shanghai + privileged: true + networks: + - base2024-network + + + diff --git a/ruoyi-mqtt/docker/mqtt/configs/cluster.hocon b/ruoyi-mqtt/docker/mqtt/configs/cluster.hocon new file mode 100644 index 0000000..42c824a --- /dev/null +++ b/ruoyi-mqtt/docker/mqtt/configs/cluster.hocon @@ -0,0 +1,51 @@ +authentication = [ + { + backend = http + body { + password = "${password}" + username = "${username}" + } + connect_timeout = 15s + enable_pipelining = 100 + headers {content-type = "application/json"} + mechanism = password_based + method = post + pool_size = 8 + request_timeout = 5s + ssl {enable = false, verify = verify_peer} + url = "http://server1:8080/emqx-login" + } +] +mqtt { + await_rel_timeout = 300s + exclusive_subscription = false + idle_timeout = 15s + ignore_loop_deliver = false + keepalive_multiplier = 1.5 + max_awaiting_rel = 100 + max_clientid_len = 65535 + max_inflight = 32 + max_mqueue_len = 1000 + max_packet_size = 256MB + max_qos_allowed = 2 + max_subscriptions = infinity + max_topic_alias = 65535 + max_topic_levels = 128 + message_expiry_interval = infinity + mqueue_default_priority = lowest + mqueue_priorities = disabled + mqueue_store_qos0 = true + peer_cert_as_clientid = disabled + peer_cert_as_username = disabled + response_information = "" + retain_available = true + retry_interval = 30s + server_keepalive = disabled + session_expiry_interval = 2h + shared_subscription = true + shared_subscription_strategy = round_robin + strict_mode = false + upgrade_qos = false + use_username_as_clientid = false + wildcard_subscription = true +} diff --git a/ruoyi-mqtt/docker/mqtt/log/README.md b/ruoyi-mqtt/docker/mqtt/log/README.md new file mode 100644 index 0000000..6fab83f --- /dev/null +++ b/ruoyi-mqtt/docker/mqtt/log/README.md @@ -0,0 +1 @@ +# emqx日志目录 \ No newline at end of file diff --git a/ruoyi-mqtt/pom.xml b/ruoyi-mqtt/pom.xml new file mode 100644 index 0000000..b2fd400 --- /dev/null +++ b/ruoyi-mqtt/pom.xml @@ -0,0 +1,41 @@ + + + + ruoyi-vue-plus + com.ruoyi + 4.6.0 + + 4.0.0 + + ruoyi-mqtt + + + mqtt模块 + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-json + + + cn.hutool + hutool-core + + + cn.hutool + hutool-cache + + + diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttEnabled.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttEnabled.java new file mode 100644 index 0000000..195f354 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttEnabled.java @@ -0,0 +1,17 @@ +package com.ruoyi.mqtt; + +import com.ruoyi.mqtt.config.MqttConfig; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 非ruoyi项目需要使用本注解 + */ +@Import(MqttConfig.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface MqttEnabled { +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttEventHandler.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttEventHandler.java new file mode 100644 index 0000000..30dbc48 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttEventHandler.java @@ -0,0 +1,57 @@ +package com.ruoyi.mqtt; + +import com.ruoyi.mqtt.event.*; + +/** + * 事件 处理句柄 + */ +public interface MqttEventHandler { + + /** + * 处理 + * + * @param event 事件 + * @param item 产生的MQTT操作项 + * @return 是否处理下一个句柄 + */ + default boolean next(MqttEvent event, MqttItem item) { + if (event instanceof MqttConnectionExceptionEvent) { + return next((MqttConnectionExceptionEvent) event, item); + } else if (event instanceof MqttConnectionLostEvent) { + return next((MqttConnectionLostEvent) event, item); + } else if (event instanceof MqttConnectionSuccessEvent) { + return next((MqttConnectionSuccessEvent) event, item); + } else if (event instanceof MqttMessageDeliveryEvent) { + return next((MqttMessageDeliveryEvent) event, item); + } else if (event instanceof MqttMessageEvent) { + return next((MqttMessageEvent) event, item); + } else if (event instanceof MqttReconnectionEvent) { + return next((MqttReconnectionEvent) event, item); + } + return false; + } + + default boolean next(MqttConnectionExceptionEvent event, MqttItem item) { + return true; + } + + default boolean next(MqttConnectionLostEvent event, MqttItem item) { + return true; + } + + default boolean next(MqttConnectionSuccessEvent event, MqttItem item) { + return true; + } + + default boolean next(MqttMessageDeliveryEvent event, MqttItem item) { + return true; + } + + default boolean next(MqttMessageEvent event, MqttItem item) { + return true; + } + + default boolean next(MqttReconnectionEvent event, MqttItem item) { + return true; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttFactory.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttFactory.java new file mode 100644 index 0000000..9aab88b --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttFactory.java @@ -0,0 +1,111 @@ +package com.ruoyi.mqtt; + +import static com.ruoyi.mqtt.config.MqttProperties.DEFAULT; + +import com.ruoyi.mqtt.config.MqttProperties.Config; +import com.ruoyi.mqtt.event.MqttEvent; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttException; + +import java.util.function.Consumer; + +/** + * MQTT操作工厂接口 + *

+ * 定义了MQTT客户端的管理与消息发送功能,支持多配置管理。 + *

+ */ +public interface MqttFactory { + + /** + * 根据配置名称获取MQTT操作项 + * + * @param configName 配置名称 + * @return MQTT操作项 + */ + MqttItem get(String configName); + + /** + * 获取默认配置的MQTT操作项 + * + * @return MQTT操作项 + */ + default MqttItem get() { + return get(DEFAULT); + } + + + /** + * 添加一个MQTT操作项,并使用默认的消息处理器 + * + * @param configName 配置名称 + * @param config MQTT配置 + * @param handlers 事件处理 + */ + void put(String configName, Config config, MqttEventHandler... handlers); + + /** + * 检查指定配置名称的MQTT操作项是否存在 + * + * @param configName 配置名称 + * @return 是否存在 + */ + boolean contains(String configName); + + /** + * 移除指定配置名称的MQTT操作项 + * + * @param configName 配置名称 + */ + void remove(String configName); + + /** + * 发送MQTT消息 + * + * @param configName 配置项名称 + * @param topic 主题 + * @param payload 消息负载(字节数组) + * @param qos 服务质量等级 QoS 0:最多一次 QoS 1:至少一次 QoS 2:恰好一次 + * @param retained 保留消息 + * @throws MqttException MQTT异常 + */ + IMqttDeliveryToken send(String configName, String topic, byte[] payload, int qos, boolean retained) throws MqttException; + + /** + * 发送MQTT消息 + * + * @param configName 配置项名称 + * @param sendName 配置的发送项名称 + * @param payload 消息负载(字节数组) + * @param params 配置的发送项参数,替换发送项中的{0},{1}... + * @throws MqttException MQTT异常 + */ + IMqttDeliveryToken send(String configName, String sendName, byte[] payload, String... params) throws MqttException; + + /** + * 发送默认配置项的MQTT消息 + * + * @param sendName 配置的发送项名称 + * @param payload 消息负载(字节数组) + * @param params 配置的发送项参数,替换发送项中的{0},{1}... + * @throws MqttException MQTT异常 + */ + default IMqttDeliveryToken send(String sendName, byte[] payload, String... params) throws MqttException { + return send(DEFAULT, sendName, payload, params); + } + + /** + * 发送默认配置项的MQTT消息 + * + * @param topic 主题 + * @param payload 消息负载(字节数组) + * @param qos 服务质量等级 QoS 0:最多一次 QoS 1:至少一次 QoS 2:恰好一次 + * @param retained 保留消息 + * @throws MqttException MQTT异常 + */ + default IMqttDeliveryToken send(String topic, byte[] payload, int qos, boolean retained) throws MqttException { + return send(DEFAULT, topic, payload, qos, retained); + } + + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttItem.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttItem.java new file mode 100644 index 0000000..9b400c0 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttItem.java @@ -0,0 +1,237 @@ +package com.ruoyi.mqtt; + +import com.ruoyi.mqtt.config.MqttProperties; +import com.ruoyi.mqtt.config.MqttProperties.Config; +import com.ruoyi.mqtt.event.*; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +/** + * MQTT操作项,支持异常重连 + *

+ * 该类封装了MQTT客户端的连接、消息发送和订阅功能, + * 并提供了自动重连机制以确保连接的稳定性。 + *

+ */ +@Slf4j +public class MqttItem { + + private final static int CHECK_GAP = 10; + + /** + * MQTT配置 + */ + @Getter + private final Config config; + + /** + * 客户端ID + */ + @Getter + private final String clientId; + + /** + * 配置名称 + */ + @Getter + private final String configName; + + @Getter + private final List messageHandlers = new CopyOnWriteArrayList<>(); + + private final Consumer consumer = (e) -> { + for (MqttEventHandler messageHandler : messageHandlers) { + if (!messageHandler.next(e, MqttItem.this)) { + return; + } + } + }; + + + private IMqttAsyncClient client; + + private final ScheduledExecutorService executorService; + + private ScheduledFuture future; + + private final AtomicBoolean connecting = new AtomicBoolean(false); + + /** + * 构造函数 + * + * @param configName 配置名称 + * @param config MQTT配置 + * @param executorService 定时任务执行器 + */ + public MqttItem(String configName, Config config, ScheduledExecutorService executorService) { + this.configName = configName; + this.config = config; + this.executorService = executorService; + this.clientId = config.getClientId() + "_" + Long.toString(System.currentTimeMillis() - 1735660800000L + new Random().nextInt(1024), 36); + connect(); + schedule(); + } + + /** + * 获取MQTT客户端 + * + * @return MQTT异步客户端 + * @throws RuntimeException 当客户端正在连接或未连接时抛出 + * @throws NullPointerException 当客户端未创建时抛出 + */ + public IMqttAsyncClient getClient() { + if (connecting.get()) { + throw new RuntimeException("mqtt客户端连接中:" + configName); + } + + if (client == null) { + throw new NullPointerException("mqtt客户端未创建:" + configName); + } + if (!client.isConnected()) { + throw new RuntimeException("mqtt客户端未连接:" + configName); + } + return client; + } + + + /** + * 定时检查连接状态并重连 + */ + private void schedule() { + future = executorService.scheduleWithFixedDelay(() -> { + if (client != null && client.isConnected()) { + return; + } + consumer.accept(new MqttReconnectionEvent(configName)); + connect(); + + }, CHECK_GAP, CHECK_GAP, TimeUnit.SECONDS); + } + + /** + * 连接到MQTT服务器 + */ + public void connect() { + if (connecting.get()) { + return; + } + connecting.set(true); + if (client != null) { + try { + client.close(); + } catch (Exception ignored) { + + } + } + client = null; + try { + MqttConnectOptions options = mqttConnectOptions(config); + client = new MqttAsyncClient(config.getUrl(), clientId, new MemoryPersistence()); + client.setCallback(new MqttCallback() { + @Override + public void connectionLost(Throwable cause) { + consumer.accept(new MqttConnectionLostEvent(configName, cause)); + connecting.set(false); + connect(); + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + if(log.isDebugEnabled()){ + log.debug("mqtt message: {}={}",topic,new String(message.getPayload(), StandardCharsets.UTF_8)); + } + consumer.accept(new MqttMessageEvent(configName, topic, message)); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + consumer.accept(new MqttMessageDeliveryEvent(configName, token)); + } + }); + client.connect(options).waitForCompletion(5000); + if (config.getSubscribes() != null && !config.getSubscribes().isEmpty()) { + client.subscribe( + config.getSubscribes().stream().map(MqttProperties.Topic::getTopic).toArray(String[]::new), + config.getSubscribes().stream().mapToInt(MqttProperties.Topic::getQos).toArray() + ); + } + consumer.accept(new MqttConnectionSuccessEvent(configName)); + } catch (Exception e) { + consumer.accept(new MqttConnectionExceptionEvent(configName, e)); + } finally { + connecting.set(false); + } + } + + /** + * 销毁MQTT客户端,释放资源 + */ + public void destroy() { + if (future != null) { + future.cancel(true); + future = null; + } + + if (client != null) { + try { + client.close(); + } catch (Exception ignored) { + + } + client = null; + } + + } + + /** + * 创建MQTT连接选项 + * + * @param properties MQTT配置属性 + * @return MQTT连接选项 + */ + public MqttConnectOptions mqttConnectOptions(MqttProperties.Config properties) { + // 连接设置 + MqttConnectOptions options = new MqttConnectOptions(); + // 是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 + // 设置为true表示每次连接服务器都是以新的身份 + options.setCleanSession(properties.getCleanSession()); +// options.setCleanSession(true); + // 设置连接用户名 + options.setUserName(properties.getUsername()); + // 设置连接密码 + options.setPassword(properties.getPassword().toCharArray()); + // 设置超时时间,单位为秒 + options.setConnectionTimeout(properties.getConnectionTimeout()); + // 设置心跳时间 单位为秒,表示服务器每隔 1.5*20秒的时间向客户端发送心跳判断客户端是否在线 + options.setKeepAliveInterval(properties.getKeepAliveInterval()); + // 设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 + options.setWill(properties.getWillTopic(), properties.getWillMessage().getBytes(), properties.getWillQos(), false); + // 设置重连 +// options.setAutomaticReconnect(properties.getAutomaticReconnect()); + options.setAutomaticReconnect(false); +// options.setMaxReconnectDelay(properties.getMaxReconnectDelay()); + options.setCustomWebSocketHeaders(properties.getCustomWebSocketHeaders()); + options.setSSLProperties(properties.getSslProperties()); + options.setMqttVersion(properties.getMqttVersion().getVersion()); + options.setMaxInflight(properties.getMaxInflight()); + + options.setServerURIs(properties.getUrls().toArray(new String[0])); + + return options; + } + + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttUtil.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttUtil.java new file mode 100644 index 0000000..7449d4a --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttUtil.java @@ -0,0 +1,178 @@ +package com.ruoyi.mqtt; + + +import com.ruoyi.mqtt.config.MqttConfig; +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttException; + +import java.nio.charset.StandardCharsets; + +/** + * MQTT工具类,提供便捷的MQTT消息发送和客户端获取功能。 + *

+ * 该工具类封装了{@link MqttFactory}的常用操作,简化了MQTT消息的发送流程。 + * 支持发送字节数组和字符串类型的消息,并可指定服务质量(QoS)和保留标志。 + *

+ */ +public class MqttUtil { + + + private MqttUtil() { + + } + + /** + * 获取MQTT工厂实例 + * + * @return MQTT工厂实例 + */ + public static MqttFactory getFactory() { + return MqttConfig.getMqttFactory(); + } + + /** + * 获取默认配置的MQTT操作项 + * + * @return MQTT操作项 + */ + public static MqttItem getItem() { + return getFactory().get(); + } + + /** + * 根据配置名称获取MQTT操作项 + * + * @param configName 配置名称 + * @return MQTT操作项 + */ + public static MqttItem getItem(String configName) { + return getFactory().get(configName); + } + + /** + * 获取默认配置的MQTT客户端 + * + * @return MQTT异步客户端 + */ + public static IMqttAsyncClient getClient() { + return getItem().getClient(); + } + + /** + * 根据配置名称获取MQTT客户端 + * + * @param configName 配置名称 + * @return MQTT异步客户端 + */ + public static IMqttAsyncClient getClient(String configName) { + return getItem(configName).getClient(); + } + + + + + /** + * 发送MQTT消息 + * + * @param configName 配置项名称 + * @param sendName 配置的发送项名称 + * @param payload 消息负载(字节数组) + * @param params 配置的发送项参数,替换发送项中的{0},{1}... + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String configName, String sendName, byte[] payload, String... params)throws MqttException { + return getFactory().send(configName, sendName, payload, params); + } + + /** + * 发送MQTT消息 + * + * @param configName 配置项名称 + * @param sendName 配置的发送项名称 + * @param payload 消息负载(字符串) + * @param params 配置的发送项参数,替换发送项中的{0},{1}... + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String configName, String sendName, String payload, String... params)throws MqttException { + return getFactory().send(configName, sendName, payload.getBytes(StandardCharsets.UTF_8), params); + } + + /** + * 发送MQTT消息(使用默认配置) + * + * @param sendName 配置的发送项名称 + * @param payload 消息负载(字节数组) + * @param params 配置的发送项参数,替换发送项中的{0},{1}... + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String sendName, byte[] payload, String... params)throws MqttException { + return getFactory().send(sendName, payload, params); + } + + /** + * 发送MQTT消息(使用默认配置) + * + * @param sendName 配置的发送项名称 + * @param payload 消息负载(字符串) + * @param params 配置的发送项参数,替换发送项中的{0},{1}... + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String sendName, String payload, String... params)throws MqttException { + return getFactory().send(sendName, payload.getBytes(StandardCharsets.UTF_8), params); + } + + /** + * 发送MQTT消息 + * + * @param configName 配置项名称 + * @param topic 主题 + * @param payload 消息负载(字节数组) + * @param qos 服务质量等级 QoS 0:最多一次 QoS 1:至少一次 QoS 2:恰好一次 + * @param retained 保留消息 + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String configName, String topic, byte[] payload, int qos, boolean retained) throws MqttException{ + return getFactory().send(configName, topic, payload, qos, retained); + } + + /** + * 发送MQTT消息 + * + * @param configName 配置项名称 + * @param topic 主题 + * @param payload 消息负载(字符串) + * @param qos 服务质量等级 QoS 0:最多一次 QoS 1:至少一次 QoS 2:恰好一次 + * @param retained 保留消息 + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String configName, String topic, String payload, int qos, boolean retained) throws MqttException{ + return getFactory().send(configName, topic, payload.getBytes(StandardCharsets.UTF_8), qos, retained); + } + + /** + * 发送MQTT消息(使用默认配置) + * + * @param topic 主题 + * @param payload 消息负载(字节数组) + * @param qos 服务质量等级 QoS 0:最多一次 QoS 1:至少一次 QoS 2:恰好一次 + * @param retained 保留消息 + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String topic, byte[] payload, int qos, boolean retained) throws MqttException{ + return getFactory().send(topic, payload, qos, retained); + } + + /** + * 发送MQTT消息(使用默认配置) + * + * @param topic 主题 + * @param payload 消息负载(字符串) + * @param qos 服务质量等级 QoS 0:最多一次 QoS 1:至少一次 QoS 2:恰好一次 + * @param retained 保留消息 + * @throws MqttException MQTT异常 + */ + public static IMqttDeliveryToken send(String topic, String payload, int qos, boolean retained) throws MqttException{ + return getFactory().send(topic, payload.getBytes(StandardCharsets.UTF_8), qos, retained); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttVersion.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttVersion.java new file mode 100644 index 0000000..5d8662a --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/MqttVersion.java @@ -0,0 +1,37 @@ +package com.ruoyi.mqtt; + +/** + * MQTT协议版本枚举类 + *

+ * 定义了支持的MQTT协议版本,包括默认版本、3.1版本和3.1.1版本。 + *

+ */ +public enum MqttVersion { + /** 默认MQTT版本 */ + MQTT_VERSION_DEFAULT(0), + /** MQTT 3.1版本 */ + MQTT_VERSION_3_1(3), + /** MQTT 3.1.1版本 */ + MQTT_VERSION_3_1_1(4); + + private int version; + + /** + * 构造函数 + * + * @param version 版本号 + */ + private MqttVersion(int version) { + this.version = version; + } + + + /** + * 获取版本号 + * + * @return 版本号 + */ + public int getVersion() { + return version; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/config/MqttConfig.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/config/MqttConfig.java new file mode 100644 index 0000000..4f2daf2 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/config/MqttConfig.java @@ -0,0 +1,181 @@ +package com.ruoyi.mqtt.config; + +import com.ruoyi.mqtt.MqttEventHandler; +import com.ruoyi.mqtt.MqttFactory; +import com.ruoyi.mqtt.MqttItem; +import com.ruoyi.mqtt.MqttUtil; +import com.ruoyi.mqtt.event.MqttEvent; +import com.ruoyi.mqtt.event.MqttSendEvent; +import com.ruoyi.mqtt.event.MqttSendTopicEvent; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +@Configuration +@EnableConfigurationProperties(MqttProperties.class) +@Slf4j +@RequiredArgsConstructor +public class MqttConfig implements MqttFactory, MqttEventHandler, PriorityOrdered { + + private final MqttProperties properties; + private final ApplicationContext act; + + @Getter + private static MqttFactory mqttFactory; + + private ScheduledExecutorService executorService; + + private final Map clients = new ConcurrentHashMap<>(); + + @PostConstruct + public void init() throws Exception { + if (!properties.getEnabled()) { + log.info("mqtt模块未激活"); + return; + } + log.info("mqtt模块启动中..."); + try { + executorService = act.getBeansOfType(ScheduledExecutorService.class).values().stream().findFirst().get(); + } catch (Exception e) { + executorService = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors()); + } + properties.getConfigs().forEach((a, b) -> { + if (!b.getEnabled()) { + return; + } + put(a, b, this); + }); + mqttFactory = this; + } + + @Override + public boolean next(MqttEvent event, MqttItem item) { + log.debug("mqtt event: {} = {}", event.getConfigName(), event.getClass().getName()); + act.publishEvent(event); + return true; + } + + @PreDestroy + public void destroy() { + clients.forEach((a, b) -> { + try { + b.destroy(); + } catch (Exception e) { + log.error("mqtt客户端关闭失败:" + a, e); + } + }); + try { + executorService.shutdown(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public MqttItem get(String configName) { + return clients.get(configName); + } + + + public void put(String configName, MqttProperties.Config config, MqttEventHandler... handlers) { + if (clients.containsKey(configName)) { + throw new RuntimeException("配置项已经存在"); + } + MqttItem mqttItem = new MqttItem(configName, config, executorService); + if (handlers != null && handlers.length > 0) { + Collections.addAll(mqttItem.getMessageHandlers(), handlers); + } + clients.put(configName, mqttItem); + log.debug("mqtt配置项添加:" + configName); + } + + public boolean contains(String configName) { + return clients.containsKey(configName); + } + + public void remove(String configName) { + if (contains(configName)) { + get(configName).destroy(); + clients.remove(configName); + } + } + + + public IMqttDeliveryToken send(String configName, String topic, byte[] payload, int qos, boolean retained) throws MqttException { + MqttItem item = get(configName); + if (item == null) { + throw new RuntimeException("mqtt客户端未找到:" + configName); + } + log.debug("mqtt发送成功:configName={},topic={}", configName, topic); + return item.getClient().publish(topic, payload, qos, retained); + } + + public IMqttDeliveryToken send(String configName, String sendName, byte[] payload, String... params) throws MqttException { + + MqttItem item = get(configName); + if (item == null) { + throw new RuntimeException("mqtt客户端未找到:" + configName); + } + + MqttProperties.Topic topic = item.getConfig().getSends().get(sendName); + if (topic == null) { + throw new RuntimeException("mqtt配置的主题未找到:" + configName + " = " + sendName); + } + String topicTempalte = topic.getTopic(); + if (params != null) { + for (int i = 0; i < params.length; i++) { + topicTempalte = topicTempalte.replace("{" + i + "}", params[i]); + } + } + + log.debug("mqtt发送成功:configName={},sendName={},topic={}", configName, sendName, topicTempalte); + return item.getClient().publish(topicTempalte, payload, topic.getQos(), topic.getRetained()); + } + + + @EventListener + public void listener(MqttSendEvent event) throws MqttException { + send(event.getConfigName(), event.getSendName(), event.getPayload(), event.getParams()); + } + + @EventListener + public void listener(MqttSendTopicEvent event) throws MqttException { + send(event.getConfigName(), event.getTopic(), event.getPayload(), event.getQos(), event.isRetained()); + } + + +// @Scheduled(cron = "*/5 * * * * ?") +// public void test() { +// String s = Long.toString(System.currentTimeMillis(), 36); +// try { +//// act.publishEvent(new MqttSendEvent("test",s.getBytes(StandardCharsets.UTF_8),s)); +// MqttUtil.send("test", s, new String[]{s}); +// log.info("test success:{}", s); +// } catch (Exception e) { +// log.info("test error:" + s, e); +// } +// } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; // 最高优先级 + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/config/MqttProperties.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/config/MqttProperties.java new file mode 100644 index 0000000..5e8494d --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/config/MqttProperties.java @@ -0,0 +1,194 @@ +package com.ruoyi.mqtt.config; + + +import com.ruoyi.mqtt.MqttVersion; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.*; + +/** + * MQTT配置属性类 + *

+ * 该类定义了MQTT客户端的配置属性,包括是否启用、默认配置、以及各个MQTT客户端的详细配置。 + *

+ */ +@Data +@ConfigurationProperties(value = MqttProperties.PREFIX, ignoreInvalidFields = true, ignoreUnknownFields = true) +public class MqttProperties { + + public final static String DEFAULT = "default"; + + /** + * 配置前缀 + */ + public static final String PREFIX = "spring.mqtt"; + + /** + * 是否启用MQTT功能 + * 默认值: false + */ + private Boolean enabled = false; + + /** + * 默认配置名称 + * 默认值: "default" + */ + private String defaultConfig = DEFAULT; + + /** + * MQTT客户端配置映射,key为配置名称,value为具体的配置项 + */ + private Map configs = new HashMap<>(); + + + /** + * MQTT主题配置类 + *

+ * 该类定义了MQTT主题的相关配置,包括主题名称、服务质量(QoS)和是否保留消息。 + *

+ */ + @Data + public static class Topic { + + /** + * 订阅主题可以使用 单级通配符 "+" 和多级通配符 "#" + * 发送主题可以使用如:{0},{1},{2},发送时使用params数组替换 + */ + private String topic; + + /** + * 服务质量(QoS) + * 默认值: 0 + */ + private Integer qos = 0; + + /** + * 是否保留消息,订阅主题时无需配置 + * 默认值: false + */ + private Boolean retained = false; + } + + /** + * MQTT客户端配置类 + *

+ * 该类定义了单个MQTT客户端的详细配置,包括连接信息、认证信息、心跳设置等。 + *

+ */ + @Data + public static class Config { + + /** + * 是否启用该MQTT客户端配置 + * 默认值: true + */ + private Boolean enabled = true; + + /** + * 客户端编号(唯一) + * 默认值: 基于系统时间生成的36进制字符串 + */ + private String clientId = Long.toString(System.currentTimeMillis(), 36); + + /** + * 服务器URL + * 默认值: tcp://127.0.0.1:1883 + */ + private String url = "tcp://127.0.0.1:1883"; + + /** + * 服务器集群URL列表(可选) + */ + private List urls = new ArrayList<>(); + + /** + * 是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 + * 设置为true表示每次连接服务器都是以新的身份 + * 默认值: true + */ + private Boolean cleanSession = true; + + /** + * 用户名 + * 默认值: admin + */ + private String username = "admin"; + + /** + * 密码 + * 默认值: 123456 + */ + private String password = "123456"; + + /** + * 连接超时时间,单位秒 + * 默认值: 5 + */ + private Integer connectionTimeout = 5; + + /** + * 心跳时间 单位为秒,表示服务器每隔60秒的时间向客户端发送心跳判断客户端是否在线 + * 默认值: 60 + */ + private Integer keepAliveInterval = 60; + + /** + * 遗嘱主题 + * 默认值: will/topic + */ + private String willTopic = "will/topic"; + + /** + * 遗嘱消息 + * 默认值: offline + */ + private String willMessage = "offline"; + + /** + * 遗嘱消息的服务质量(QoS) + * 默认值: 0 + */ + private Integer willQos = 0; + + /** + * 订阅主题列表 + */ + private List subscribes = new ArrayList<>(); + + /** + * 是否自动重连 + * 默认值: true (注释掉的配置) + */ +// private Boolean automaticReconnect = true; + +// private Integer maxReconnectDelay = Integer.MAX_VALUE; + + /** + * 自定义WebSocket头部信息 + */ + private Properties customWebSocketHeaders = new Properties(); + + /** + * SSL属性配置 + */ + private Properties sslProperties = new Properties(); + + /** + * 发送主题映射,key为发送名称,value为具体的主题配置 + */ + private Map sends = new HashMap<>(); + + /** + * MQTT协议版本 + * 默认值: MQTT_VERSION_DEFAULT + */ + private MqttVersion mqttVersion = MqttVersion.MQTT_VERSION_DEFAULT; + + /** + * 最大未确认消息数量 + * 默认值: 10 + */ + private Integer maxInflight = 10; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionExceptionEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionExceptionEvent.java new file mode 100644 index 0000000..ab07694 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionExceptionEvent.java @@ -0,0 +1,21 @@ +package com.ruoyi.mqtt.event; + +import lombok.Getter; + +/** + * MQTT连接异常 + */ +public final class MqttConnectionExceptionEvent extends MqttEvent{ + + @Getter + private Throwable cause; + + public MqttConnectionExceptionEvent(Throwable cause) { + this.cause = cause; + } + + public MqttConnectionExceptionEvent(String configName, Throwable cause) { + super(configName); + this.cause = cause; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionLostEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionLostEvent.java new file mode 100644 index 0000000..0bbe2dc --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionLostEvent.java @@ -0,0 +1,21 @@ +package com.ruoyi.mqtt.event; + +import lombok.Getter; + +/** + * MQTT连接丢失 + */ +public final class MqttConnectionLostEvent extends MqttEvent{ + + @Getter + private Throwable cause; + + public MqttConnectionLostEvent(Throwable cause) { + this.cause = cause; + } + + public MqttConnectionLostEvent(String configName,Throwable cause) { + super(configName); + this.cause = cause; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionSuccessEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionSuccessEvent.java new file mode 100644 index 0000000..45882f4 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttConnectionSuccessEvent.java @@ -0,0 +1,14 @@ +package com.ruoyi.mqtt.event; + +/** + * MQTT连接成功 + */ +public final class MqttConnectionSuccessEvent extends MqttEvent{ + + public MqttConnectionSuccessEvent() { + } + + public MqttConnectionSuccessEvent(String configName) { + super(configName); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttEvent.java new file mode 100644 index 0000000..1fb8586 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttEvent.java @@ -0,0 +1,26 @@ +package com.ruoyi.mqtt.event; + +import com.ruoyi.mqtt.config.MqttProperties; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * Mqtt事件 + */ +public abstract class MqttEvent extends ApplicationEvent { + @Getter + private String configName = MqttProperties.DEFAULT; + + public MqttEvent() { + this(MqttProperties.DEFAULT); + } + + public MqttEvent(String configName) { + super(configName); + } + + @Override + public String getSource() { + return (String)super.getSource(); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttMessageDeliveryEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttMessageDeliveryEvent.java new file mode 100644 index 0000000..390358f --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttMessageDeliveryEvent.java @@ -0,0 +1,24 @@ +package com.ruoyi.mqtt.event; + +import lombok.Getter; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * 接受的消息 + */ +public final class MqttMessageDeliveryEvent extends MqttEvent { + + @Getter + private IMqttDeliveryToken token; + + + public MqttMessageDeliveryEvent(IMqttDeliveryToken token) { + this.token = token; + } + + public MqttMessageDeliveryEvent(String configName, IMqttDeliveryToken token) { + super(configName); + this.token = token; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttMessageEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttMessageEvent.java new file mode 100644 index 0000000..82cca86 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttMessageEvent.java @@ -0,0 +1,27 @@ +package com.ruoyi.mqtt.event; + +import lombok.Getter; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * 接受的消息 + */ +public final class MqttMessageEvent extends MqttEvent { + + @Getter + private String topic; + + @Getter + private MqttMessage message; + + public MqttMessageEvent(String topic, MqttMessage message) { + this.topic = topic; + this.message = message; + } + + public MqttMessageEvent(String configName, String topic, MqttMessage message) { + super(configName); + this.topic = topic; + this.message = message; + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttReconnectionEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttReconnectionEvent.java new file mode 100644 index 0000000..6102d8c --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttReconnectionEvent.java @@ -0,0 +1,15 @@ + +package com.ruoyi.mqtt.event; + +/** + * MQTT重连开始 + */ +public final class MqttReconnectionEvent extends MqttEvent{ + + public MqttReconnectionEvent() { + } + + public MqttReconnectionEvent(String configName) { + super(configName); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttSendEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttSendEvent.java new file mode 100644 index 0000000..cdd9443 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttSendEvent.java @@ -0,0 +1,53 @@ +package com.ruoyi.mqtt.event; + +import com.ruoyi.mqtt.config.MqttProperties; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +/** + * 发送的 + */ +public class MqttSendEvent extends ApplicationEvent { + + @Getter + private String configName; + + @Getter + private String sendName; + + @Getter + private byte[] payload; + + @Getter + private String[] params; + + + public MqttSendEvent(String configName, String sendName, byte[] payload, String... params) { + super(configName); + this.configName = configName; + this.sendName = sendName; + this.payload = payload; + this.params = params; + } + + public MqttSendEvent(String configName, String sendName, byte[] payload, List params) { + this(configName, sendName, payload, params.toArray(new String[0])); + } + + public MqttSendEvent(String sendName, byte[] payload, List params) { + this(MqttProperties.DEFAULT, sendName, payload, params.toArray(new String[0])); + } + + public MqttSendEvent(String sendName, byte[] payload, String... params) { + this(MqttProperties.DEFAULT, sendName, payload, params); + } + + + @Override + public String getSource() { + return (String) super.getSource(); + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttSendTopicEvent.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttSendTopicEvent.java new file mode 100644 index 0000000..7783088 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/event/MqttSendTopicEvent.java @@ -0,0 +1,49 @@ +package com.ruoyi.mqtt.event; + +import com.ruoyi.mqtt.config.MqttProperties; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +/** + * 发送的 + */ +public class MqttSendTopicEvent extends ApplicationEvent { + + @Getter + private String configName; + + @Getter + private String topic; + + @Getter + private byte[] payload; + + @Getter + private int qos; + + @Getter + private boolean retained; + + + public MqttSendTopicEvent(String configName, String topic, byte[] payload, int qos, boolean retained) { + super(configName); + this.configName = configName; + this.topic = topic; + this.payload = payload; + this.qos = qos; + this.retained = retained; + } + + public MqttSendTopicEvent(String topic, byte[] payload, int qos, boolean retained) { + this(MqttProperties.DEFAULT, topic, payload, qos, retained); + } + + + @Override + public String getSource() { + return (String) super.getSource(); + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/Const.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/Const.java new file mode 100644 index 0000000..9307169 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/Const.java @@ -0,0 +1,39 @@ +package com.ruoyi.mqtt.rmi; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * MQTT RMI 常量接口 + * + * 该接口定义了 MQTT RMI 相关的常量。 + */ +public interface Const { + String REQUEST_TOPICE = "/request"; + String RESPONSE_TOPICE = "/response"; + + /** + * 将异常的栈跟踪信息转换为字符串 + * + * @param e 异常对象 + * @return 包含完整栈跟踪信息的字符串,如果e为null则返回"null" + */ + public static String getStackTraceAsString(Throwable e) { + // 处理null情况 + if (e == null) { + return null; + } + + // 使用StringWriter和PrintWriter来捕获栈跟踪信息 + try ( + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + ) { + e.printStackTrace(pw); + return sw.toString(); + } catch (IOException ex) { + return null; + } + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiRequest.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiRequest.java new file mode 100644 index 0000000..d29907b --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiRequest.java @@ -0,0 +1,26 @@ +package com.ruoyi.mqtt.rmi; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +/** + * MQTT RMI 请求类 + * + * 该类用于封装 MQTT RMI 调用的请求数据。 + */ +public class MqttRmiRequest { + + private String requestId; + private String name; + private String method; + private List args; +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiRequestSender.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiRequestSender.java new file mode 100644 index 0000000..3139fd9 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiRequestSender.java @@ -0,0 +1,10 @@ +package com.ruoyi.mqtt.rmi; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public interface MqttRmiRequestSender { + + ObjectMapper getMapper(); + + MqttRmiResponse request(MqttRmiRequest request, long timeout); +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiResponse.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiResponse.java new file mode 100644 index 0000000..aa791a3 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiResponse.java @@ -0,0 +1,29 @@ +package com.ruoyi.mqtt.rmi; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +/** + * MQTT RMI 响应类 + * + * 该类用于封装 MQTT RMI 调用的响应数据。 + */ +public class MqttRmiResponse { + + private String requestId; + private String name; + private String method; + private Boolean ok; + private JsonNode body; + private String error; + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiUtil.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiUtil.java new file mode 100644 index 0000000..d2c6b69 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/MqttRmiUtil.java @@ -0,0 +1,23 @@ +package com.ruoyi.mqtt.rmi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.rmi.impl.MqttRmiConsumerImpl; + +public class MqttRmiUtil { + + private MqttRmiUtil() { + } + + public static MqttRmiResponse request(MqttRmiRequest request, long timeout) { + return MqttRmiConsumerImpl.getSender().request(request, timeout); + } + + public static MqttRmiResponse request(MqttRmiRequest request) { + return MqttRmiConsumerImpl.getSender().request(request, 0); + } + + public static ObjectMapper getMapper() { + return MqttRmiConsumerImpl.getSender().getMapper(); + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiEnabled.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiEnabled.java new file mode 100644 index 0000000..3f50dae --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiEnabled.java @@ -0,0 +1,17 @@ +package com.ruoyi.mqtt.rmi.annotation; + +import com.ruoyi.mqtt.rmi.config.MqttRmiConfig; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 非ruoyi项目需要使用本注解 + */ +@Import(MqttRmiConfig.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface MqttRmiEnabled { +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiMethod.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiMethod.java new file mode 100644 index 0000000..c06c40d --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiMethod.java @@ -0,0 +1,32 @@ +package com.ruoyi.mqtt.rmi.annotation; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Component +/** + * MQTT RMI 方法注解 + * + * 该注解用于标记一个方法为 MQTT RMI 可调用方法。 + */ +public @interface MqttRmiMethod { + + /** + * 远程调用的方法不支持方法重载 + * 方法名,默认为本方法名 + * @return + */ + String value() default ""; + + /** + * 超时,默认:0 表示使用配置的默认值 + * @return + */ + long timeout() default 0; +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiProvider.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiProvider.java new file mode 100644 index 0000000..41c6ad5 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiProvider.java @@ -0,0 +1,23 @@ +package com.ruoyi.mqtt.rmi.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +/** + * MQTT RMI 提供者注解 + * + * 该注解用于标记一个类为 MQTT RMI 服务提供者。 + */ +public @interface MqttRmiProvider { + + /** + * 默认为类名 + * @return + */ + String value() default ""; +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiScan.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiScan.java new file mode 100644 index 0000000..4f08754 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiScan.java @@ -0,0 +1,16 @@ +package com.ruoyi.mqtt.rmi.annotation; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +/** + * MQTT RMI 扫描注解 + * + * 该注解用于标记需要扫描的包路径,以便发现和注册 MQTT RMI 服务接口。 + */ +public @interface MqttRmiScan { + String[] value() default {}; +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiService.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiService.java new file mode 100644 index 0000000..607bd67 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/annotation/MqttRmiService.java @@ -0,0 +1,23 @@ +package com.ruoyi.mqtt.rmi.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +/** + * MQTT RMI 服务注解 + * + * 该注解用于标记一个接口为 MQTT RMI 服务接口。 + */ +public @interface MqttRmiService { + + /** + * 默认为类名 + * @return + */ + String value() default ""; +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/config/MqttRmiConfig.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/config/MqttRmiConfig.java new file mode 100644 index 0000000..ceae503 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/config/MqttRmiConfig.java @@ -0,0 +1,57 @@ +package com.ruoyi.mqtt.rmi.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.MqttFactory; +import com.ruoyi.mqtt.rmi.impl.MqttRmiConsumerBeanDefinitionRegistryPostProcessor; +import com.ruoyi.mqtt.rmi.impl.MqttRmiConsumerImpl; +import com.ruoyi.mqtt.rmi.impl.MqttRmiProviderImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +@Configuration +@EnableConfigurationProperties(MqttRmiProperties.class) +/** + * MQTT RMI 配置类 + * + * 该类负责配置和初始化 MQTT RMI 相关的 Bean。 + */ +@Slf4j +@RequiredArgsConstructor +public class MqttRmiConfig implements PriorityOrdered { + + MqttRmiProperties properties; + + @Bean +// @ConditionalOnProperty(prefix = MqttRmiProperties.PREFIX,name = "provider",havingValue = "true",matchIfMissing = false) + public MqttRmiProviderImpl mqttRmiProviderImpl(MqttRmiProperties properties, MqttFactory factory, ObjectMapper mapper) { + return new MqttRmiProviderImpl(properties, factory, mapper); + } + + @Bean +// @ConditionalOnProperty(prefix = MqttRmiProperties.PREFIX,name = "consumer",havingValue = "true",matchIfMissing = false) + public MqttRmiConsumerImpl mqttRmiConsumer(MqttRmiProperties properties, MqttFactory factory, ObjectMapper mapper, ApplicationContext act) { + return new MqttRmiConsumerImpl(properties, factory, mapper, act); + } + + @Bean + @ConditionalOnProperty(prefix = MqttRmiProperties.PREFIX,name = "consumer",havingValue = "true",matchIfMissing = false) + public MqttRmiConsumerBeanDefinitionRegistryPostProcessor mqttRmiConsumerBeanDefinitionRegistryPostProcessor() { + return new MqttRmiConsumerBeanDefinitionRegistryPostProcessor(); + } + + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; // 最高优先级 + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/config/MqttRmiProperties.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/config/MqttRmiProperties.java new file mode 100644 index 0000000..c29e2e7 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/config/MqttRmiProperties.java @@ -0,0 +1,74 @@ +package com.ruoyi.mqtt.rmi.config; + + +import com.ruoyi.mqtt.config.MqttProperties; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * MQTT配置属性类 + *

+ * 该类定义了MQTT客户端的配置属性,包括是否启用、默认配置、以及各个MQTT客户端的详细配置。 + *

+ */ +@Data +@ConfigurationProperties(value = MqttRmiProperties.PREFIX, ignoreInvalidFields = true, ignoreUnknownFields = true) +/** + * MQTT RMI 配置属性类 + * + * 该类用于配置 MQTT RMI 相关的属性。 + */ +public class MqttRmiProperties { + + + /** + * 配置前缀 + */ + public static final String PREFIX = "spring.mqtt.rmi"; + + /** + * 是否启用MQTT的远程方法调用功能 + * 默认值: false + */ + private Boolean enabled = false; + + /** + * 是否是提供者,负责接口的实现, 默认: false + */ + private Boolean provider = false; + + /** + * 是否是消费者,负责接口的定义, 默认: false + */ + private Boolean consumer = false; + + /** + * 默认配置名称 + * 默认值: "default" + */ + private String configName = MqttProperties.DEFAULT; + + /** + * 用于rmi的发送和订阅主题,不能含变量和通配符 + * 请求主题会加后缀: /request + * 响应主题会加后缀: /response + */ + private MqttProperties.Topic topic; + + /** + * 发送请求时,主题添加的前缀 + */ + private String topicRequestPrefix=""; + + /** + * 没有发现方法是否发送错误, 默认: true + */ + private Boolean notFindMethodSendError = true; + + /** + * 远程方法调用默认超时(毫秒),默认: 500 + */ + private Long timeout = 500L; + + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiErrorException.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiErrorException.java new file mode 100644 index 0000000..273b104 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiErrorException.java @@ -0,0 +1,20 @@ +package com.ruoyi.mqtt.rmi.exception; + +import lombok.Getter; + +/** + * MQTT RMI 错误异常类 + * + * 该类表示 MQTT RMI 调用过程中发生错误的异常。 + */ +public class MqttRmiErrorException extends MqttRmiException { + + public MqttRmiErrorException(String requestId) { + super(requestId); + } + + public MqttRmiErrorException(String requestId, String message) { + super(requestId, message); + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiException.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiException.java new file mode 100644 index 0000000..89ee6e4 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiException.java @@ -0,0 +1,25 @@ +package com.ruoyi.mqtt.rmi.exception; + +import lombok.Getter; + +/** + * MQTT RMI 异常类 + * + * 该类是 MQTT RMI 相关异常的基类,包含请求 ID 信息。 + */ +public class MqttRmiException extends RuntimeException { + + @Getter + private String requestId; + + public MqttRmiException(String requestId) { + super(); + this.requestId = requestId; + } + + public MqttRmiException(String requestId,String message) { + super(message); + this.requestId = requestId; + } + +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiTimeoutException.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiTimeoutException.java new file mode 100644 index 0000000..f00042b --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/exception/MqttRmiTimeoutException.java @@ -0,0 +1,17 @@ +package com.ruoyi.mqtt.rmi.exception; + +/** + * MQTT RMI 超时异常类 + * + * 该类表示 MQTT RMI 调用超时的异常。 + */ +public class MqttRmiTimeoutException extends MqttRmiException { + + public MqttRmiTimeoutException(String requestId) { + super(requestId); + } + + public MqttRmiTimeoutException(String requestId, String message) { + super(requestId, message); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerBeanDefinitionRegistryPostProcessor.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerBeanDefinitionRegistryPostProcessor.java new file mode 100644 index 0000000..279e3f2 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerBeanDefinitionRegistryPostProcessor.java @@ -0,0 +1,67 @@ +package com.ruoyi.mqtt.rmi.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.rmi.MqttRmiRequestSender; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiScan; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiService; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; + +import java.util.*; +import java.util.stream.Collectors; + + +@Slf4j +/** + * MQTT RMI 消费者 Bean 定义注册后置处理器 + * + * 该类负责扫描 @MqttRmiScan 注解,并为带有 @MqttRmiService 注解的接口创建代理 Bean。 + */ +public class MqttRmiConsumerBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { + + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException { + + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { + Map scanBean = configurableListableBeanFactory.getBeansWithAnnotation(MqttRmiScan.class); + List basePackages = new ArrayList<>(); + + for (Object obj : scanBean.values()) { +// log.info("{}={}", obj.getClass(), obj.getClass().isAnnotationPresent(MqttRmiScan.class)); + MqttRmiScan mqttRmiScan = obj.getClass().getAnnotation(MqttRmiScan.class); + String[] temp = new String[]{obj.getClass().getPackage().getName()}; + if (mqttRmiScan.value().length > 0) { + temp = mqttRmiScan.value(); + } + Collections.addAll(basePackages, temp); + } + log.info("basePackages:{}", basePackages); + + + Set> set = new HashSet<>(); + for (String basePackage : basePackages) { + set.addAll(ClassUtil.scanPackageByAnnotation(basePackage, MqttRmiService.class).stream().filter(Class::isInterface).collect(Collectors.toSet())); + + } + if (CollUtil.isEmpty(set)) { + return; + } + + log.info("mqttRmiConsumerProxy:{}", set); + Object bean = new MqttRmiConsumerProxy(set.toArray(new Class[0])).createProxy(); + configurableListableBeanFactory.autowireBean(bean); + configurableListableBeanFactory.registerSingleton("mqttRmiConsumerProxy", bean); + + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerImpl.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerImpl.java new file mode 100644 index 0000000..d048093 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerImpl.java @@ -0,0 +1,158 @@ +package com.ruoyi.mqtt.rmi.impl; + + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.MqttFactory; +import com.ruoyi.mqtt.MqttItem; +import com.ruoyi.mqtt.rmi.MqttRmiRequest; +import com.ruoyi.mqtt.rmi.MqttRmiRequestSender; +import com.ruoyi.mqtt.rmi.MqttRmiResponse; +import com.ruoyi.mqtt.rmi.config.MqttRmiProperties; +import com.ruoyi.mqtt.rmi.exception.MqttRmiErrorException; +import com.ruoyi.mqtt.rmi.exception.MqttRmiTimeoutException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +import static com.ruoyi.mqtt.rmi.Const.*; + + +@RequiredArgsConstructor +@Slf4j +/** + * MQTT RMI 消费者实现类 + * + * 该类负责发送 MQTT RMI 请求,并监听和处理 MQTT 响应消息。 + */ +public class MqttRmiConsumerImpl implements IMqttMessageListener, MqttRmiRequestSender, PriorityOrdered { + + private final MqttRmiProperties properties; + private final MqttFactory factory; + private final ObjectMapper mapper; + private final ApplicationContext act; + + + private MqttItem mqttItem; + private TimedCache> requestCache; + + @Getter + private static MqttRmiRequestSender sender; + + + @PostConstruct + public void init() throws Exception { + sender = this; + if (!properties.getConsumer() || !properties.getEnabled()) { + log.info("MqttRmiConsumer未激活"); + return; + } + log.info("MqttRmiConsumer已激活"); + + mqttItem = factory.get(properties.getConfigName()); + mqttItem.getClient().subscribe(properties.getTopic().getTopic() + RESPONSE_TOPICE, properties.getTopic().getQos(), this).waitForCompletion(); + + requestCache = CacheUtil.newTimedCache(properties.getTimeout()); +// requestCache = new TimedCache<>(properties.getTimeout()); + requestCache.setListener((a, b) -> { + if (!b.isDone()) { + b.complete(null); + } + }); + requestCache.schedulePrune(properties.getTimeout() / 10); + } + + @Override + public ObjectMapper getMapper() { + return mapper; + } + + public MqttRmiResponse request(MqttRmiRequest request, long timeout) { + if (StrUtil.isBlank(request.getRequestId())) { + request.setRequestId(IdUtil.nanoId()); + } + CompletableFuture future = new CompletableFuture<>(); + if (timeout > 0) { + requestCache.put(request.getRequestId(), future, timeout); + } else { + requestCache.put(request.getRequestId(), future); + } + send(request); + MqttRmiResponse ret = future.join(); + if (ObjUtil.isNull(ret)) { +// return MqttRmiResponse.builder() +// .requestId(request.getRequestId()) +// .name(request.getRequestId()) +// .method(request.getRequestId()) +// .ok(false) +// .error("timeout") +// .build(); + throw new MqttRmiTimeoutException(request.getRequestId()); + } + requestCache.remove(request.getRequestId()); + if (!ret.getOk()) { + throw new MqttRmiErrorException(ret.getRequestId(), ret.getError()); + } + return ret; + } + + + public void send(MqttRmiRequest request) { + try { + mqttItem.getClient().publish( + properties.getTopicRequestPrefix() + properties.getTopic().getTopic() + REQUEST_TOPICE, + mapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8), + properties.getTopic().getQos(), + properties.getTopic().getRetained()); + } catch (Exception ex) { + log.warn("MqttRmiRequest send error", ex); + } + } + + + public void messageArrived(String topic, MqttMessage message) throws Exception { + String json = new String(message.getPayload(), StandardCharsets.UTF_8); + MqttRmiResponse response = null; + try { + response = mapper.readValue(json, MqttRmiResponse.class); + } catch (JsonProcessingException e) { + log.debug("MqttRmiConsumer json parse error: {}", + json); + return; + } + String requestId = response.getRequestId(); + if (StrUtil.isBlank(requestId) || !requestCache.containsKey(requestId)) { + log.debug("MqttRmiConsumer ignore requestId: {} = {}", + requestId, json); + return; + } + try { + requestCache.get(requestId).complete(response); + } catch (Exception e) { + log.debug("MqttRmiConsumer complete error requestId: {} ", + requestId); + } + + } + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; // 最高优先级 + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerProxy.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerProxy.java new file mode 100644 index 0000000..697783f --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiConsumerProxy.java @@ -0,0 +1,93 @@ +package com.ruoyi.mqtt.rmi.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.rmi.MqttRmiRequest; +import com.ruoyi.mqtt.rmi.MqttRmiRequestSender; +import com.ruoyi.mqtt.rmi.MqttRmiResponse; +import com.ruoyi.mqtt.rmi.MqttRmiUtil; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiMethod; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Slf4j +/** + * MQTT RMI 消费者代理类 + * + * 该类负责创建 MQTT RMI 服务的代理实例,并处理远程方法调用的逻辑。 + */ +public class MqttRmiConsumerProxy implements InvocationHandler { + + // 目标接口的Class对象 + private final Class[] targetInterfaces; + + + /** + * 创建代理实例 + */ + @SuppressWarnings("unchecked") + public T createProxy() { + return (T) Proxy.newProxyInstance( + Thread.currentThread().getContextClassLoader(), + targetInterfaces, + this + ); + } + + /** + * 代理方法逻辑 + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 方法调用前的逻辑 + + Class clazz = method.getDeclaringClass(); + String name = clazz.getSimpleName(); + if (clazz.isAnnotationPresent(MqttRmiService.class)) { + MqttRmiService mqttRmiService = clazz.getAnnotation(MqttRmiService.class); + if (StrUtil.isNotBlank(mqttRmiService.value())) { + name = mqttRmiService.value(); + } + } + + String methodName = method.getName(); + long timeout = 0; + if (method.isAnnotationPresent(MqttRmiMethod.class)) { + MqttRmiMethod mqttRmiMethod = method.getAnnotation(MqttRmiMethod.class); + if (StrUtil.isNotBlank(mqttRmiMethod.value())) { + methodName = mqttRmiMethod.value(); + } + timeout = mqttRmiMethod.timeout(); + } + List list = new ArrayList<>(); + if (ArrayUtil.isNotEmpty(args)) { + for (Object obj : args) { + list.add(MqttRmiUtil.getMapper().valueToTree(obj)); + } + } + + log.debug("MqttRmiConsumerProxy remote invoke: name={},method={},timeout={},args={}", name, methodName, timeout, list); + MqttRmiRequest request = MqttRmiRequest.builder() + .name(name) + .method(methodName) + .args(list) + .build(); + MqttRmiResponse response = MqttRmiUtil.request(request, timeout); + + Class retType = method.getReturnType(); + if (retType == void.class) { + return null; + } + return MqttRmiUtil.getMapper().convertValue(response.getBody(),retType); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiProviderBeanPostProcessor.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiProviderBeanPostProcessor.java new file mode 100644 index 0000000..4b446c5 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiProviderBeanPostProcessor.java @@ -0,0 +1,86 @@ +package com.ruoyi.mqtt.rmi.impl; + + +import com.ruoyi.mqtt.rmi.annotation.MqttRmiMethod; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiProvider; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Getter +@Configuration +@RequiredArgsConstructor +/** + * MQTT RMI 提供者 Bean 后置处理器 + * + * 该类负责扫描并注册带有 @MqttRmiProvider 注解的 Bean, + * 以便在 MQTT 消息到达时能够调用相应的方法。 + */ +public class MqttRmiProviderBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { + + + public final static String SP = "\t"; + + private final static Map methodMap = new HashMap<>(); + private final static Map beanMap = new HashMap<>(); + + public final static Collection getMethods() { + return methodMap.values(); + } + + public final static Method getMethod(String name, String method) { + return methodMap.get(name + SP + method); + } + + public final static Collection getBeans() { + return beanMap.values(); + } + + public final static Object getBean(String name) { + return beanMap.get(name); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (bean.getClass().isAnnotationPresent(MqttRmiProvider.class)) { + String name = bean.getClass().getSimpleName(); + MqttRmiProvider mqttRmiProvider = bean.getClass().getAnnotation(MqttRmiProvider.class); + if (!mqttRmiProvider.value().isEmpty()) { + name = mqttRmiProvider.value(); + } + + beanMap.put(name,bean); + for (Method m : bean.getClass().getMethods()) { + if (!m.isAnnotationPresent(MqttRmiMethod.class)) { + continue; + } + String methodName = m.getName(); + MqttRmiMethod mqttRmiMethod = m.getAnnotation(MqttRmiMethod.class); + if (!mqttRmiMethod.value().isEmpty()) { + methodName = mqttRmiMethod.value(); + } + + methodMap.put(name + SP + methodName, m); + log.debug("MqttRmiProvider: name={},method={} > {}", name, methodName,m.toString()); + } + } + return bean; + } + + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; // 最高优先级 + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiProviderImpl.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiProviderImpl.java new file mode 100644 index 0000000..52f1cf2 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/impl/MqttRmiProviderImpl.java @@ -0,0 +1,137 @@ +package com.ruoyi.mqtt.rmi.impl; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.MqttFactory; +import com.ruoyi.mqtt.MqttItem; +import com.ruoyi.mqtt.rmi.MqttRmiRequest; +import com.ruoyi.mqtt.rmi.MqttRmiResponse; +import com.ruoyi.mqtt.rmi.config.MqttRmiProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +import javax.annotation.PostConstruct; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +import static com.ruoyi.mqtt.rmi.Const.*; +import static com.ruoyi.mqtt.rmi.impl.MqttRmiProviderBeanPostProcessor.*; + +@RequiredArgsConstructor +@Slf4j +/** + * MQTT RMI 提供者实现类 + * + * 该类负责监听 MQTT 消息,并根据消息内容调用相应的本地方法。 + */ +public class MqttRmiProviderImpl implements PriorityOrdered, IMqttMessageListener { + + private final MqttRmiProperties properties; + private final MqttFactory factory; + private final ObjectMapper mapper; + + private MqttItem mqttItem; + + @PostConstruct + public void afterPropertiesSet() throws Exception { + if (!properties.getProvider() || !properties.getEnabled()) { + log.info("MqttRmiProvider未激活"); + return; + } + log.info("MqttRmiProvider已激活"); + + mqttItem = factory.get(properties.getConfigName()); + mqttItem.getClient().subscribe(properties.getTopic().getTopic() + REQUEST_TOPICE, properties.getTopic().getQos(), this).waitForCompletion(); + + } + + + private void send(MqttRmiResponse response) { + try { + mqttItem.getClient().publish( + properties.getTopic().getTopic() + RESPONSE_TOPICE, + mapper.writeValueAsString(response).getBytes(StandardCharsets.UTF_8), + properties.getTopic().getQos(), + properties.getTopic().getRetained()); + } catch (Exception ex) { + log.warn("MqttRmiReponse send error", ex); + } + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + // if (!event.getTopic().equals(properties.getTopic().getTopic() + REQUEST_TOPICE)) { +// return true; +// } +// log.debug("size:{}",mqttItem.getMessageHandlers().size()); + String json = new String(message.getPayload(), StandardCharsets.UTF_8); + log.debug("MqttRmiProvider json={}", json); + + MqttRmiRequest request = null; + try { + request = mapper.readValue(json, MqttRmiRequest.class); + } catch (JsonProcessingException e) { + log.warn("MqttRmiRequest JSON Error", e); + } + MqttRmiResponse response = new MqttRmiResponse(); + response.setOk(false); + response.setName(request.getName()); + response.setRequestId(request.getRequestId()); + response.setMethod(request.getMethod()); + + Method method = getMethod(request.getName(), request.getMethod()); + //如果没有对应的方法不做处理 + if (method == null) { + if (properties.getNotFindMethodSendError()) { + response.setError("not find method"); + send(response); + return; + } else { + log.warn("MqttRmiProvider not find method"); + return; + } + + } + Class[] pts = method.getParameterTypes(); + Object[] args = new Object[pts.length]; + for (int i = 0; i < pts.length; i++) { + try { + args[i] = mapper.convertValue(request.getArgs().get(i), pts[i]); + } catch (Exception e) { + response.setError("args error"); +// response.setStackTrace(getStackTraceAsString(e)); + log.warn("MqttRmiProvider args error", e); + send(response); + return; + } + } + + Object ret = null; + try { + ret = method.invoke(getBean(request.getName()), args); + response.setOk(true); + if (ret != null) { + response.setBody(mapper.valueToTree(ret)); + } + send(response); + } catch (Exception e) { + response.setError(e.getCause().getMessage()); +// response.setStackTrace(getStackTraceAsString(e.getCause())); + log.warn("MqttRmiProvider invoke error: " + e.getCause().getMessage(), e.getCause()); + send(response); + } + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; // 最高优先级 + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiScanEnabler.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiScanEnabler.java new file mode 100644 index 0000000..84974ad --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiScanEnabler.java @@ -0,0 +1,14 @@ +package com.ruoyi.mqtt.rmi.test; + +import com.ruoyi.mqtt.rmi.annotation.MqttRmiScan; +import org.springframework.stereotype.Component; + +//@Component +@MqttRmiScan +/** + * MQTT RMI 扫描启用类 + * + * 该类用于启用 MQTT RMI 扫描功能,扫描默认包路径。 + */ +public class MqttRmiScanEnabler { +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiScanEnabler1.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiScanEnabler1.java new file mode 100644 index 0000000..16764fa --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiScanEnabler1.java @@ -0,0 +1,14 @@ +package com.ruoyi.mqtt.rmi.test; + +import com.ruoyi.mqtt.rmi.annotation.MqttRmiScan; +import org.springframework.stereotype.Component; + +//@Component +@MqttRmiScan({"com.ruoyi.service.xxxxxx"}) +/** + * MQTT RMI 扫描启用类1 + * + * 该类用于启用 MQTT RMI 扫描功能,扫描指定包路径。 + */ +public class MqttRmiScanEnabler1 { +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestProvider.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestProvider.java new file mode 100644 index 0000000..84e3c59 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestProvider.java @@ -0,0 +1,38 @@ +package com.ruoyi.mqtt.rmi.test; + +import com.ruoyi.mqtt.rmi.annotation.MqttRmiMethod; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +//@Component +@MqttRmiProvider +@Slf4j +/** + * MQTT RMI 测试提供者类 + * + * 该类用于测试 MQTT RMI 功能,包含几个示例方法。 + */ +public class MqttRmiTestProvider { + + @MqttRmiMethod + public void test0() { + log.debug("test0"); + } + + @MqttRmiMethod + public void test1(String name) { + log.debug("test1:{}",name); + } + + @MqttRmiMethod("test22") + public String test2(String name) { + log.debug("test2:{}",name); + return "test2+"+name; + } + + @MqttRmiMethod + public String test3(String name) { + throw new RuntimeException(name); + } +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestService.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestService.java new file mode 100644 index 0000000..703cbf2 --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestService.java @@ -0,0 +1,21 @@ +package com.ruoyi.mqtt.rmi.test; + +import com.ruoyi.mqtt.rmi.annotation.MqttRmiMethod; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiScan; +import com.ruoyi.mqtt.rmi.annotation.MqttRmiService; + + +//@MqttRmiService("MqttRmiTestProvider") +/** + * MQTT RMI 测试服务接口 + * + * 该接口用于测试 MQTT RMI 功能,定义了几个示例方法。 + */ +public interface MqttRmiTestService { + + void test0(); + void test1(String name); + @MqttRmiMethod(value="test22",timeout = 500) + String test2(String name); + String test3(String name); +} diff --git a/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestServiceTest.java b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestServiceTest.java new file mode 100644 index 0000000..5e5a7de --- /dev/null +++ b/ruoyi-mqtt/src/main/java/com/ruoyi/mqtt/rmi/test/MqttRmiTestServiceTest.java @@ -0,0 +1,64 @@ +package com.ruoyi.mqtt.rmi.test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.mqtt.rmi.MqttRmiRequestSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.DependsOn; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +//@Component +@RequiredArgsConstructor +@Slf4j +public class MqttRmiTestServiceTest implements ApplicationRunner { + + private final MqttRmiTestService service; + + private final MqttRmiRequestSender sender; + private final ObjectMapper mapper; + +// public MqttRmiTestServiceTest() { +// log.info("MqttRmiTestServiceTest create"); +// } + + + public void test() { + String s = Long.toString(System.currentTimeMillis(), 36); + try { + service.test0(); + log.info("service.test0 success"); + } catch (Exception e) { + log.info("service.test0 error",e); + } + try { + service.test1(s); + log.info("service.test1(s) success"); + } catch (Exception e) { + log.info("service.test1(s) error",e); + } + + try { + String ret =service.test2(s); + log.info("service.test2(s) success: "+ret); + } catch (Exception e) { + log.info("service.test2(s) error",e); + } + + try { + String ret =service.test3(s); + log.info("service.test3(s) success: "+ret); + } catch (Exception e) { + log.info("service.test3(s) error",e); + } + } + + @Override + public void run(ApplicationArguments args) throws Exception { + test(); + } +} diff --git a/ruoyi-mqtt/src/main/resources/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-mqtt/src/main/resources/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f3a8b93 --- /dev/null +++ b/ruoyi-mqtt/src/main/resources/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.ruoyi.mqtt.config.MqttConfig +com.ruoyi.mqtt.rmi.config.MqttRmiConfig diff --git a/ruoyi-mqtt/src/main/resources/template.yaml b/ruoyi-mqtt/src/main/resources/template.yaml new file mode 100644 index 0000000..88aa95a --- /dev/null +++ b/ruoyi-mqtt/src/main/resources/template.yaml @@ -0,0 +1,45 @@ +--- # mqtt配置 +spring: + mqtt: + enabled: true # 是否启用MQTT功能, 默认值: false + default-config: default # 默认配置名称, 默认值: "default" + configs: + default: + enabled: true # 是否启用该MQTT客户端配置, 默认值: true + client-id: ${ruoyi.name}-client # 客户端编号(唯一), 默认值: 基于系统时间生成的36进制字符串 + url: tcp://192.168.3.222:1883 # 服务器URL, 默认值: tcp://127.0.0.1:1883 + urls: # 服务器集群URL列表(可选) + - tcp://192.168.3.222:1883 + username: ${ruoyi.name} # 用户名, 默认值: admin + password: ${ruoyi.name}1415926 # 密码, 默认值: 123456 + clean-session: true # 是否清空session, 默认值: true + connection-timeout: 5 # 连接超时时间(秒), 默认值: 5 + keep-alive-interval: 60 # 心跳时间(秒), 默认值: 60 + will-topic: will/topic # 遗嘱主题, 默认值: will/topic + will-message: offline # 遗嘱消息, 默认值: offline + will-qos: 0 # 遗嘱消息的服务质量(QoS), 默认值: 0 + custom-web-socket-headers: {} # 自定义WebSocket头部信息 + ssl-properties: {} # SSL属性配置 + mqtt-version: MQTT_VERSION_DEFAULT # MQTT协议版本, 默认值: MQTT_VERSION_DEFAULT + max-inflight: 10 # 最大未确认消息数量, 默认值: 10 + sends: # 发送主题映射 发送主题可以使用如:{0},{1},{2},发送时使用params数组替换 + test: + topic: ${ruoyi.name}/test/{0} + qos: 0 # 服务质量(QoS), 默认值: 0 + retained: false # 是否保留消息, 默认值: false + subscribes: # 订阅主题列表,订阅主题可以使用 单级通配符 "+" 和多级通配符 "#" + - topic: ${ruoyi.name}/# + qos: 0 # 服务质量(QoS), 默认值: 0 + + rmi: + enabled: false # 是否启用MQTT的远程方法调用功能, 默认值: false + provider: true # 是否是提供者,负责接口的实现, 默认值: false + consumer: true # 是否是消费者,负责接口的定义, 默认值: false + config-name: default # 默认配置名称, 默认值: "default" + topic: # 用于rmi的发送和订阅主题,不能含变量和通配符 + topic: ${ruoyi.name}/rmi # 主题名称 + qos: 0 # 服务质量(QoS), 默认值: 0 + retained: false # 是否保留消息, 默认值: false + topic-request-prefix: "" # 发送请求时,主题添加的前缀 + not-find-method-send-error: true # 没有发现方法是否发送错误, 默认值: true + timeout: 500 # 远程方法调用默认超时(毫秒), 默认值: 500 diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java index 1f85c40..4d6d3b8 100644 --- a/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java @@ -6,10 +6,7 @@ import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import com.ruoyi.common.utils.spring.SpringUtils; -import lombok.AccessLevel; -import lombok.Data; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -21,6 +18,7 @@ import java.io.File; import java.io.InputStream; +@EqualsAndHashCode(callSuper = true) @Configuration @ConfigurationProperties(prefix = "ruoyi.file") @Data