整个工具的代码都在Gitee或者Github地址内
gitee:solomon-parent: 这个项目主要是总结了工作上遇到的问题以及学习一些框架用于整合例如:rabbitMq、reids、Mqtt、S3协议的文件服务器、mongodb
需要引入的JAR包
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
</dependency>
需要引入的JAR包(版本根据自身要求使用,本教程用的版本均为最新)
1.新增MessageListener注解
java
@Target(value = { ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Component
@Conditional(MqttCondition.class)
public @interface MessageListener {
/**
* 主题
*/
String[] topics();
/**
* 消息质量
*/
int qos() default 0;
/**
* 允许订阅的租户范围
*/
String[] tenantRange() default StrUtil.EMPTY;
}
2.异常编码
java
public interface MqttErrorCode extends BaseExceptionCode {
String CLIENT_IS_NULL="CLIENT_IS_NULL";
}
3.Mqtt开关控制
java
public class MqttCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String enabled = ValidateUtils.getOrDefault(context.getEnvironment().getProperty("mqtt.enabled"),"true");
return BooleanUtil.toBoolean(enabled);
}
}
4.Mqtt配置
java
@Configuration
@EnableConfigurationProperties(value = {TenantMqttProfile.class})
@Import(value = {MqttUtils.class})
@ConditionalOnProperty(name = "mqtt.enabled", havingValue = "true", matchIfMissing = true)
public class MqttConfig extends AbstractMessageLineRunner<MessageListener> {
private final TenantMqttProfile profile;
private final MqttUtils mqttUtils;
public MqttConfig(TenantMqttProfile profile, ApplicationContext applicationContext, MqttUtils mqttUtils) {
this.profile = profile;
this.mqttUtils = mqttUtils;
SpringUtil.setContext(applicationContext);
}
@Override
public void init(List<Object> clazzList) throws Exception {
// 检查 MQTT 是否启用
if (!profile.getEnabled()) {
logger.error("mqtt 不启用,不初始化队列以及消费者");
return;
}
// 获取所有租户的配置 Map<租户编码,MQTT 配置>
Map<String, MqttProfile> tenantProfileMap = profile.getTenant();
if (ValidateUtils.isEmpty(tenantProfileMap)) {
logger.error("AbstractMessageLineRunner:没有 MQTT 配置");
return;
}
// 获取 MqttInitService Bean,如果不存在则使用默认实现
Map<String, MqttInitService> abstractMQMap = SpringUtil.getBeansOfType(MqttInitService.class);
MqttInitService mqttInitService = ValidateUtils.isNotEmpty(abstractMQMap)
? abstractMQMap.values().stream().findFirst().get()
: new DefaultMqttInitService(mqttUtils);
// 遍历所有租户配置,为每个租户初始化 MQTT Client
for (Map.Entry<String, MqttProfile> entry : tenantProfileMap.entrySet()) {
// entry.getKey()=租户编码,entry.getValue()=该租户的 MQTT 配置
mqttInitService.initMqttClient(entry.getKey(), entry.getValue(), clazzList);
}
}
}
java
public class MqttProfile {
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 连接
*/
private String url;
/**
* 客户端的标识(不可重复,为空时侯用uuid)
*/
private String clientId;
/**
* 连接超时(毫秒)
*/
private int completionTimeout = 30000;
/**
* 是否自动重连
*/
private boolean automaticReconnect = true;
/**
* 客户端掉线后,是否自动清除session
*/
private boolean cleanSession = false;
/**
* 心跳时间
*/
private int keepAliveInterval = MqttClientOptions.DEFAULT_KEEP_ALIVE_INTERVAL;
/**
* 遗嘱消息
*/
private MqttWill will;
/**
* 最大未确认消息数量
*/
private int maxInflight = MqttClientOptions.DEFAULT_MAX_INFLIGHT_QUEUE;
/**
* 重连次数(-1 无限重连 0 不重连)
*/
private int reconnectAttempts = -1;
/**
* 重连间隔(毫秒)- 注意:Vert.x 5.x 不支持自动重连
*/
private long reconnectInterval = 1000;
/**
* ssl连接是否验证证书
*/
private boolean verifyCertificate = false;
private VertxConfig vertx;
public static class MqttWill implements Serializable {
/**
* 遗嘱主题
*/
private String topic;
/**
* 遗嘱消息
*/
private String message;
/**
* 遗嘱消息质量
*/
private int qos;
/**
* 是否保留消息
*/
private boolean retained;
public boolean getRetained() {
return retained;
}
public void setRetained(boolean retained) {
this.retained = retained;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getQos() {
return qos;
}
public void setQos(int qos) {
this.qos = qos;
}
}
public static class VertxConfig implements Serializable {
// ==================== 基本配置 ====================
/**
* 事件循环线程池大小。默认值:2 * CPU 核心数。
*/
private int eventLoopPoolSize = VertxOptions.DEFAULT_EVENT_LOOP_POOL_SIZE;
/**
* Worker 线程池大小。默认值:20。
*/
private int workerPoolSize = VertxOptions.DEFAULT_WORKER_POOL_SIZE;
/**
* 内部阻塞线程池大小。默认值:20。
*/
private int internalBlockingPoolSize = VertxOptions.DEFAULT_INTERNAL_BLOCKING_POOL_SIZE;
/**
* 阻塞线程检查间隔(数值)。默认值:1(秒)。
*/
private long blockedThreadCheckInterval = 1L;
/**
* 阻塞线程检查间隔的时间单位。默认值:SECONDS。
*/
private TimeUnit blockedThreadCheckIntervalUnit = TimeUnit.SECONDS;
/**
* 事件循环线程最大执行时间(数值)。默认值:2(秒)。
*/
private long maxEventLoopExecuteTime = 2L;
/**
* 事件循环线程最大执行时间单位。默认值:SECONDS。
*/
private TimeUnit maxEventLoopExecuteTimeUnit = TimeUnit.SECONDS;
/**
* Worker 线程最大执行时间(数值)。默认值:60(秒)。
*/
private long maxWorkerExecuteTime = 60L;
/**
* Worker 线程最大执行时间单位。默认值:SECONDS。
*/
private TimeUnit maxWorkerExecuteTimeUnit = TimeUnit.SECONDS;
/**
* 警告异常时间(数值)。默认值:5(秒)。
*/
private long warningExceptionTime = 5L;
/**
* 警告异常时间单位。默认值:SECONDS。
*/
private TimeUnit warningExceptionTimeUnit = TimeUnit.SECONDS;
// ==================== 高可用配置 ====================
/**
* 是否启用高可用模式。默认值:false。
*/
private boolean haEnabled = false;
/**
* 法定节点数。默认值:1。
*/
private int quorumSize = 1;
/**
* 高可用组名。默认值:__DEFAULT__。
*/
private String haGroup = "__DEFAULT__";
// ==================== 高级配置 ====================
/**
* 是否优先使用原生传输(如 epoll、kqueue)。默认值:false。
*/
private boolean preferNativeTransport = false;
/**
* 是否禁用线程上下文类加载器(TCCL)。默认值:false。
*/
private boolean disableTCCL = false;
/**
* 是否使用守护线程。默认值:false。
*/
private Boolean useDaemonThread = false;
public int getEventLoopPoolSize() {
return eventLoopPoolSize;
}
public void setEventLoopPoolSize(int eventLoopPoolSize) {
this.eventLoopPoolSize = eventLoopPoolSize;
}
public int getWorkerPoolSize() {
return workerPoolSize;
}
public void setWorkerPoolSize(int workerPoolSize) {
this.workerPoolSize = workerPoolSize;
}
public int getInternalBlockingPoolSize() {
return internalBlockingPoolSize;
}
public void setInternalBlockingPoolSize(int internalBlockingPoolSize) {
this.internalBlockingPoolSize = internalBlockingPoolSize;
}
public long getBlockedThreadCheckInterval() {
return blockedThreadCheckInterval;
}
public void setBlockedThreadCheckInterval(long blockedThreadCheckInterval) {
this.blockedThreadCheckInterval = blockedThreadCheckInterval;
}
public TimeUnit getBlockedThreadCheckIntervalUnit() {
return blockedThreadCheckIntervalUnit;
}
public void setBlockedThreadCheckIntervalUnit(TimeUnit blockedThreadCheckIntervalUnit) {
this.blockedThreadCheckIntervalUnit = blockedThreadCheckIntervalUnit;
}
public long getMaxEventLoopExecuteTime() {
return maxEventLoopExecuteTime;
}
public void setMaxEventLoopExecuteTime(long maxEventLoopExecuteTime) {
this.maxEventLoopExecuteTime = maxEventLoopExecuteTime;
}
public TimeUnit getMaxEventLoopExecuteTimeUnit() {
return maxEventLoopExecuteTimeUnit;
}
public void setMaxEventLoopExecuteTimeUnit(TimeUnit maxEventLoopExecuteTimeUnit) {
this.maxEventLoopExecuteTimeUnit = maxEventLoopExecuteTimeUnit;
}
public long getMaxWorkerExecuteTime() {
return maxWorkerExecuteTime;
}
public void setMaxWorkerExecuteTime(long maxWorkerExecuteTime) {
this.maxWorkerExecuteTime = maxWorkerExecuteTime;
}
public TimeUnit getMaxWorkerExecuteTimeUnit() {
return maxWorkerExecuteTimeUnit;
}
public void setMaxWorkerExecuteTimeUnit(TimeUnit maxWorkerExecuteTimeUnit) {
this.maxWorkerExecuteTimeUnit = maxWorkerExecuteTimeUnit;
}
public long getWarningExceptionTime() {
return warningExceptionTime;
}
public void setWarningExceptionTime(long warningExceptionTime) {
this.warningExceptionTime = warningExceptionTime;
}
public TimeUnit getWarningExceptionTimeUnit() {
return warningExceptionTimeUnit;
}
public void setWarningExceptionTimeUnit(TimeUnit warningExceptionTimeUnit) {
this.warningExceptionTimeUnit = warningExceptionTimeUnit;
}
public boolean isHaEnabled() {
return haEnabled;
}
public void setHaEnabled(boolean haEnabled) {
this.haEnabled = haEnabled;
}
public int getQuorumSize() {
return quorumSize;
}
public void setQuorumSize(int quorumSize) {
this.quorumSize = quorumSize;
}
public String getHaGroup() {
return haGroup;
}
public void setHaGroup(String haGroup) {
this.haGroup = haGroup;
}
public boolean isPreferNativeTransport() {
return preferNativeTransport;
}
public void setPreferNativeTransport(boolean preferNativeTransport) {
this.preferNativeTransport = preferNativeTransport;
}
public boolean isDisableTCCL() {
return disableTCCL;
}
public void setDisableTCCL(boolean disableTCCL) {
this.disableTCCL = disableTCCL;
}
public Boolean getUseDaemonThread() {
return useDaemonThread;
}
public void setUseDaemonThread(Boolean useDaemonThread) {
this.useDaemonThread = useDaemonThread;
}
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public int getCompletionTimeout() {
return completionTimeout;
}
public void setCompletionTimeout(int completionTimeout) {
this.completionTimeout = completionTimeout;
}
public boolean isAutomaticReconnect() {
return automaticReconnect;
}
public void setAutomaticReconnect(boolean automaticReconnect) {
this.automaticReconnect = automaticReconnect;
}
public boolean isCleanSession() {
return cleanSession;
}
public void setCleanSession(boolean cleanSession) {
this.cleanSession = cleanSession;
}
public int getKeepAliveInterval() {
return keepAliveInterval;
}
public void setKeepAliveInterval(int keepAliveInterval) {
this.keepAliveInterval = keepAliveInterval;
}
public MqttWill getWill() {
return will;
}
public void setWill(MqttWill will) {
this.will = will;
}
public int getMaxInflight() {
return maxInflight;
}
public void setMaxInflight(int maxInflight) {
this.maxInflight = maxInflight;
}
public int getReconnectAttempts() {
return reconnectAttempts;
}
public void setReconnectAttempts(int reconnectAttempts) {
this.reconnectAttempts = reconnectAttempts;
}
public long getReconnectInterval() {
return reconnectInterval;
}
public void setReconnectInterval(long reconnectInterval) {
this.reconnectInterval = reconnectInterval;
}
public boolean isVerifyCertificate() {
return verifyCertificate;
}
public void setVerifyCertificate(boolean verifyCertificate) {
this.verifyCertificate = verifyCertificate;
}
public VertxConfig getVertx() {
return vertx;
}
public void setVertx(VertxConfig vertx) {
this.vertx = vertx;
}
}
java
@ConfigurationProperties("mqtt")
public class TenantMqttProfile {
public Map<String,MqttProfile> tenant;
//是否启用
private boolean enabled = true;
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Map<String, MqttProfile> getTenant() {
return tenant;
}
public void setTenant(Map<String, MqttProfile> tenant) {
this.tenant = tenant;
}
}
5.抽象消费者类
java
public abstract class AbstractConsumer<T, R> implements CommonMqttMessageListener<T, R, MqttModel<T>> {
protected final Logger logger = LoggerUtils.logger(getClass());
protected String tenantCode;
public void messageArrived(String topic, MqttPublishMessage message) throws Exception {
String json = message.payload().toString(StandardCharsets.UTF_8);
Throwable throwable = null;
R result = null;
MqttModel<T> model = null;
try {
model = conversion(json);
tenantCode = model.getTenantCode();
logger.info("线程名:{},租户编码为:{},消息ID:{},topic主题:{},{}:消费者消息: {}", Thread.currentThread().getName(), tenantCode, message.messageId(), topic,getClass().getName(), json);
// 判断是否重复消费
if (checkMessageKey(model)) {
throw new BaseException(MqErrorCode.MESSAGE_REPEAT_CONSUMPTION);
}
if (ValidateUtils.isNotEmpty(tenantCode)) {
RequestHeaderHolder.setTenantCode(tenantCode);
}
// 消费消息
result = this.handleMessage(model.getBody());
} catch (Throwable e) {
logger.error("AbstractConsumer:消费报错,消息为:{}, 异常为:", json, e);
throwable = e;
} finally {
deleteCheckMessageKey(model);
// 保存消费成功/失败消息
saveLog(result, throwable, model);
}
}
}
6.Mqtt接收发送实体类
java
public class MqttModel<T> extends BaseMq<T> {
private String topic;
private boolean retained;
private int qos;
public int getQos() {
return qos;
}
public void setQos(int qos) {
this.qos = qos;
}
public boolean getRetained() {
return retained;
}
public void setRetained(boolean retained) {
this.retained = retained;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public MqttModel(String tenantCode) {
super();
setTenantCode(tenantCode);
}
public MqttModel(String tenantCode, String topic, T body) {
super(body);
this.topic = topic;
setTenantCode(tenantCode);
}
}
7.初始化多租户连接接口
java
public interface MqttInitService {
void initMqttClient(String tenantCode, MqttProfile mqttProfile, List<Object> clazzList) throws Exception;
void initMqttClient(String tenantCode, MqttProfile mqttProfile) throws Exception;
}
java
public class DefaultMqttInitService implements MqttInitService {
private final Logger logger = LoggerUtils.logger(DefaultMqttInitService.class);
private final MqttUtils utils;
public DefaultMqttInitService(MqttUtils utils) {
this.utils = utils;
}
@Override
public void initMqttClient(String tenantCode, MqttProfile mqttProfile, List<Object> clazzList) throws Exception {
// 初始化 Vert.x
Vertx vertx = utils.initVertx(mqttProfile.getVertx());
utils.putVertx(tenantCode, vertx);
// 从配置 URL 中获取第一个地址 (支持集群配置,逗号分隔)
String url = mqttProfile.getUrl().split(",")[0];
// 解析 host 和 port
String[] parts = url.replace("tcp://", "").replace("ssl://", "").split(":");
String host = parts[0];
int port = parts.length > 1 ? Integer.parseInt(parts[1]) : 1883;
// 初始化 MQTT 连接配置
MqttClientOptions options = utils.initMqttConnectOptions(mqttProfile);
// 设置 clientId
String clientId = ValidateUtils.getOrDefault(mqttProfile.getClientId(), UUID.randomUUID().toString());
options.setClientId(clientId);
// 创建 MQTT Client
MqttClient mqttClient = MqttClient.create(vertx, options);
// 保存配置到 Map 中,key=租户编码
utils.putOptionsMap(tenantCode, options);
// 连接 MQTT Broker
mqttClient.connect(port, host).onComplete(ar -> {
if (ar.succeeded()) {
logger.info("租户:{} MQTT 连接成功,clientId:{}", tenantCode, clientId);
// 连接成功后订阅主题
try {
utils.subscribe(mqttClient, clazzList, tenantCode);
} catch (Exception e) {
logger.error("租户:{} 订阅主题失败", tenantCode, e);
}
} else {
logger.error("租户:{} MQTT 连接失败", tenantCode, ar.cause());
}
});
// 配置断开连接回调 - Vert.x 5.x 没有 reconnect 方法,需要手动重连
mqttClient.closeHandler(v -> {
logger.info("租户:{} MQTT 连接断开", tenantCode);
mqttClient.connect(port, host).onComplete(ar -> {
if (ar.succeeded()) {
logger.info("租户:{} MQTT 连接成功,clientId:{}", tenantCode, clientId);
// 连接成功后订阅主题
try {
utils.subscribe(mqttClient, clazzList, tenantCode);
} catch (Exception e) {
logger.error("租户:{} 订阅主题失败", tenantCode, e);
}
} else {
logger.error("租户:{} MQTT 连接失败", tenantCode, ar.cause());
}
});
});
// 保存 client 到 Map 中,key=租户编码
utils.putClient(tenantCode, mqttClient);
}
@Override
public void initMqttClient(String tenantCode, MqttProfile mqttProfile) throws Exception {
this.initMqttClient(tenantCode, mqttProfile, new ArrayList<>(SpringUtil.getBeansWithAnnotation(MessageListener.class).values()));
}
}
8.Mqtt工具类
java
public class MqttTopicFilterMatcher {
private MqttTopicFilterMatcher() {}
// ====================== 核心API ======================
/**
* 查找匹配主题的第一个通配符过滤器
*
* @param topic 主题 (e.g. "sensor/room1/temperature")
* @param filters 通配符主题过滤器集合 (e.g. ["sensor/+/temperature", "alarm/#"])
* @return 匹配成功的过滤器,若无匹配返回null
*/
public static String findFirstMatchingFilter(String topic, List<String> filters) {
for (String filter : filters) {
if (matches(filter, topic)) {
return filter;
}
}
return null;
}
/**
* 查找所有匹配主题的通配符过滤器
*
* @param topic 主题
* @param filters 通配符主题过滤器集合
* @return 所有匹配成功的过滤器列表(保持原顺序)
*/
public static List<String> findAllMatchingFilters(String topic, List<String> filters) {
if (topic == null || filters == null) {
return Collections.emptyList();
}
List<String> matches = new ArrayList<>();
for (String filter : filters) {
if (matches(filter, topic)) {
matches.add(filter);
}
}
return matches;
}
// ====================== 底层匹配逻辑 ======================
/**
* 判断主题是否匹配单个通配符过滤器
*
* @param filter 通配符主题过滤器 (e.g. "sensor/+/temperature")
* @param topic 实际主题 (e.g. "sensor/room1/temperature")
* @return true 如果匹配成功
*/
public static boolean matches(String filter, String topic) {
// 空检查(MQTT规范禁止空主题)
if (filter == null || topic == null ||
filter.isEmpty() || topic.isEmpty()) {
return false;
}
// 分割层级(保留末尾空字符串以检测非法结尾'/')
String[] filterLevels = filter.split("/", -1);
String[] topicLevels = topic.split("/", -1);
// 检查过滤器合法性(纯文本主题也需验证)
if (!isValidFilter(filterLevels)) {
return false;
}
// 逐层匹配
int i = 0;
while (i < filterLevels.length && i < topicLevels.length) {
String f = filterLevels[i];
String t = topicLevels[i];
// 处理非法空层级(规范禁止)
if (t.isEmpty()) {
return false;
}
if (f.equals("+")) {
// '+' 匹配任意单层非空主题
i++;
} else if (f.equals("#")) {
// '#' 必须位于末尾(已在isValidFilter中验证)
return true;
} else {
// 严格字符串匹配(兼容纯文本主题)
if (!f.equals(t)) {
return false;
}
i++;
}
}
// 检查层级数是否一致
return i == filterLevels.length && i == topicLevels.length;
}
/**
* 验证主题过滤器是否符合MQTT规范
*/
private static boolean isValidFilter(String[] levels) {
boolean hasMultiLevelWildcard = false;
for (int i = 0; i < levels.length; i++) {
String level = levels[i];
// 检查非法空层级(末尾空字符串允许表示以'/'结尾)
if (level.isEmpty() && i < levels.length - 1) {
return false;
}
// 检查多级通配符 #
if (level.equals("#")) {
if (hasMultiLevelWildcard || i != levels.length - 1) {
return false; // '#' 必须是最后一个层级
}
hasMultiLevelWildcard = true;
}
// 单级通配符+无需额外检查
}
return true;
}
// ====================== 使用示例 ======================
public static void main(String[] args) {
// 测试数据
String topic = "sensor/room1/temperature";
List<String> filters = List.of(
"sensor/+/temperature", // 通配符匹配
"sensor/room1/temperature", // 纯文本匹配
"alarm/#",
"sensor/room1/+"
);
// 场景1:获取第一个匹配的过滤器
String firstMatch = findFirstMatchingFilter(topic, filters);
System.out.println("第一个匹配: " + firstMatch);
// 输出: sensor/+/temperature
// 场景2:获取所有匹配的过滤器
List<String> allMatches = findAllMatchingFilters(topic, filters);
System.out.println("所有匹配: " + allMatches);
// 输出: [sensor/+/temperature, sensor/room1/temperature, sensor/room1/+]
// 验证纯文本主题匹配
assert matches("sensor/room1/temperature", "sensor/room1/temperature"); // true
assert !matches("sensor/room1/temp", "sensor/room1/temperature"); // false
}
}
java
@Configuration
public class MqttUtils implements SendService<MqttModel<?>> {
private final Logger logger = LoggerUtils.logger(MqttUtils.class);
private final Map<String, MqttClient> clientMap = new HashMap<>();
private final Map<String, MqttClientOptions> optionsMap = new HashMap<>();
private final Map<String, Vertx> vertxMap = new HashMap<>();
public Map<String, MqttClientOptions> getOptionsMap() {
return optionsMap;
}
public static Map<String,AbstractConsumer<?,?>> consumerMap = new HashMap<>();
public void putOptionsMap(String tenantCode, MqttClientOptions options) {
this.optionsMap.put(tenantCode, options);
}
public Map<String, MqttClient> getClientMap() {
return clientMap;
}
public void putClient(String tenantCode, MqttClient client) {
this.clientMap.put(tenantCode, client);
}
public Map<String, Vertx> getVertxMap() {
return vertxMap;
}
public void putVertx(String tenantCode, Vertx vertx) {
this.vertxMap.put(tenantCode, vertx);
}
/**
* 发送消息
*
* @param data 消息内容
*/
@Override
public void send(MqttModel<?> data) throws Exception {
try {
MqttClient client = getClient(data.getTenantCode());
String json = JSONUtil.toJsonStr(data);
client.publish(data.getTopic(),
Buffer.buffer(json.getBytes(StandardCharsets.UTF_8)),
MqttQoS.valueOf(data.getQos()),
data.getRetained(),
false);
} catch (Exception e) {
logger.error(String.format("MQTT: 主题[%s]发送消息失败", data.getTopic()), e);
throw e;
}
}
@Override
public void sendDelay(MqttModel<?> data, long delay) throws Exception {
send(data);
}
@Override
public void sendExpiration(MqttModel<?> data, long expiration) throws Exception {
send(data);
}
/**
* 订阅消息
*
* @param tenantCode 租户编码
* @param topic 主题
* @param qos 消息质量
* @param consumer 消费者
*/
public void subscribe(String tenantCode, String topic, int qos, AbstractConsumer<?, ?> consumer) throws BaseException {
if (ValidateUtils.isEmpty(topic)) {
return;
}
MqttClient client = getClient(tenantCode);
client.subscribe(topic, qos);
// Vert.x MQTT 通过 publishHandler 接收消息
client.publishHandler(message -> {
String msgTopic = message.topicName();
if (msgTopic.equals(topic)) {
try {
consumer.messageArrived(msgTopic, message);
} catch (Exception e) {
logger.error("消费消息异常:", e);
}
}
});
}
/**
* 订阅消息
*
* @param client mqtt连接
* @param tenantCode 租户编码
*/
public void subscribe(MqttClient client, String tenantCode) throws Exception {
List<Object> clazzList = new ArrayList<>(SpringUtil.getBeansWithAnnotation(MessageListener.class).values());
this.subscribe(client, clazzList, tenantCode);
}
/**
* 订阅消息
*
* @param client mqtt连接
* @param clazzList 消费者列表
* @param tenantCode 租户编码
*/
public void subscribe(MqttClient client, List<Object> clazzList, String tenantCode) throws Exception {
if (ValidateUtils.isNotEmpty(clazzList)) {
for (Object abstractConsumer : clazzList) {
MessageListener messageListener = AnnotationUtil.getAnnotation(abstractConsumer.getClass(), MessageListener.class);
if (ValidateUtils.isEmpty(messageListener) || ValidateUtils.isEmpty(messageListener.topics())) {
continue;
}
List<String> rangeList = Lambda.toList(Arrays.asList(messageListener.tenantRange()), ValidateUtils::isNotEmpty, key -> key);
if (ValidateUtils.isEmpty(rangeList) || rangeList.contains(tenantCode)) {
for (String topic : messageListener.topics()) {
topic = SpringUtil.getElValue(topic);
AbstractConsumer<?, ?> consumer = (AbstractConsumer<?, ?>) BeanUtil.copyProperties(abstractConsumer, abstractConsumer.getClass(), (String) null);
// 订阅主题
client.subscribe(topic, messageListener.qos());
consumerMap.put(topic, consumer);
}
client.publishHandler(message -> {
try {
String topic = MqttTopicFilterMatcher.findFirstMatchingFilter(message.topicName(), new ArrayList<>(consumerMap.keySet()));
AbstractConsumer<?, ?> consumer = consumerMap.get(topic);
if(ObjectUtil.isNull(consumer)){
logger.error("主题:{},不存在消费者,消费内容为:{}",message.topicName(),String.valueOf(message.payload()));
return;
}
consumer.messageArrived(message.topicName(), message);
} catch (Exception e) {
logger.error("消费消息异常,topic:{}", message.topicName(), e);
}
});
} else {
logger.info("{}租户,{}只支持{}范围", tenantCode, abstractConsumer.getClass().getSimpleName(), rangeList.toArray());
}
}
}
}
/**
* 取消订阅
*
* @param topic 主题
*/
public void unsubscribe(String tenantCode, String[] topic) throws Exception {
if (ValidateUtils.isEmpty(topic)) {
return;
}
MqttClient client = getClient(tenantCode);
for (String t : topic) {
client.unsubscribe(t);
}
}
/**
* 关闭连接
*/
public void disconnect(String tenantCode) throws Exception {
MqttClient client = getClient(tenantCode);
client.disconnect();
Vertx vertx = vertxMap.get(tenantCode);
if (vertx != null) {
vertx.close();
}
}
/**
* 重新连接 - Vert.x 5.x 没有内置 reconnect 方法,需要重新创建连接
*/
public void reconnect(String tenantCode) throws Exception {
// Vert.x 5.x 没有 reconnect 方法,需要重新创建连接
logger.warn("Vert.x 5.x 不支持 reconnect 方法,请重新初始化客户端");
}
public void reconnect(String tenantCode, MqttProfile mqttProfile) throws Exception {
reconnect(tenantCode);
}
private MqttClient getClient(String tenantCode) throws BaseException {
MqttClient client = getClientMap().get(tenantCode);
if (ValidateUtils.isEmpty(client)) {
throw new BaseException(MqttErrorCode.CLIENT_IS_NULL, tenantCode);
}
return client;
}
public MqttClientOptions initMqttConnectOptions(MqttProfile mqttProfile) {
MqttClientOptions options = new MqttClientOptions();
// 基本认证配置
options.setUsername(mqttProfile.getUserName());
options.setPassword(mqttProfile.getPassword());
// 会话配置
options.setCleanSession(mqttProfile.isCleanSession());
options.setAutoGeneratedClientId(false);
// 心跳配置
options.setKeepAliveInterval(mqttProfile.getKeepAliveInterval());
options.setAutoKeepAlive(true);
// 消息流控配置
options.setMaxInflightQueue(mqttProfile.getMaxInflight());
options.setAckTimeout(mqttProfile.getCompletionTimeout() / 1000); // 毫秒转秒
// 自动确认配置
options.setAutoAck(true);
options.setReconnectAttempts(mqttProfile.getReconnectAttempts());
options.setReconnectInterval(mqttProfile.getReconnectInterval());
// 设置遗嘱消息
MqttWill will = mqttProfile.getWill();
if (ValidateUtils.isNotEmpty(will)) {
options.setWillFlag(true);
options.setWillTopic(will.getTopic());
// Vert.x 5.x 使用 setWillMessageBytes 而不是 setWillMessage
if (ValidateUtils.isNotEmpty(will.getMessage())) {
options.setWillMessageBytes(Buffer.buffer(will.getMessage().getBytes(StandardCharsets.UTF_8)));
}
options.setWillQoS(will.getQos());
options.setWillRetain(will.getRetained());
}
// SSL/TLS 配置
if (mqttProfile.getUrl() != null && mqttProfile.getUrl().startsWith("ssl://")) {
options.setSsl(true);
}
if (!mqttProfile.isVerifyCertificate()) {
options.setTrustAll(true);
}
return options;
}
public Vertx initVertx(MqttProfile.VertxConfig vertxConfig) {
if (vertxConfig == null) {
return Vertx.vertx();
}
VertxOptions options = new VertxOptions()
.setEventLoopPoolSize(vertxConfig.getEventLoopPoolSize())
.setWorkerPoolSize(vertxConfig.getWorkerPoolSize())
.setInternalBlockingPoolSize(vertxConfig.getInternalBlockingPoolSize())
.setBlockedThreadCheckIntervalUnit(vertxConfig.getBlockedThreadCheckIntervalUnit())
.setMaxEventLoopExecuteTimeUnit(vertxConfig.getMaxEventLoopExecuteTimeUnit())
.setMaxWorkerExecuteTimeUnit(vertxConfig.getMaxWorkerExecuteTimeUnit())
.setWarningExceptionTimeUnit(vertxConfig.getWarningExceptionTimeUnit())
.setHAEnabled(vertxConfig.isHaEnabled())
.setQuorumSize(vertxConfig.getQuorumSize())
.setHAGroup(vertxConfig.getHaGroup())
.setPreferNativeTransport(vertxConfig.isPreferNativeTransport())
.setDisableTCCL(vertxConfig.isDisableTCCL());
if (vertxConfig.getUseDaemonThread() != null) {
options.setUseDaemonThread(vertxConfig.getUseDaemonThread());
}
return Vertx.vertx(options);
}
}