需要引入的JAR包
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
</dependencies>
需要一个注解标注消费者处理器
/**
* mqtt注解
*/
@Target(value = { ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Component
@Scope("prototype")
public @interface Mqtt {
/**
* 主题
*/
String[] topics();
/**
* 消息质量
*/
int qos() default 0;
}
其次就需要一个通用的消费处理器
public abstract class AbstractConsumer<T> implements IMqttMessageListener {
protected final Logger logger = LoggerUtils.logger(getClass());
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String json = new String(message.getPayload(), StandardCharsets.UTF_8);
try {
if(checkMessageKey(topic,message)){
throw new BaseException(MqErrorCode.MESSAGE_REPEAT_CONSUMPTION);
}
MqttModel mqttModel = JackJsonUtils.conversionClass(json, MqttModel.class);
HeardHolder.setTenantCode(mqttModel.getTenantCode());
logger.info("线程名:{},租户编码为:{},topic主题:{},AbstractConsumer:消费者消息: {}",mqttModel.getTenantCode(),Thread.currentThread().getName(),topic, json);
// 消费者消费消息
this.handleMessage((T) mqttModel.getBody());
} catch (Throwable e){
logger.info("AbstractConsumer:消费报错,消息为:{}, 异常为:",json, e);
saveFailMessage(topic,message,e);
}
}
/**
* 消费方法
* @param body 请求数据
*/
public abstract void handleMessage(T body) throws Exception;
/**
* 保存消费失败的消息
*
* @param message mq所包含的信息
* @param e 异常
*/
public void saveFailMessage(String topic,MqttMessage message, Throwable e){
}
/**
* 判断是否重复消费
* @return true 重复消费 false 不重复消费
*/
public boolean checkMessageKey(String topic, MqttMessage message){
return false;
}
}
然后就是发送mq消息的通用实体类
public class BaseMq<T> implements Serializable {
/**
* 消费者数据
*/
private T body;
public BaseMq(){
}
public BaseMq(T body){
this.body = body;
}
public T getBody() {
return body;
}
public void setBody(T body) {
this.body = body;
}
}
/**
* 基础发送MQ基类
*/
public class MqttModel<T> extends BaseMq<T> {
private String topic;
private String tenantCode;
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getTenantCode() {
return tenantCode;
}
public void setTenantCode(String tenantCode) {
this.tenantCode = tenantCode;
}
public MqttModel(String tenantCode) {
super();
this.tenantCode = tenantCode;
}
public MqttModel(String tenantCode,String topic,T body) {
super(body);
this.topic = topic;
this.tenantCode = tenantCode;
}
}
然后就是配置文件
public class MqttProfile {
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 连接
*/
private String url;
/**
* 客户端的标识(不可重复,为空时侯用uuid)
*/
private String clientId;
/**
* 连接超时
*/
private int completionTimeout = 30;
/**
* 是否自动重连
*/
private boolean automaticReconnect = false;
/**
* 客户端掉线后,是否自动清除session
*/
private boolean cleanSession = true;
/**
* 心跳时间
*/
private int keepAliveInterval = 60;
/**
* 遗嘱消息
*/
private MqttWill will;
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 MqttWill getWill() {
return will;
}
public void setWill(MqttWill will) {
this.will = will;
}
public int getKeepAliveInterval() {
return keepAliveInterval;
}
public void setKeepAliveInterval(int keepAliveInterval) {
this.keepAliveInterval = keepAliveInterval;
}
public boolean getCleanSession() {
return cleanSession;
}
public void setCleanSession(boolean cleanSession) {
this.cleanSession = cleanSession;
}
public boolean getAutomaticReconnect() {
return automaticReconnect;
}
public void setAutomaticReconnect(boolean automaticReconnect) {
this.automaticReconnect = automaticReconnect;
}
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;
}
}
这个是多租户的配置
@ConfigurationProperties("mqtt")
public class TenantMqttProfile {
public Map<String,MqttProfile> tenant;
public Map<String, MqttProfile> getTenant() {
return tenant;
}
public void setTenant(Map<String, MqttProfile> tenant) {
this.tenant = tenant;
}
}
然后就是工具类
public interface SendService<T extends BaseMq> {
/**
* 发送消息
*/
void send(T data) throws Exception;
/**
* 发送延缓信息
*/
void sendDelay(T data, long delay) throws Exception;
/**
* 发送消息,并设置消息过期时间
*/
void sendExpiration(T data, long expiration) throws Exception;
}
@Configuration
public class MqttUtils implements SendService<MqttModel> {
private Logger logger = LoggerUtils.logger(MqttUtils.class);
private Map<String,MqttClient> clientMap = new HashMap<>();
private Map<String,MqttConnectOptions> optionsMap = new HashMap<>();
public Map<String, MqttConnectOptions> getOptionsMap() {
return optionsMap;
}
public void putOptionsMap(String tenantCode, MqttConnectOptions 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);
}
/**
* 发送消息
* @param data 消息内容
*/
@Override
public void send(MqttModel data) throws Exception {
// 获取客户端实例
ObjectMapper mapper = new ObjectMapper();
try {
// 转换消息为json字符串
String json = mapper.writeValueAsString(data);
getClientMap().get(data.getTenantCode()).getTopic(data.getTopic()).publish(new MqttMessage(json.getBytes(StandardCharsets.UTF_8)));
} catch (JsonProcessingException e) {
logger.error(String.format("MQTT: 主题[%s]发送消息转换json失败", data.getTopic()));
} catch (MqttException e) {
logger.error(String.format("MQTT: 主题[%s]发送消息失败", data.getTopic()));
}
}
@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, IMqttMessageListener consumer) throws MqttException {
if(ValidateUtils.isEmpty(topic)){
return;
}
getClientMap().get(tenantCode).subscribe(topic, qos,consumer);
}
/**
* 订阅消息
* @param client mqtt连接
*/
public void subscribe(MqttClient client) throws MqttException {
List<Object> clazzList = new ArrayList<>(SpringUtil.getBeansWithAnnotation(Mqtt.class).values());
if (ValidateUtils.isNotEmpty(clazzList)) {
for (Object abstractConsumer : clazzList) {
Mqtt mqtt = AnnotationUtils.findAnnotation(abstractConsumer.getClass(), Mqtt.class);
if (ValidateUtils.isEmpty(mqtt)) {
continue;
}
for (String topic : mqtt.topics()) {
client.subscribe(topic, mqtt.qos(), (IMqttMessageListener) BeanUtil.copyProperties(abstractConsumer,abstractConsumer.getClass(), (String) null));
}
}
}
}
/**
* 取消订阅
* @param topic 主题
*/
public void unsubscribe(String tenantCode,String[] topic) throws MqttException {
if(ValidateUtils.isEmpty(topic)){
return;
}
getClientMap().get(tenantCode).unsubscribe(topic);
}
/**
* 关闭连接
*/
public void disconnect(String tenantCode) throws MqttException {
getClientMap().get(tenantCode).disconnect();
}
/**
* 重新连接
*/
public void reconnect(String tenantCode) throws MqttException {
MqttClient client = getClientMap().get(tenantCode);
if(!client.isConnected()){
client.connect(getOptionsMap().get(tenantCode));
subscribe(client);
}
}
public MqttConnectOptions initMqttConnectOptions(MqttProfile mqttProfile) {
MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
mqttConnectOptions.setUserName(mqttProfile.getUserName());
mqttConnectOptions.setPassword(mqttProfile.getPassword().toCharArray());
mqttConnectOptions.setServerURIs(new String[]{mqttProfile.getUrl()});
//设置同一时间可以发送的最大未确认消息数量
mqttConnectOptions.setMaxInflight(mqttProfile.getMaxInflight());
//设置超时时间
mqttConnectOptions.setConnectionTimeout(mqttProfile.getCompletionTimeout());
//设置自动重连
mqttConnectOptions.setAutomaticReconnect(mqttProfile.getAutomaticReconnect());
//cleanSession 设为 true;当客户端掉线时;服务器端会清除 客户端session;重连后 客户端会有一个新的session,cleanSession
// 设为false,客户端掉线后 服务器端不会清除session,当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息
mqttConnectOptions.setCleanSession(mqttProfile.getCleanSession());
// 设置会话心跳时间 单位为秒 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线,但这个方法并没有重连的机制
mqttConnectOptions.setKeepAliveInterval(mqttProfile.getKeepAliveInterval());
// 设置重新连接之间等待的最长时间
mqttConnectOptions.setMaxReconnectDelay(mqttProfile.getMaxReconnectDelay());
// 设置连接超时值,该值以秒为单位 0 禁用超时处理,这意味着客户端将等待,直到网络连接成功或失败.
mqttConnectOptions.setConnectionTimeout(mqttProfile.getConnectionTimeout());
// 设置执行器服务应等待的时间(以秒为单位)在强制终止之前终止.不建议更改除非您绝对确定需要,否则该值.
mqttConnectOptions.setExecutorServiceTimeout(mqttProfile.getExecutorServiceTimeout());
//设置遗嘱消息
if (ValidateUtils.isNotEmpty(mqttProfile.getWill())) {
MqttWill will = mqttProfile.getWill();
mqttConnectOptions.setWill(will.getTopic(), will.getMessage().getBytes(), will.getQos(), will.getRetained());
}
return mqttConnectOptions;
}
}
最后一步就是需要一个初始化配置的代码了
@Configuration
@IntegrationComponentScan
@EnableConfigurationProperties(value={TenantMqttProfile.class,})
@Import(value = {MqttUtils.class})
public class MqttConfig {
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);
}
@PostConstruct
public void afterPropertiesSet() throws Exception {
Map<String,MqttProfile> tenantProfileMap = profile.getTenant();
if(ValidateUtils.isEmpty(tenantProfileMap)){
return ;
}
Map<String, MqttInitService> abstractMQMap = SpringUtil.getBeansOfType(MqttInitService.class);
MqttInitService mqttInitService = ValidateUtils.isNotEmpty(abstractMQMap) ? abstractMQMap.values().stream().findFirst().get() : new DefaultMqttInitService(mqttUtils);
for(Entry<String,MqttProfile> entry: tenantProfileMap.entrySet()){
mqttInitService.initMqttClient(entry.getKey(),entry.getValue());
}
}
@Bean
@ConditionalOnMissingBean(MessageChannel.class)
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
然后配置一个初始化接口,如果使用者没有实现,则使用默认的初始化方法,默认初始化方法会在重连成功时候,重新订阅主题
public interface MqttInitService {
void initMqttClient(String tenantCode, MqttProfile mqttProfile) throws Exception;
}
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) throws Exception {
MqttClient mqttClient = new MqttClient(mqttProfile.getUrl(), ValidateUtils.getOrDefault(mqttProfile.getClientId(), UUID.randomUUID().toString()));
MqttConnectOptions options = utils.initMqttConnectOptions(mqttProfile);
mqttClient.connect(options);
utils.putOptionsMap(tenantCode,options);
// 订阅主题
utils.subscribe(mqttClient);
//配置callback
mqttClient.setCallback(new MqttCallbackExtended() {
@Override
public void connectComplete(boolean reconnect, String serverURI) {
logger.info("租户:{} 重连{}",tenantCode,reconnect ? "成功" : "失败");
if(reconnect){
List<Object> clazzList = new ArrayList<>(SpringUtil.getBeansWithAnnotation(Mqtt.class).values());
for (Object abstractConsumer : clazzList) {
Mqtt mqtt = AnnotationUtils.findAnnotation(abstractConsumer.getClass(), Mqtt.class);
if (ValidateUtils.isNotEmpty(mqtt)) {
try {
for(String topic : mqtt.topics()){
logger.info("租户:{} 重新订阅[{}]主题",tenantCode,topic);
mqttClient.subscribe(topic, mqtt.qos(), (IMqttMessageListener) BeanUtil.copyProperties(abstractConsumer,abstractConsumer.getClass(), (String) null));
}
} catch (MqttException e) {
logger.error("重连重新订阅主题失败,异常为:",e);
}
}
}
}
}
@Override
public void connectionLost(Throwable cause) {
logger.info("租户:{} 断开连接,异常为:",tenantCode,cause);
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
}
});
//保存client
utils.putClient(tenantCode,mqttClient);
}
}
基本上的整合流程就是这样子的了
如果需要看到使用配置可以参考如下
mqtt:
tenant:
default: #租户编码
user-name: #用户名
password: #密码
url: #连接
client-id: #客户端的标识(不可重复,为空时侯用uuid)
completion-timeout: #连接超时
automatic-reconnect: #是否自动重连
clean-session: #客户端掉线后,是否自动清除session
keep-alive-interval: #心跳时间
will: #遗嘱消息
topic: #遗嘱主题
message: #遗嘱消息
qos: #遗嘱消息质量
retained: #遗嘱是否保留消息
消费者实例
@Mqtt(qos = 2,topics = "test")
@Scope("prototype")
public class TestHandler extends AbstractConsumer {
@Override
public void handleMessage(String body) throws Exception {
//这里处理业务逻辑
}
}