From 3251fd2b9ef9c53bf5f72783056490275993ad08 Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Tue, 26 Jul 2022 16:56:34 +0300 Subject: [PATCH 1/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Introduce some initial design. Add a new interface `ClientManager` which will manage clients and connections. Use this manager in v3 topic adapter and message handler. --- .../integration/mqtt/core/ClientManager.java | 25 +++ .../mqtt/core/Mqttv3ClientManager.java | 145 ++++++++++++++++++ ...stractMqttMessageDrivenChannelAdapter.java | 16 +- .../MqttPahoMessageDrivenChannelAdapter.java | 104 +++++++++---- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 4 +- 5 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java create mode 100644 spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java new file mode 100644 index 00000000000..e09d7a1532d --- /dev/null +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.mqtt.core; + +import org.springframework.integration.support.management.ManageableLifecycle; + +public interface ClientManager extends ManageableLifecycle { + + T getClient(); + +} diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java new file mode 100644 index 00000000000..fbaadc7c74c --- /dev/null +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -0,0 +1,145 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.mqtt.core; + +import java.time.Instant; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import org.springframework.integration.context.IntegrationObjectSupport; +import org.springframework.integration.support.management.ManageableLifecycle; + +public class Mqttv3ClientManager extends IntegrationObjectSupport implements ClientManager, + ManageableLifecycle, MqttCallback { + + private AtomicReference> scheduledReconnect; + + private final MqttConnectOptions connectOptions; + + private final String clientId; + + private IMqttAsyncClient client; + + public Mqttv3ClientManager(MqttConnectOptions connectOptions, String clientId) throws MqttException { + this.connectOptions = connectOptions; + this.client = new MqttAsyncClient(connectOptions.getServerURIs()[0], clientId); + this.client.setCallback(this); + this.clientId = clientId; + } + + @Override + public IMqttAsyncClient getClient() { + return client; + } + + @Override + public void start() { + if (this.client == null) { + try { + this.client = new MqttAsyncClient(this.connectOptions.getServerURIs()[0], this.clientId); + } + catch (MqttException e) { + throw new IllegalStateException("could not start client manager", e); + } + this.client.setCallback(this); + } + try { + connect(); + } + catch (MqttException e) { + logger.error(e, "could not start client manager, scheduling reconnect, client_id=" + + this.client.getClientId()); + scheduleReconnect(); + } + } + + @Override + public void stop() { + if (this.client == null) { + return; + } + try { + this.client.disconnectForcibly(this.connectOptions.getConnectionTimeout()); + } + catch (MqttException e) { + logger.error(e, "could not disconnect from the client"); + } + finally { + try { + this.client.close(); + } + catch (MqttException e) { + logger.error(e, "could not close the client"); + } + this.client = null; + } + } + + @Override + public boolean isRunning() { + return this.client != null; + } + + private synchronized void connect() throws MqttException { + if (this.client == null) { + logger.error("could not connect on a null client reference"); + return; + } + MqttConnectOptions options = Mqttv3ClientManager.this.connectOptions; + this.client.connect(options).waitForCompletion(options.getConnectionTimeout()); + } + + @Override + public synchronized void connectionLost(Throwable cause) { + logger.error(cause, "connection lost, scheduling reconnect, client_id=" + this.client.getClientId()); + scheduleReconnect(); + } + + private void scheduleReconnect() { + if (this.scheduledReconnect.get() != null) { + this.scheduledReconnect.get().cancel(false); + } + this.scheduledReconnect.set(getTaskScheduler().schedule(() -> { + try { + connect(); + this.scheduledReconnect.set(null); + } + catch (MqttException e) { + logger.error(e, "could not reconnect"); + scheduleReconnect(); + } + }, Instant.now().plusSeconds(10))); + } + + @Override + public void messageArrived(String topic, MqttMessage message) { + // not this manager concern + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + // nor this manager concern + } +} diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java index da2d3f2c7ab..a3227e4c568 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java @@ -26,6 +26,7 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.log.LogMessage; import org.springframework.integration.endpoint.MessageProducerSupport; +import org.springframework.integration.mqtt.core.ClientManager; import org.springframework.integration.mqtt.support.MqttMessageConverter; import org.springframework.integration.support.management.IntegrationManagedResource; import org.springframework.jmx.export.annotation.ManagedAttribute; @@ -38,6 +39,8 @@ /** * Abstract class for MQTT Message-Driven Channel Adapters. * + * @param MQTT Client type + * * @author Gary Russell * @author Artem Bilan * @author Trung Pham @@ -48,7 +51,7 @@ */ @ManagedResource @IntegrationManagedResource -public abstract class AbstractMqttMessageDrivenChannelAdapter extends MessageProducerSupport +public abstract class AbstractMqttMessageDrivenChannelAdapter extends MessageProducerSupport implements ApplicationEventPublisherAware { /** @@ -70,6 +73,8 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends MessagePro private MqttMessageConverter converter; + protected ClientManager clientManager; + protected final Lock topicLock = new ReentrantLock(); // NOSONAR public AbstractMqttMessageDrivenChannelAdapter(@Nullable String url, String clientId, String... topic) { @@ -89,6 +94,15 @@ public void setConverter(MqttMessageConverter converter) { this.converter = converter; } + public void setClientManager(ClientManager clientManager) { + Assert.notNull(clientManager, "'clientManager' cannot be null"); + this.clientManager = clientManager; + } + + public ClientManager getClientManager() { + return this.clientManager; + } + /** * Set the QoS for each topic; a single value will apply to all topics otherwise * the correct number of qos values must be provided. diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index 19c88b6da64..5f33cdc8a4b 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -19,11 +19,13 @@ import java.time.Instant; import java.util.Arrays; import java.util.concurrent.ScheduledFuture; +import java.util.stream.Stream; -import org.eclipse.paho.client.mqttv3.IMqttClient; +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttMessageListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; @@ -59,7 +61,7 @@ * @since 4.0 * */ -public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter +public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter implements MqttCallback, MqttPahoComponent { /** @@ -75,7 +77,7 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv private long disconnectCompletionTimeout = DISCONNECT_COMPLETION_TIMEOUT; - private volatile IMqttClient client; + private volatile IMqttAsyncClient client; private volatile ScheduledFuture reconnectFuture; @@ -155,7 +157,7 @@ public MqttConnectOptions getConnectionInfo() { String url = getUrl(); if (url != null) { options = MqttUtils.cloneConnectOptions(options); - options.setServerURIs(new String[]{ url }); + options.setServerURIs(new String[] {url}); } } return options; @@ -187,18 +189,11 @@ protected void doStart() { @Override protected synchronized void doStop() { cancelReconnect(); + if (getClientManager() != null) { + unsubscribe(getClientManager().getClient()); + } if (this.client != null) { - try { - if (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_ALWAYS) - || (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_CLEAN) - && this.cleanSession)) { - - this.client.unsubscribe(getTopic()); - } - } - catch (MqttException ex) { - logger.error(ex, "Exception while unsubscribing"); - } + unsubscribe(this.client); try { this.client.disconnectForcibly(this.disconnectCompletionTimeout); } @@ -219,6 +214,20 @@ protected synchronized void doStop() { } } + private void unsubscribe(IMqttAsyncClient clientInstance) { + try { + if (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_ALWAYS) + || (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_CLEAN) + && this.cleanSession)) { + + clientInstance.unsubscribe(getTopic()); + } + } + catch (MqttException ex) { + logger.error(ex, "Exception while unsubscribing"); + } + } + @Override public void addTopic(String topic, int qos) { this.topicLock.lock(); @@ -263,22 +272,38 @@ private synchronized void connectAndSubscribe() throws MqttException { // NOSONA } Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null, "If no 'url' provided, connectionOptions.getServerURIs() must not be null"); - this.client = this.clientFactory.getClientInstance(getUrl(), getClientId()); - this.client.setCallback(this); - if (this.client instanceof MqttClient) { - ((MqttClient) this.client).setTimeToWait(getCompletionTimeout()); + + IMqttAsyncClient clientInstance; + + if (getClientManager() == null) { + this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); + this.client.setCallback(this); + clientInstance = this.client; + } + else { + clientInstance = getClientManager().getClient(); } this.topicLock.lock(); String[] topics = getTopic(); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); try { - this.client.connect(connectionOptions); - this.client.setManualAcks(isManualAcks()); + long completionTimeout = getCompletionTimeout(); + if (!clientInstance.isConnected()) { + clientInstance.connect(connectionOptions).waitForCompletion(completionTimeout); + } + clientInstance.setManualAcks(isManualAcks()); if (topics.length > 0) { int[] requestedQos = getQos(); - int[] grantedQos = Arrays.copyOf(requestedQos, requestedQos.length); - this.client.subscribe(topics, grantedQos); + MessageListener[] listeners = Stream.of(topics) + .map(t -> new MessageListener(client)) + .toArray(MessageListener[]::new); + IMqttToken subscribeToken = clientInstance.subscribe(topics, requestedQos, listeners); + subscribeToken.waitForCompletion(completionTimeout); + int[] grantedQos = subscribeToken.getGrantedQos(); + if (grantedQos.length == 1 && grantedQos[0] == 0x80) { + throw new MqttException(MqttException.REASON_CODE_SUBSCRIBE_FAILED); + } warnInvalidQosForSubscription(topics, requestedQos, grantedQos); } } @@ -304,7 +329,7 @@ private synchronized void connectAndSubscribe() throws MqttException { // NOSONA finally { this.topicLock.unlock(); } - if (this.client.isConnected()) { + if (getClientManager() != null || this.client.isConnected()) { this.connected = true; String message = "Connected and subscribed to " + Arrays.toString(topics); logger.debug(message); @@ -447,7 +472,7 @@ private static class AcknowledgmentImpl implements SimpleAcknowledgment { private final int qos; - private final IMqttClient ackClient; + private final IMqttAsyncClient ackClient; /** * Construct an instance with the provided properties. @@ -455,7 +480,7 @@ private static class AcknowledgmentImpl implements SimpleAcknowledgment { * @param qos the message QOS. * @param client the client. */ - AcknowledgmentImpl(int id, int qos, IMqttClient client) { + AcknowledgmentImpl(int id, int qos, IMqttAsyncClient client) { this.id = id; this.qos = qos; this.ackClient = client; @@ -478,4 +503,29 @@ public void acknowledge() { } + private class MessageListener implements IMqttMessageListener, MqttCallback { + + private final IMqttAsyncClient client; + + MessageListener(IMqttAsyncClient client) { + this.client = client; + } + + @Override + public void messageArrived(String topic, MqttMessage mqttMessage) { + MqttPahoMessageDrivenChannelAdapter.this.messageArrived(topic, mqttMessage); + } + + @Override + public void connectionLost(Throwable cause) { + // not this component concern + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + // not this component concern + } + + } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index aaab5792f0d..440860cdb1a 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -71,7 +71,7 @@ * @since 5.5.5 * */ -public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter +public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter implements MqttCallback, MqttComponent { private final MqttConnectionOptions connectionOptions; @@ -90,7 +90,7 @@ public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDr public Mqttv5PahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) { super(url, clientId, topic); this.connectionOptions = new MqttConnectionOptions(); - this.connectionOptions.setServerURIs(new String[]{ url }); + this.connectionOptions.setServerURIs(new String[] {url}); this.connectionOptions.setAutomaticReconnect(true); } From d9a04741b1ee8567dd540e0d4f37509511318ef2 Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Wed, 27 Jul 2022 16:24:51 +0300 Subject: [PATCH 2/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Add a new interface `ClientManager` which will manage clients and connections. Add different implementations for v3 and v5 MQTT clients. Use this manager in v3/v5 topic adapters and message handlers. --- .../mqtt/core/AbstractMqttClientManager.java | 59 ++++++++ .../integration/mqtt/core/ClientManager.java | 6 +- .../mqtt/core/Mqttv3ClientManager.java | 129 +++++++++------- .../mqtt/core/Mqttv5ClientManager.java | 139 ++++++++++++++++++ ...stractMqttMessageDrivenChannelAdapter.java | 2 +- .../MqttPahoMessageDrivenChannelAdapter.java | 46 +++--- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 101 +++++++++---- .../outbound/AbstractMqttMessageHandler.java | 18 ++- .../mqtt/outbound/MqttPahoMessageHandler.java | 10 +- .../outbound/Mqttv5PahoMessageHandler.java | 37 +++-- .../integration/mqtt/MqttAdapterTests.java | 74 +++++----- 11 files changed, 460 insertions(+), 161 deletions(-) create mode 100644 spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java create mode 100644 spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java new file mode 100644 index 00000000000..9d1e715096d --- /dev/null +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.mqtt.core; + +import org.springframework.util.Assert; + +public abstract class AbstractMqttClientManager implements ClientManager { + + private boolean manualAcks; + + private String url; + + private String clientId; + + AbstractMqttClientManager(String url, String clientId) { + Assert.notNull(clientId, "'clientId' is required"); + this.clientId = clientId; + this.url = url; + } + + @Override + public boolean isManualAcks() { + return this.manualAcks; + } + + public void setManualAcks(boolean manualAcks) { + this.manualAcks = manualAcks; + } + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } +} diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java index e09d7a1532d..f4706b49eeb 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java @@ -16,10 +16,12 @@ package org.springframework.integration.mqtt.core; -import org.springframework.integration.support.management.ManageableLifecycle; +import org.springframework.context.SmartLifecycle; -public interface ClientManager extends ManageableLifecycle { +public interface ClientManager extends SmartLifecycle { T getClient(); + boolean isManualAcks(); + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index fbaadc7c74c..38e390a1fa1 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -18,128 +18,153 @@ import java.time.Instant; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.springframework.integration.context.IntegrationObjectSupport; -import org.springframework.integration.support.management.ManageableLifecycle; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.Assert; -public class Mqttv3ClientManager extends IntegrationObjectSupport implements ClientManager, - ManageableLifecycle, MqttCallback { +public class Mqttv3ClientManager extends AbstractMqttClientManager implements MqttCallback { - private AtomicReference> scheduledReconnect; + /** + * The default reconnect timeout in millis. + */ + private static final long DEFAULT_RECOVERY_INTERVAL = 10_000; - private final MqttConnectOptions connectOptions; + private final Log logger = LogFactory.getLog(this.getClass()); - private final String clientId; + private final MqttPahoClientFactory clientFactory; - private IMqttAsyncClient client; + private final TaskScheduler taskScheduler; - public Mqttv3ClientManager(MqttConnectOptions connectOptions, String clientId) throws MqttException { - this.connectOptions = connectOptions; - this.client = new MqttAsyncClient(connectOptions.getServerURIs()[0], clientId); - this.client.setCallback(this); - this.clientId = clientId; + private volatile ScheduledFuture scheduledReconnect; + + private volatile IMqttAsyncClient client; + + private long recoveryInterval = DEFAULT_RECOVERY_INTERVAL; + + public Mqttv3ClientManager(MqttPahoClientFactory clientFactory, TaskScheduler taskScheduler, String url, + String clientId) { + + super(url, clientId); + Assert.notNull(clientId, "'clientFactory' is required"); + Assert.notNull(clientId, "'taskScheduler' is required"); + if (url == null) { + Assert.notEmpty(clientFactory.getConnectionOptions().getServerURIs(), "'serverURIs' must be provided in the 'MqttConnectionOptions'"); + } + this.clientFactory = clientFactory; + this.taskScheduler = taskScheduler; } @Override public IMqttAsyncClient getClient() { - return client; + return this.client; } @Override - public void start() { + public synchronized void start() { if (this.client == null) { try { - this.client = new MqttAsyncClient(this.connectOptions.getServerURIs()[0], this.clientId); + this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); + this.client.setManualAcks(isManualAcks()); + this.client.setCallback(this); } catch (MqttException e) { throw new IllegalStateException("could not start client manager", e); } - this.client.setCallback(this); } try { connect(); } catch (MqttException e) { - logger.error(e, "could not start client manager, scheduling reconnect, client_id=" + - this.client.getClientId()); + this.logger.error("could not start client manager, scheduling reconnect, client_id=" + + this.client.getClientId(), e); scheduleReconnect(); } } @Override - public void stop() { + public synchronized void stop() { if (this.client == null) { return; } try { - this.client.disconnectForcibly(this.connectOptions.getConnectionTimeout()); + this.client.disconnectForcibly(this.clientFactory.getConnectionOptions().getConnectionTimeout()); } catch (MqttException e) { - logger.error(e, "could not disconnect from the client"); + this.logger.error("could not disconnect from the client", e); } finally { try { this.client.close(); } catch (MqttException e) { - logger.error(e, "could not close the client"); + this.logger.error("could not close the client", e); } this.client = null; } } @Override - public boolean isRunning() { + public synchronized boolean isRunning() { return this.client != null; } - private synchronized void connect() throws MqttException { - if (this.client == null) { - logger.error("could not connect on a null client reference"); - return; - } - MqttConnectOptions options = Mqttv3ClientManager.this.connectOptions; - this.client.connect(options).waitForCompletion(options.getConnectionTimeout()); + @Override + public synchronized void connectionLost(Throwable cause) { + this.logger.error("connection lost, scheduling reconnect, client_id=" + this.client.getClientId(), + cause); + scheduleReconnect(); // todo: do we need to resubscribe if con lost? } @Override - public synchronized void connectionLost(Throwable cause) { - logger.error(cause, "connection lost, scheduling reconnect, client_id=" + this.client.getClientId()); - scheduleReconnect(); + public void messageArrived(String topic, MqttMessage message) { + // not this manager concern + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + // nor this manager concern + } + + public long getRecoveryInterval() { + return this.recoveryInterval; + } + + public void setRecoveryInterval(long recoveryInterval) { + this.recoveryInterval = recoveryInterval; } - private void scheduleReconnect() { - if (this.scheduledReconnect.get() != null) { - this.scheduledReconnect.get().cancel(false); + private synchronized void connect() throws MqttException { + MqttConnectOptions options = this.clientFactory.getConnectionOptions(); + this.client.connect(options).waitForCompletion(options.getConnectionTimeout()); + } + + private synchronized void scheduleReconnect() { + if (this.scheduledReconnect != null) { + this.scheduledReconnect.cancel(false); } - this.scheduledReconnect.set(getTaskScheduler().schedule(() -> { + this.scheduledReconnect = this.taskScheduler.schedule(() -> { try { + if (this.client.isConnected()) { + return; + } + connect(); - this.scheduledReconnect.set(null); + this.scheduledReconnect = null; } catch (MqttException e) { - logger.error(e, "could not reconnect"); + this.logger.error("could not reconnect", e); scheduleReconnect(); } - }, Instant.now().plusSeconds(10))); + }, Instant.now().plusMillis(getRecoveryInterval())); } - @Override - public void messageArrived(String topic, MqttMessage message) { - // not this manager concern - } - - @Override - public void deliveryComplete(IMqttDeliveryToken token) { - // nor this manager concern - } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java new file mode 100644 index 00000000000..935b6d03cc2 --- /dev/null +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java @@ -0,0 +1,139 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.mqtt.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.paho.mqttv5.client.IMqttAsyncClient; +import org.eclipse.paho.mqttv5.client.IMqttToken; +import org.eclipse.paho.mqttv5.client.MqttAsyncClient; +import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; + +import org.springframework.util.Assert; + +public class Mqttv5ClientManager extends AbstractMqttClientManager implements MqttCallback { + + private final Log logger = LogFactory.getLog(this.getClass()); + + private final MqttConnectionOptions connectionOptions; + + private volatile IMqttAsyncClient client; + + public Mqttv5ClientManager(MqttConnectionOptions connectionOptions, String url, String clientId) { + super(url, clientId); + Assert.notNull(connectionOptions, "'connectionOptions' is required"); + if (url == null) { + Assert.notEmpty(connectionOptions.getServerURIs(), "'serverURIs' must be provided in the 'MqttConnectionOptions'"); + } + this.connectionOptions = connectionOptions; + } + + @Override + public IMqttAsyncClient getClient() { + return this.client; + } + + @Override + public synchronized void start() { + if (this.client == null) { + try { + this.client = new MqttAsyncClient(getUrl(), getClientId()); + this.client.setManualAcks(isManualAcks()); + this.client.setCallback(this); + } + catch (MqttException e) { + throw new IllegalStateException("could not start client manager", e); + } + } + try { + this.client.connect(this.connectionOptions) + .waitForCompletion(this.connectionOptions.getConnectionTimeout()); + } + catch (MqttException e) { + this.logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); + } + } + + @Override + public synchronized void stop() { + if (this.client == null) { + return; + } + + try { + this.client.disconnectForcibly(this.connectionOptions.getConnectionTimeout()); + } + catch (MqttException e) { + this.logger.error("could not disconnect from the client", e); + } + finally { + try { + this.client.close(); + } + catch (MqttException e) { + this.logger.error("could not close the client", e); + } + this.client = null; + } + } + + @Override + public synchronized boolean isRunning() { + return this.client != null; + } + + @Override + public void messageArrived(String topic, MqttMessage message) { + // not this manager concern + } + + @Override + public void deliveryComplete(IMqttToken token) { + // not this manager concern + } + + @Override + public void connectComplete(boolean reconnect, String serverURI) { + if (this.logger.isInfoEnabled()) { + this.logger.info("MQTT connect complete to " + serverURI); + } + // probably makes sense to use custom callbacks in the future + } + + @Override + public void authPacketArrived(int reasonCode, MqttProperties properties) { + // not this manager concern + } + + @Override + public void disconnected(MqttDisconnectResponse disconnectResponse) { + if (this.logger.isInfoEnabled()) { + this.logger.info("MQTT disconnected" + disconnectResponse); + } + } + + @Override + public void mqttErrorOccurred(MqttException exception) { + this.logger.error("MQTT error occurred", exception); + } + +} diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java index a3227e4c568..75d4b9188bd 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java @@ -195,7 +195,7 @@ public void setManualAcks(boolean manualAcks) { } protected boolean isManualAcks() { - return this.manualAcks; + return this.clientManager == null ? this.manualAcks : this.clientManager.isManualAcks(); } /** diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index 5f33cdc8a4b..f5320d5a1e3 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -157,7 +157,7 @@ public MqttConnectOptions getConnectionInfo() { String url = getUrl(); if (url != null) { options = MqttUtils.cloneConnectOptions(options); - options.setServerURIs(new String[] {url}); + options.setServerURIs(new String[]{ url }); } } return options; @@ -236,6 +236,11 @@ public void addTopic(String topic, int qos) { if (this.client != null && this.client.isConnected()) { this.client.subscribe(topic, qos); } + var theClientManager = getClientManager(); + if (theClientManager != null) { + theClientManager.getClient().subscribe(topic, qos, new MessageListener()) + .waitForCompletion(getCompletionTimeout()); + } } catch (MqttException e) { super.removeTopic(topic); @@ -253,6 +258,10 @@ public void removeTopic(String... topic) { if (this.client != null && this.client.isConnected()) { this.client.unsubscribe(topic); } + var theClientManager = getClientManager(); + if (theClientManager != null) { + theClientManager.getClient().unsubscribe(topic).waitForCompletion(getCompletionTimeout()); + } super.removeTopic(topic); } catch (MqttException e) { @@ -289,16 +298,18 @@ private synchronized void connectAndSubscribe() throws MqttException { // NOSONA ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); try { long completionTimeout = getCompletionTimeout(); - if (!clientInstance.isConnected()) { + if (getClientManager() == null) { clientInstance.connect(connectionOptions).waitForCompletion(completionTimeout); + clientInstance.setManualAcks(isManualAcks()); } - clientInstance.setManualAcks(isManualAcks()); if (topics.length > 0) { int[] requestedQos = getQos(); MessageListener[] listeners = Stream.of(topics) - .map(t -> new MessageListener(client)) + .map(t -> new MessageListener()) .toArray(MessageListener[]::new); - IMqttToken subscribeToken = clientInstance.subscribe(topics, requestedQos, listeners); + IMqttToken subscribeToken = getClientManager() == null ? + clientInstance.subscribe(topics, requestedQos) : + clientInstance.subscribe(topics, requestedQos, listeners); subscribeToken.waitForCompletion(completionTimeout); int[] grantedQos = subscribeToken.getGrantedQos(); if (grantedQos.length == 1 && grantedQos[0] == 0x80) { @@ -358,6 +369,10 @@ private synchronized void cancelReconnect() { } private synchronized void scheduleReconnect() { + if (getClientManager() != null) { + return; + } + cancelReconnect(); if (isActive()) { try { @@ -412,8 +427,9 @@ public void messageArrived(String topic, MqttMessage mqttMessage) { AbstractIntegrationMessageBuilder builder = toMessageBuilder(topic, mqttMessage); if (builder != null) { if (isManualAcks()) { + var theClient = this.client != null ? this.client : getClientManager().getClient(); builder.setHeader(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, - new AcknowledgmentImpl(mqttMessage.getId(), mqttMessage.getQos(), this.client)); + new AcknowledgmentImpl(mqttMessage.getId(), mqttMessage.getQos(), theClient)); } Message message = builder.build(); try { @@ -503,29 +519,13 @@ public void acknowledge() { } - private class MessageListener implements IMqttMessageListener, MqttCallback { - - private final IMqttAsyncClient client; - - MessageListener(IMqttAsyncClient client) { - this.client = client; - } + private class MessageListener implements IMqttMessageListener { @Override public void messageArrived(String topic, MqttMessage mqttMessage) { MqttPahoMessageDrivenChannelAdapter.this.messageArrived(topic, mqttMessage); } - @Override - public void connectionLost(Throwable cause) { - // not this component concern - } - - @Override - public void deliveryComplete(IMqttDeliveryToken token) { - // not this component concern - } - } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 440860cdb1a..5d379146d11 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -18,8 +18,10 @@ import java.util.Arrays; import java.util.Map; +import java.util.stream.IntStream; import org.eclipse.paho.mqttv5.client.IMqttAsyncClient; +import org.eclipse.paho.mqttv5.client.IMqttMessageListener; import org.eclipse.paho.mqttv5.client.IMqttToken; import org.eclipse.paho.mqttv5.client.MqttAsyncClient; import org.eclipse.paho.mqttv5.client.MqttCallback; @@ -28,6 +30,7 @@ import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; import org.eclipse.paho.mqttv5.common.MqttException; import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttSubscription; import org.eclipse.paho.mqttv5.common.packet.MqttProperties; import org.springframework.beans.factory.BeanCreationException; @@ -90,7 +93,7 @@ public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDr public Mqttv5PahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) { super(url, clientId, topic); this.connectionOptions = new MqttConnectionOptions(); - this.connectionOptions.setServerURIs(new String[] {url}); + this.connectionOptions.setServerURIs(new String[]{ url }); this.connectionOptions.setAutomaticReconnect(true); } @@ -143,7 +146,7 @@ public void setHeaderMapper(HeaderMapper headerMapper) { @Override protected void onInit() { super.onInit(); - if (this.mqttClient == null) { + if (getClientManager() == null && this.mqttClient == null) { try { this.mqttClient = new MqttAsyncClient(getUrl(), getClientId(), this.persistence); this.mqttClient.setCallback(this); @@ -162,6 +165,11 @@ protected void onInit() { @Override protected void doStart() { + if (getClientManager() != null) { + subscribeToAll(); + return; + } + ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); try { this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); @@ -189,6 +197,10 @@ protected void doStop() { this.topicLock.lock(); String[] topics = getTopic(); try { + var theClientManager = getClientManager(); + if (theClientManager != null) { + theClientManager.getClient().unsubscribe(topics).waitForCompletion(getCompletionTimeout()); + } if (this.mqttClient != null && this.mqttClient.isConnected()) { this.mqttClient.unsubscribe(topics).waitForCompletion(getCompletionTimeout()); this.mqttClient.disconnect().waitForCompletion(getCompletionTimeout()); @@ -223,6 +235,11 @@ public void addTopic(String topic, int qos) { if (this.mqttClient != null && this.mqttClient.isConnected()) { this.mqttClient.subscribe(topic, qos).waitForCompletion(getCompletionTimeout()); } + var theClientManager = getClientManager(); + if (theClientManager != null) { + theClientManager.getClient().subscribe(new MqttSubscription(topic, qos), new MessageListener()) + .waitForCompletion(getCompletionTimeout()); + } } catch (MqttException ex) { throw new MessagingException("Failed to subscribe to topic " + topic, ex); @@ -239,6 +256,10 @@ public void removeTopic(String... topic) { if (this.mqttClient != null && this.mqttClient.isConnected()) { this.mqttClient.unsubscribe(topic).waitForCompletion(getCompletionTimeout()); } + var theClientManager = getClientManager(); + if (theClientManager != null) { + theClientManager.getClient().unsubscribe(topic).waitForCompletion(getCompletionTimeout()); + } super.removeTopic(topic); } catch (MqttException ex) { @@ -259,8 +280,9 @@ public void messageArrived(String topic, MqttMessage mqttMessage) { headers.put(MqttHeaders.RECEIVED_TOPIC, topic); if (isManualAcks()) { + var client = this.mqttClient != null ? this.mqttClient : getClientManager().getClient(); headers.put(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, - new AcknowledgmentImpl(mqttMessage.getId(), mqttMessage.getQos(), this.mqttClient)); + new AcknowledgmentImpl(mqttMessage.getId(), mqttMessage.getQos(), client)); } Object payload = @@ -309,31 +331,11 @@ public void deliveryComplete(IMqttToken token) { @Override public void connectComplete(boolean reconnect, String serverURI) { - if (!reconnect) { - ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); - String[] topics = getTopic(); - this.topicLock.lock(); - try { - if (topics.length > 0) { - int[] requestedQos = getQos(); - this.mqttClient.subscribe(topics, requestedQos).waitForCompletion(getCompletionTimeout()); - String message = "Connected and subscribed to " + Arrays.toString(topics); - logger.debug(message); - if (applicationEventPublisher != null) { - applicationEventPublisher.publishEvent(new MqttSubscribedEvent(this, message)); - } - } - } - catch (MqttException ex) { - if (applicationEventPublisher != null) { - applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex)); - } - logger.error(ex, () -> "Error subscribing to " + Arrays.toString(topics)); - } - finally { - this.topicLock.unlock(); - } + if (reconnect) { + return; } + + subscribeToAll(); } @Override @@ -341,6 +343,42 @@ public void authPacketArrived(int reasonCode, MqttProperties properties) { } + private void subscribeToAll() { + ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); + String[] topics = getTopic(); + this.topicLock.lock(); + try { + if (topics.length == 0) { + return; + } + + int[] requestedQos = getQos(); + if (this.mqttClient != null) { + this.mqttClient.subscribe(topics, requestedQos).waitForCompletion(getCompletionTimeout()); + } + if (getClientManager() != null) { + MqttSubscription[] subscriptions = IntStream.range(0, topics.length) + .mapToObj(i -> new MqttSubscription(topics[i], requestedQos[i])) + .toArray(MqttSubscription[]::new); + getClientManager().getClient().subscribe(subscriptions, new MessageListener()); + } + String message = "Connected and subscribed to " + Arrays.toString(topics); + logger.debug(message); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttSubscribedEvent(this, message)); + } + } + catch (MqttException ex) { + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex)); + } + logger.error(ex, () -> "Error subscribing to " + Arrays.toString(topics)); + } + finally { + this.topicLock.unlock(); + } + } + private static String obtainServerUrlFromOptions(MqttConnectionOptions connectionOptions) { Assert.notNull(connectionOptions, "'connectionOptions' must not be null"); String[] serverURIs = connectionOptions.getServerURIs(); @@ -384,4 +422,13 @@ public void acknowledge() { } + private class MessageListener implements IMqttMessageListener { + + @Override + public void messageArrived(String topic, MqttMessage message) { + Mqttv5PahoMessageDrivenChannelAdapter.this.messageArrived(topic, message); + } + + } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java index a87077cd6fb..3da372f4a49 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor; import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.mqtt.core.ClientManager; import org.springframework.integration.mqtt.support.MqttHeaders; import org.springframework.integration.mqtt.support.MqttMessageConverter; import org.springframework.integration.support.management.ManageableLifecycle; @@ -36,13 +37,15 @@ /** * Abstract class for MQTT outbound channel adapters. * + * @param MQTT Client type + * * @author Gary Russell * @author Artem Bilan * * @since 4.0 * */ -public abstract class AbstractMqttMessageHandler extends AbstractMessageHandler +public abstract class AbstractMqttMessageHandler extends AbstractMessageHandler implements ManageableLifecycle, ApplicationEventPublisherAware { /** @@ -86,6 +89,8 @@ public abstract class AbstractMqttMessageHandler extends AbstractMessageHandler private int clientInstance; + protected ClientManager clientManager; + public AbstractMqttMessageHandler(@Nullable String url, String clientId) { Assert.hasText(clientId, "'clientId' cannot be null or empty"); this.url = url; @@ -292,6 +297,15 @@ protected long getDisconnectCompletionTimeout() { return this.disconnectCompletionTimeout; } + protected ClientManager getClientManager() { + return this.clientManager; + } + + public void setClientManager(ClientManager clientManager) { + Assert.notNull(clientManager, "'clientManager' cannot be null"); + this.clientManager = clientManager; + } + @Override protected void onInit() { super.onInit(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java index aa00aca70b4..8c10a744d34 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,8 @@ * @since 4.0 * */ -public class MqttPahoMessageHandler extends AbstractMqttMessageHandler implements MqttCallback, MqttPahoComponent { +public class MqttPahoMessageHandler extends AbstractMqttMessageHandler + implements MqttCallback, MqttPahoComponent { private final MqttPahoClientFactory clientFactory; @@ -169,6 +170,11 @@ protected void doStop() { } private synchronized IMqttAsyncClient checkConnection() throws MqttException { + var theClientManager = getClientManager(); + if (theClientManager != null) { + return theClientManager.getClient(); + } + if (this.client != null && !this.client.isConnected()) { this.client.setCallback(null); this.client.close(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index 46908724b60..486361105f4 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -55,7 +55,7 @@ * * @since 5.5.5 */ -public class Mqttv5PahoMessageHandler extends AbstractMqttMessageHandler +public class Mqttv5PahoMessageHandler extends AbstractMqttMessageHandler implements MqttCallback, MqttComponent { private final MqttConnectionOptions connectionOptions; @@ -131,9 +131,11 @@ public void setAsyncEvents(boolean asyncEvents) { protected void onInit() { super.onInit(); try { - this.mqttClient = new MqttAsyncClient(getUrl(), getClientId(), this.persistence); - this.mqttClient.setCallback(this); - incrementClientInstance(); + if (getClientManager() == null) { + this.mqttClient = new MqttAsyncClient(getUrl(), getClientId(), this.persistence); + this.mqttClient.setCallback(this); + incrementClientInstance(); + } } catch (MqttException ex) { throw new BeanCreationException("Cannot create 'MqttAsyncClient' for: " + getComponentName(), ex); @@ -152,17 +154,21 @@ protected void onInit() { @Override protected void doStart() { try { - this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); + if (this.mqttClient != null) { + this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); + } } catch (MqttException ex) { - logger.error(ex, "MQTT client failed to connect."); + logger.error(ex, "MQTT client failed to connect."); } } @Override protected void doStop() { try { - this.mqttClient.disconnect().waitForCompletion(getDisconnectCompletionTimeout()); + if (this.mqttClient != null) { + this.mqttClient.disconnect().waitForCompletion(getDisconnectCompletionTimeout()); + } } catch (MqttException ex) { logger.error(ex, "Failed to disconnect 'MqttAsyncClient'"); @@ -173,7 +179,9 @@ protected void doStop() { public void destroy() { super.destroy(); try { - this.mqttClient.close(true); + if (this.mqttClient != null) { + this.mqttClient.close(true); + } } catch (MqttException ex) { logger.error(ex, "Failed to close 'MqttAsyncClient'"); @@ -237,10 +245,17 @@ protected void publish(String topic, Object mqttMessage, Message message) { Assert.isInstanceOf(MqttMessage.class, mqttMessage, "The 'mqttMessage' must be an instance of 'MqttMessage'"); long completionTimeout = getCompletionTimeout(); try { - if (!this.mqttClient.isConnected()) { - this.mqttClient.connect(this.connectionOptions).waitForCompletion(completionTimeout); + IMqttAsyncClient theClient; + if (getClientManager() != null) { + theClient = getClientManager().getClient(); + } + else { + if (!this.mqttClient.isConnected()) { + this.mqttClient.connect(this.connectionOptions).waitForCompletion(completionTimeout); + } + theClient = this.mqttClient; } - IMqttToken token = this.mqttClient.publish(topic, (MqttMessage) mqttMessage); + IMqttToken token = theClient.publish(topic, (MqttMessage) mqttMessage); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); if (!this.async) { token.waitForCompletion(completionTimeout); // NOSONAR (sync) diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java index a8c742017a7..69d5d323d14 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willReturn; @@ -53,11 +52,9 @@ import org.aopalliance.intercept.MethodInterceptor; import org.assertj.core.api.Condition; import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; -import org.eclipse.paho.client.mqttv3.IMqttClient; import org.eclipse.paho.client.mqttv3.IMqttToken; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttException; @@ -67,7 +64,6 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; -import org.mockito.internal.stubbing.answers.CallsRealMethods; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.beans.DirectFieldAccessor; @@ -122,9 +118,9 @@ public class MqttAdapterTests { @Test public void testCloseOnBadConnectIn() throws Exception { - final IMqttClient client = mock(IMqttClient.class); - willThrow(new MqttException(0)).given(client).connect(any()); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); + willThrow(new MqttException(0)).given(client).connect(any()); adapter.start(); verify(client).close(); adapter.stop(); @@ -133,8 +129,8 @@ public void testCloseOnBadConnectIn() throws Exception { @Test public void testCloseOnBadConnectOut() throws Exception { final IMqttAsyncClient client = mock(IMqttAsyncClient.class); - willThrow(new MqttException(0)).given(client).connect(any()); MqttPahoMessageHandler adapter = buildAdapterOut(client); + willThrow(new MqttException(0)).given(client).connect(any()); adapter.start(); try { adapter.handleMessage(new GenericMessage<>("foo")); @@ -233,8 +229,8 @@ public void testInboundOptionsApplied() throws Exception { factory.setConnectionOptions(connectOptions); factory = spy(factory); - final IMqttClient client = mock(IMqttClient.class); - willAnswer(invocation -> client).given(factory).getClientInstance(anyString(), anyString()); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); + willReturn(client).given(factory).getAsyncClientInstance(anyString(), anyString()); final AtomicBoolean connectCalled = new AtomicBoolean(); final AtomicBoolean failConnection = new AtomicBoolean(); @@ -242,6 +238,7 @@ public void testInboundOptionsApplied() throws Exception { final CountDownLatch failInProcess = new CountDownLatch(1); final CountDownLatch goodConnection = new CountDownLatch(2); final MqttException reconnectException = new MqttException(MqttException.REASON_CODE_SERVER_CONNECT_ERROR); + IMqttToken token = mock(IMqttToken.class); willAnswer(invocation -> { if (failConnection.get()) { failInProcess.countDown(); @@ -260,8 +257,10 @@ public void testInboundOptionsApplied() throws Exception { assertThat(options.getWillMessage().getQos()).isEqualTo(2); connectCalled.set(true); goodConnection.countDown(); - return null; + return token; }).given(client).connect(any(MqttConnectOptions.class)); + given(client.subscribe(any(String[].class), any(int[].class))).willReturn(token); + given(token.getGrantedQos()).willReturn(new int[]{ 2 }); final AtomicReference callback = new AtomicReference<>(); willAnswer(invocation -> { @@ -369,7 +368,7 @@ public Message toMessage(Object payload, MessageHeaders headers) { @Test public void testStopActionDefault() throws Exception { - final IMqttClient client = mock(IMqttClient.class); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, null); adapter.start(); @@ -379,7 +378,7 @@ public void testStopActionDefault() throws Exception { @Test public void testStopActionDefaultNotClean() throws Exception { - final IMqttClient client = mock(IMqttClient.class); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, false, null); adapter.start(); @@ -389,7 +388,7 @@ public void testStopActionDefaultNotClean() throws Exception { @Test public void testStopActionAlways() throws Exception { - final IMqttClient client = mock(IMqttClient.class); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, false, ConsumerStopAction.UNSUBSCRIBE_ALWAYS); @@ -407,7 +406,7 @@ public void testStopActionAlways() throws Exception { @Test public void testStopActionNever() throws Exception { - final IMqttClient client = mock(IMqttClient.class); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); adapter.start(); @@ -438,7 +437,7 @@ public void testCustomExpressions() { @Test public void testReconnect() throws Exception { - final IMqttClient client = mock(IMqttClient.class); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); adapter.setRecoveryInterval(10); LogAccessor logger = spy(TestUtils.getPropertyValue(adapter, "logger", LogAccessor.class)); @@ -487,19 +486,14 @@ public void testSubscribeFailure() throws Exception { connectOptions.setWill("foo", "bar".getBytes(), 2, true); factory = spy(factory); - MqttAsyncClient aClient = mock(MqttAsyncClient.class); - final MqttClient client = mock(MqttClient.class); - willAnswer(invocation -> client).given(factory).getClientInstance(anyString(), anyString()); + final MqttAsyncClient client = mock(MqttAsyncClient.class); + willReturn(client).given(factory).getAsyncClientInstance(anyString(), anyString()); given(client.isConnected()).willReturn(true); - new DirectFieldAccessor(client).setPropertyValue("aClient", aClient); - willAnswer(new CallsRealMethods()).given(client).connect(any(MqttConnectOptions.class)); - willAnswer(new CallsRealMethods()).given(client).subscribe(any(String[].class), any(int[].class)); - willAnswer(new CallsRealMethods()).given(client).subscribe(any(String[].class), any(int[].class), isNull()); - willReturn(alwaysComplete).given(aClient).connect(any(MqttConnectOptions.class), any(), any()); + willReturn(alwaysComplete).given(client).connect(any(MqttConnectOptions.class)); IMqttToken token = mock(IMqttToken.class); given(token.getGrantedQos()).willReturn(new int[]{ 0x80 }); - willReturn(token).given(aClient).subscribe(any(String[].class), any(int[].class), isNull(), isNull(), any()); + willReturn(token).given(client).subscribe(any(String[].class), any(int[].class)); MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", factory, "baz", "fix"); @@ -535,19 +529,14 @@ public void testDifferentQos() throws Exception { connectOptions.setWill("foo", "bar".getBytes(), 2, true); factory = spy(factory); - MqttAsyncClient aClient = mock(MqttAsyncClient.class); - final MqttClient client = mock(MqttClient.class); - willAnswer(invocation -> client).given(factory).getClientInstance(anyString(), anyString()); + final MqttAsyncClient client = mock(MqttAsyncClient.class); + willReturn(client).given(factory).getAsyncClientInstance(anyString(), anyString()); given(client.isConnected()).willReturn(true); - new DirectFieldAccessor(client).setPropertyValue("aClient", aClient); - willAnswer(new CallsRealMethods()).given(client).connect(any(MqttConnectOptions.class)); - willAnswer(new CallsRealMethods()).given(client).subscribe(any(String[].class), any(int[].class)); - willAnswer(new CallsRealMethods()).given(client).subscribe(any(String[].class), any(int[].class), isNull()); - willReturn(alwaysComplete).given(aClient).connect(any(MqttConnectOptions.class), any(), any()); + willReturn(alwaysComplete).given(client).connect(any(MqttConnectOptions.class)); IMqttToken token = mock(IMqttToken.class); given(token.getGrantedQos()).willReturn(new int[]{ 2, 0 }); - willReturn(token).given(aClient).subscribe(any(String[].class), any(int[].class), isNull(), isNull(), any()); + willReturn(token).given(client).subscribe(any(String[].class), any(int[].class)); MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", factory, "baz", "fix"); @@ -566,7 +555,6 @@ public void testDifferentQos() throws Exception { logMessage.get() .equals("Granted QOS different to Requested QOS; topics: [baz, fix] " + "requested: [1, 1] granted: [2, 0]"))); - verify(client).setTimeToWait(30_000L); new DirectFieldAccessor(adapter).setPropertyValue("running", Boolean.TRUE); adapter.stop(); @@ -575,7 +563,7 @@ public void testDifferentQos() throws Exception { @Test public void testNoNPEOnReconnectAndStopRaceCondition() throws Exception { - final IMqttClient client = mock(IMqttClient.class); + final IMqttAsyncClient client = mock(IMqttAsyncClient.class); MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); adapter.setRecoveryInterval(10); @@ -583,7 +571,7 @@ public void testNoNPEOnReconnectAndStopRaceCondition() throws Exception { willThrow(mqttException) .given(client) - .subscribe(any(), ArgumentMatchers.any()); + .subscribe(any(), ArgumentMatchers.any()); LogAccessor logger = spy(TestUtils.getPropertyValue(adapter, "logger", LogAccessor.class)); new DirectFieldAccessor(adapter).setPropertyValue("logger", logger); @@ -615,13 +603,13 @@ public void testNoNPEOnReconnectAndStopRaceCondition() throws Exception { taskScheduler.destroy(); } - private MqttPahoMessageDrivenChannelAdapter buildAdapterIn(final IMqttClient client, Boolean cleanSession, - ConsumerStopAction action) { + private MqttPahoMessageDrivenChannelAdapter buildAdapterIn(final IMqttAsyncClient client, Boolean cleanSession, + ConsumerStopAction action) throws MqttException { DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory() { @Override - public IMqttClient getClientInstance(String uri, String clientId) throws MqttException { + public IMqttAsyncClient getAsyncClientInstance(String uri, String clientId) { return client; } @@ -636,6 +624,10 @@ public IMqttClient getClientInstance(String uri, String clientId) throws MqttExc } factory.setConnectionOptions(connectOptions); given(client.isConnected()).willReturn(true); + IMqttToken token = mock(IMqttToken.class); + given(client.connect(any(MqttConnectOptions.class))).willReturn(token); + given(client.subscribe(any(String[].class), any(int[].class))).willReturn(token); + given(token.getGrantedQos()).willReturn(new int[]{ 2 }); MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("client", factory, "foo"); adapter.setApplicationEventPublisher(mock(ApplicationEventPublisher.class)); adapter.setOutputChannel(new NullChannel()); @@ -663,14 +655,14 @@ public IMqttAsyncClient getAsyncClientInstance(String uri, String clientId) { return adapter; } - private void verifyUnsubscribe(IMqttClient client) throws Exception { + private void verifyUnsubscribe(IMqttAsyncClient client) throws Exception { verify(client).connect(any(MqttConnectOptions.class)); verify(client).subscribe(any(String[].class), any(int[].class)); verify(client).unsubscribe(any(String[].class)); verify(client).disconnectForcibly(anyLong()); } - private void verifyNotUnsubscribe(IMqttClient client) throws Exception { + private void verifyNotUnsubscribe(IMqttAsyncClient client) throws Exception { verify(client).connect(any(MqttConnectOptions.class)); verify(client).subscribe(any(String[].class), any(int[].class)); verify(client, never()).unsubscribe(any(String[].class)); From 9f47cc8c7c3c51021460d55ffcdee2da62c8f164 Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Thu, 28 Jul 2022 16:57:30 +0300 Subject: [PATCH 3/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Add a couple of unit/integration tests to cover client manager usage. Several small code improvements after the code review: * Improve client manager usage via providing several mutual exclusive constructors, whether the users provides `url` or `connectionOptions` or `clientFactory` for v3. * Move the logger to `AbstractMqttClientManager` * Do not inject TaskScheduler in constructor for v3 client manager but use lazy init via `BeanFactory` and `IntegrationContextUtils` * Other smaller code readability improvements --- .../mqtt/core/AbstractMqttClientManager.java | 13 +- .../mqtt/core/Mqttv3ClientManager.java | 65 ++++--- .../mqtt/core/Mqttv5ClientManager.java | 42 +++-- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 6 +- .../mqtt/ClientManagerBackToBackTests.java | 158 ++++++++++++++++++ .../integration/mqtt/MqttAdapterTests.java | 69 +++++++- 6 files changed, 303 insertions(+), 50 deletions(-) create mode 100644 spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index 9d1e715096d..cd8ab00878e 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -16,20 +16,24 @@ package org.springframework.integration.mqtt.core; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.util.Assert; public abstract class AbstractMqttClientManager implements ClientManager { + protected final Log logger = LogFactory.getLog(this.getClass()); + private boolean manualAcks; private String url; - private String clientId; + private final String clientId; - AbstractMqttClientManager(String url, String clientId) { + AbstractMqttClientManager(String clientId) { Assert.notNull(clientId, "'clientId' is required"); this.clientId = clientId; - this.url = url; } @Override @@ -53,7 +57,4 @@ public String getClientId() { return this.clientId; } - public void setClientId(String clientId) { - this.clientId = clientId; - } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index 38e390a1fa1..1d93a2a2053 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -19,8 +19,6 @@ import java.time.Instant; import java.util.concurrent.ScheduledFuture; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; @@ -28,21 +26,26 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; -public class Mqttv3ClientManager extends AbstractMqttClientManager implements MqttCallback { +public class Mqttv3ClientManager extends AbstractMqttClientManager + implements MqttCallback, InitializingBean, BeanFactoryAware { /** * The default reconnect timeout in millis. */ private static final long DEFAULT_RECOVERY_INTERVAL = 10_000; - private final Log logger = LogFactory.getLog(this.getClass()); - private final MqttPahoClientFactory clientFactory; - private final TaskScheduler taskScheduler; + private BeanFactory beanFactory; + + private TaskScheduler taskScheduler; private volatile ScheduledFuture scheduledReconnect; @@ -50,17 +53,24 @@ public class Mqttv3ClientManager extends AbstractMqttClientManager implements MqttCallback { - private final Log logger = LogFactory.getLog(this.getClass()); - private final MqttConnectionOptions connectionOptions; private volatile IMqttAsyncClient client; - public Mqttv5ClientManager(MqttConnectionOptions connectionOptions, String url, String clientId) { - super(url, clientId); + public Mqttv5ClientManager(String url, String clientId) { + super(clientId); + Assert.notNull(url, "'url' is required"); + setUrl(url); + this.connectionOptions = new MqttConnectionOptions(); + this.connectionOptions.setServerURIs(new String[]{ url }); + this.connectionOptions.setAutomaticReconnect(true); + } + + public Mqttv5ClientManager(MqttConnectionOptions connectionOptions, String clientId) { + super(clientId); Assert.notNull(connectionOptions, "'connectionOptions' is required"); - if (url == null) { - Assert.notEmpty(connectionOptions.getServerURIs(), "'serverURIs' must be provided in the 'MqttConnectionOptions'"); - } this.connectionOptions = connectionOptions; + if (!this.connectionOptions.isAutomaticReconnect()) { + logger.warn("It is recommended to set 'automaticReconnect' MQTT connection option. " + + "Otherwise connection check and reconnect should be done manually."); + } + Assert.notEmpty(connectionOptions.getServerURIs(), "'serverURIs' must be provided in the 'MqttConnectionOptions'"); + setUrl(connectionOptions.getServerURIs()[0]); } @Override @@ -69,7 +77,7 @@ public synchronized void start() { .waitForCompletion(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { - this.logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); + logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); } } @@ -83,14 +91,14 @@ public synchronized void stop() { this.client.disconnectForcibly(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { - this.logger.error("could not disconnect from the client", e); + logger.error("could not disconnect from the client", e); } finally { try { this.client.close(); } catch (MqttException e) { - this.logger.error("could not close the client", e); + logger.error("could not close the client", e); } this.client = null; } @@ -113,8 +121,8 @@ public void deliveryComplete(IMqttToken token) { @Override public void connectComplete(boolean reconnect, String serverURI) { - if (this.logger.isInfoEnabled()) { - this.logger.info("MQTT connect complete to " + serverURI); + if (logger.isInfoEnabled()) { + logger.info("MQTT connect complete to " + serverURI); } // probably makes sense to use custom callbacks in the future } @@ -126,14 +134,14 @@ public void authPacketArrived(int reasonCode, MqttProperties properties) { @Override public void disconnected(MqttDisconnectResponse disconnectResponse) { - if (this.logger.isInfoEnabled()) { - this.logger.info("MQTT disconnected" + disconnectResponse); + if (logger.isInfoEnabled()) { + logger.info("MQTT disconnected" + disconnectResponse); } } @Override public void mqttErrorOccurred(MqttException exception) { - this.logger.error("MQTT error occurred", exception); + logger.error("MQTT error occurred", exception); } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 5d379146d11..4b0a6dd6401 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -331,11 +331,9 @@ public void deliveryComplete(IMqttToken token) { @Override public void connectComplete(boolean reconnect, String serverURI) { - if (reconnect) { - return; + if (!reconnect) { + subscribeToAll(); } - - subscribeToAll(); } @Override diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java new file mode 100644 index 00000000000..c8ebf1e9835 --- /dev/null +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.mqtt; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; +import org.springframework.integration.mqtt.core.MqttPahoClientFactory; +import org.springframework.integration.mqtt.core.Mqttv3ClientManager; +import org.springframework.integration.mqtt.core.Mqttv5ClientManager; +import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; +import org.springframework.integration.mqtt.inbound.Mqttv5PahoMessageDrivenChannelAdapter; +import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler; +import org.springframework.integration.mqtt.outbound.Mqttv5PahoMessageHandler; +import org.springframework.integration.mqtt.support.MqttHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.PollableChannel; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * @author Artem Vozhdayenko + */ +class ClientManagerBackToBackTests implements MosquittoContainerTest { + + static final String SHARED_CLIENT_ID = "shared-client-id"; + + public static final String TOPIC_NAME = "test-topic"; + + @Test + void testSameV3ClientIdWorksForPubAndSub() { + testSubscribeAndPublish(Mqttv3Config.class); + } + + @Test + void testSameV5ClientIdWorksForPubAndSub() { + testSubscribeAndPublish(Mqttv5Config.class); + } + + private void testSubscribeAndPublish(Class configClass) { + try (var ctx = new AnnotationConfigApplicationContext(configClass)) { + // given + var input = ctx.getBean("mqttOutFlow.input", MessageChannel.class); + var output = ctx.getBean("fromMqttChannel", PollableChannel.class); + String testPayload = "foo"; + + // when + input.send(MessageBuilder.withPayload(testPayload).setHeader(MqttHeaders.TOPIC, TOPIC_NAME).build()); + Message receive = output.receive(10_000); + + // then + assertThat(receive).isNotNull(); + assertThat(receive.getPayload()).isEqualTo(testPayload); + } + } + + @Configuration + @EnableIntegration + public static class Mqttv3Config { + + @Bean + public TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); + } + + @Bean + public Mqttv3ClientManager mqttv3ClientManager(MqttPahoClientFactory pahoClientFactory) { + return new Mqttv3ClientManager(pahoClientFactory, SHARED_CLIENT_ID); + } + + @Bean + public MqttPahoClientFactory pahoClientFactory() { + var pahoClientFactory = new DefaultMqttPahoClientFactory(); + MqttConnectOptions connectionOptions = new MqttConnectOptions(); + connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); + pahoClientFactory.setConnectionOptions(connectionOptions); + return pahoClientFactory; + } + + @Bean + public IntegrationFlow mqttOutFlow(MqttPahoClientFactory pahoClientFactory, + Mqttv3ClientManager mqttv3ClientManager) { + + var mqttHandler = new MqttPahoMessageHandler(SHARED_CLIENT_ID, pahoClientFactory); + mqttHandler.setClientManager(mqttv3ClientManager); + return f -> f.handle(mqttHandler); + } + + @Bean + public IntegrationFlow mqttInFlow(MqttPahoClientFactory pahoClientFactory, + Mqttv3ClientManager mqttv3ClientManager) { + + var mqttAdapter = new MqttPahoMessageDrivenChannelAdapter(SHARED_CLIENT_ID, pahoClientFactory, TOPIC_NAME); + mqttAdapter.setClientManager(mqttv3ClientManager); + return IntegrationFlow.from(mqttAdapter) + .channel(c -> c.queue("fromMqttChannel")) + .get(); + } + + } + + @Configuration + @EnableIntegration + public static class Mqttv5Config { + + @Bean + public TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); + } + + @Bean + public Mqttv5ClientManager mqttv5ClientManager() { + return new Mqttv5ClientManager(MosquittoContainerTest.mqttUrl(), SHARED_CLIENT_ID); + } + + @Bean + public IntegrationFlow mqttOutFlow(Mqttv5ClientManager mqttv5ClientManager) { + var mqttHandler = new Mqttv5PahoMessageHandler(MosquittoContainerTest.mqttUrl(), SHARED_CLIENT_ID); + mqttHandler.setClientManager(mqttv5ClientManager); // todo: add into ctor + return f -> f.handle(mqttHandler); + } + + @Bean + public IntegrationFlow mqttInFlow(Mqttv5ClientManager mqttv5ClientManager) { + var mqttAdapter = new Mqttv5PahoMessageDrivenChannelAdapter(MosquittoContainerTest.mqttUrl(), SHARED_CLIENT_ID, TOPIC_NAME); + mqttAdapter.setClientManager(mqttv5ClientManager); + return IntegrationFlow.from(mqttAdapter) + .channel(c -> c.queue("fromMqttChannel")) + .get(); + } + + } + +} diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java index 69d5d323d14..5bfcd58d61f 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java @@ -79,6 +79,7 @@ import org.springframework.integration.handler.MessageProcessor; import org.springframework.integration.mqtt.core.ConsumerStopAction; import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; +import org.springframework.integration.mqtt.core.Mqttv3ClientManager; import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent; import org.springframework.integration.mqtt.event.MqttIntegrationEvent; import org.springframework.integration.mqtt.event.MqttSubscribedEvent; @@ -210,6 +211,72 @@ public void testOutboundOptionsApplied() throws Exception { handler.stop(); } + @Test + void testClientManagerIsNotConnectedAndClosedInHandler() throws Exception { + // given + var clientManager = mock(Mqttv3ClientManager.class); + var client = mock(MqttAsyncClient.class); + given(clientManager.getClient()).willReturn(client); + + var deliveryToken = mock(MqttDeliveryToken.class); + given(client.publish(anyString(), any(MqttMessage.class))).willReturn(deliveryToken); + + var handler = new MqttPahoMessageHandler("foo", "bar", + new DefaultMqttPahoClientFactory()); + handler.setClientManager(clientManager); + handler.setDefaultTopic("mqtt-foo"); + handler.setBeanFactory(mock(BeanFactory.class)); + handler.afterPropertiesSet(); + handler.start(); + + // when + handler.handleMessage(new GenericMessage<>("Hello, world!")); + handler.connectionLost(new IllegalStateException()); + handler.stop(); + + // then + verify(client, never()).connect(any(MqttConnectOptions.class)); + verify(client).publish(anyString(), any(MqttMessage.class)); + verify(client, never()).disconnect(); + verify(client, never()).disconnect(anyLong()); + verify(client, never()).close(); + } + + @Test + void testClientManagerIsNotConnectedAndClosedInAdapter() throws Exception { + // given + var clientManager = mock(Mqttv3ClientManager.class); + var client = mock(MqttAsyncClient.class); + given(clientManager.getClient()).willReturn(client); + + var subscribeToken = mock(MqttToken.class); + given(subscribeToken.getGrantedQos()).willReturn(new int[]{ 2 }); + given(client.subscribe(any(String[].class), any(int[].class), any())) + .willReturn(subscribeToken); + + var adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", + new DefaultMqttPahoClientFactory(), "mqtt-foo"); + adapter.setClientManager(clientManager); + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.initialize(); + adapter.setTaskScheduler(taskScheduler); + adapter.setBeanFactory(mock(BeanFactory.class)); + adapter.afterPropertiesSet(); + + // when + adapter.start(); + adapter.connectionLost(new IllegalStateException()); + adapter.stop(); + + // then + verify(client, never()).connect(any(MqttConnectOptions.class)); + verify(client).subscribe(eq(new String[]{ "mqtt-foo" }), any(int[].class), any()); + verify(client).unsubscribe(new String[]{ "mqtt-foo" }); + verify(client, never()).disconnect(); + verify(client, never()).disconnect(anyLong()); + verify(client, never()).close(); + } + @Test public void testInboundOptionsApplied() throws Exception { DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); @@ -571,7 +638,7 @@ public void testNoNPEOnReconnectAndStopRaceCondition() throws Exception { willThrow(mqttException) .given(client) - .subscribe(any(), ArgumentMatchers.any()); + .subscribe(any(), any()); LogAccessor logger = spy(TestUtils.getPropertyValue(adapter, "logger", LogAccessor.class)); new DirectFieldAccessor(adapter).setPropertyValue("logger", logger); From d338577ef0021705e18bb740638b814b16ee8f30 Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Fri, 29 Jul 2022 16:13:57 +0300 Subject: [PATCH 4/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Add new tests with reconnect cases. Other code improvements after the code review: * Adjust javadocs according to standards * Remove `setClientManager` and use exclusive ctors * Make automatic reconnects using the v3 client instead of manually using task scheduler --- .../mqtt/core/AbstractMqttClientManager.java | 7 + .../integration/mqtt/core/ClientManager.java | 11 ++ .../mqtt/core/Mqttv3ClientManager.java | 109 +++-------- .../mqtt/core/Mqttv5ClientManager.java | 37 +++- ...stractMqttMessageDrivenChannelAdapter.java | 30 +-- .../MqttPahoMessageDrivenChannelAdapter.java | 32 +++- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 51 ++++- .../outbound/AbstractMqttMessageHandler.java | 15 +- .../mqtt/outbound/MqttPahoMessageHandler.java | 24 +++ .../outbound/Mqttv5PahoMessageHandler.java | 26 ++- .../mqtt/ClientManagerBackToBackTests.java | 181 ++++++++++++++---- .../integration/mqtt/MqttAdapterTests.java | 9 +- 12 files changed, 376 insertions(+), 156 deletions(-) diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index cd8ab00878e..f692339bd16 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -21,6 +21,13 @@ import org.springframework.util.Assert; +/** + * @param MQTT client type + * + * @author Artem Vozhdayenko + * + * @since 6.0 + */ public abstract class AbstractMqttClientManager implements ClientManager { protected final Log logger = LogFactory.getLog(this.getClass()); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java index f4706b49eeb..a2de51b7813 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java @@ -18,6 +18,17 @@ import org.springframework.context.SmartLifecycle; +/** + * A utility abstraction over MQTT client which can be used in any MQTT-related component + * without need to handle generic client callbacks, reconnects etc. + * Using this manager in multiple MQTT integrations will preserve a single connection. + * + * @param MQTT client type + * + * @author Artem Vozhdayenko + * + * @since 6.0 + */ public interface ClientManager extends SmartLifecycle { T getClient(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index 1d93a2a2053..96b4bac3883 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -16,9 +16,6 @@ package org.springframework.integration.mqtt.core; -import java.time.Instant; -import java.util.concurrent.ScheduledFuture; - import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; @@ -26,51 +23,45 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; -public class Mqttv3ClientManager extends AbstractMqttClientManager - implements MqttCallback, InitializingBean, BeanFactoryAware { - - /** - * The default reconnect timeout in millis. - */ - private static final long DEFAULT_RECOVERY_INTERVAL = 10_000; +/** + * @author Artem Vozhdayenko + * @since 6.0 + */ +public class Mqttv3ClientManager extends AbstractMqttClientManager implements MqttCallback { private final MqttPahoClientFactory clientFactory; - private BeanFactory beanFactory; - - private TaskScheduler taskScheduler; - - private volatile ScheduledFuture scheduledReconnect; - private volatile IMqttAsyncClient client; - private long recoveryInterval = DEFAULT_RECOVERY_INTERVAL; - public Mqttv3ClientManager(MqttPahoClientFactory clientFactory, String clientId) { super(clientId); Assert.notNull(clientFactory, "'clientFactory' is required"); this.clientFactory = clientFactory; - String[] serverURIs = clientFactory.getConnectionOptions().getServerURIs(); + MqttConnectOptions connectionOptions = clientFactory.getConnectionOptions(); + String[] serverURIs = connectionOptions.getServerURIs(); Assert.notEmpty(serverURIs, "'serverURIs' must be provided in the 'MqttConnectionOptions'"); setUrl(serverURIs[0]); + if (!connectionOptions.isAutomaticReconnect()) { + logger.info("If this `ClientManager` is used from message-driven channel adapters, " + + "it is recommended to set 'automaticReconnect' MQTT connection option. " + + "Otherwise connection check and reconnect should be done manually."); + } } public Mqttv3ClientManager(String url, String clientId) { - super(clientId); + this(buildDefaultClientFactory(url), clientId); + } + + private static MqttPahoClientFactory buildDefaultClientFactory(String url) { Assert.notNull(url, "'url' is required"); - setUrl(url); MqttConnectOptions connectOptions = new MqttConnectOptions(); connectOptions.setServerURIs(new String[]{ url }); + connectOptions.setAutomaticReconnect(true); DefaultMqttPahoClientFactory defaultFactory = new DefaultMqttPahoClientFactory(); defaultFactory.setConnectionOptions(connectOptions); - this.clientFactory = defaultFactory; + return defaultFactory; } @Override @@ -78,17 +69,6 @@ public IMqttAsyncClient getClient() { return this.client; } - @Override - public void afterPropertiesSet() { - this.taskScheduler = IntegrationContextUtils.getTaskScheduler(this.beanFactory); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) { - Assert.notNull(beanFactory, "'beanFactory' must not be null"); - this.beanFactory = beanFactory; - } - @Override public synchronized void start() { if (this.client == null) { @@ -102,12 +82,20 @@ public synchronized void start() { } } try { - connect(); + MqttConnectOptions options = this.clientFactory.getConnectionOptions(); + this.client.connect(options).waitForCompletion(options.getConnectionTimeout()); } catch (MqttException e) { - logger.error("could not start client manager, scheduling reconnect, client_id=" + - this.client.getClientId(), e); - scheduleReconnect(); + logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); + + if (this.clientFactory.getConnectionOptions().isAutomaticReconnect()) { + try { + this.client.reconnect(); + } + catch (MqttException ex) { + logger.error("MQTT client failed to re-connect.", ex); + } + } } } @@ -140,9 +128,7 @@ public synchronized boolean isRunning() { @Override public synchronized void connectionLost(Throwable cause) { - logger.error("connection lost, scheduling reconnect, client_id=" + this.client.getClientId(), - cause); - scheduleReconnect(); + logger.error("connection lost, client_id=" + this.client.getClientId(), cause); } @Override @@ -155,37 +141,4 @@ public void deliveryComplete(IMqttDeliveryToken token) { // nor this manager concern } - public long getRecoveryInterval() { - return this.recoveryInterval; - } - - public void setRecoveryInterval(long recoveryInterval) { - this.recoveryInterval = recoveryInterval; - } - - private synchronized void connect() throws MqttException { - MqttConnectOptions options = this.clientFactory.getConnectionOptions(); - this.client.connect(options).waitForCompletion(options.getConnectionTimeout()); - } - - private synchronized void scheduleReconnect() { - if (this.scheduledReconnect != null) { - this.scheduledReconnect.cancel(false); - } - this.scheduledReconnect = this.taskScheduler.schedule(() -> { - try { - if (this.client.isConnected()) { - return; - } - - connect(); - this.scheduledReconnect = null; - } - catch (MqttException e) { - logger.error("could not reconnect", e); - scheduleReconnect(); - } - }, Instant.now().plusMillis(getRecoveryInterval())); - } - } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java index 1a38984680b..185320eb15d 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java @@ -28,33 +28,41 @@ import org.springframework.util.Assert; +/** + * @author Artem Vozhdayenko + * @since 6.0 + */ public class Mqttv5ClientManager extends AbstractMqttClientManager implements MqttCallback { private final MqttConnectionOptions connectionOptions; private volatile IMqttAsyncClient client; - public Mqttv5ClientManager(String url, String clientId) { - super(clientId); - Assert.notNull(url, "'url' is required"); - setUrl(url); - this.connectionOptions = new MqttConnectionOptions(); - this.connectionOptions.setServerURIs(new String[]{ url }); - this.connectionOptions.setAutomaticReconnect(true); - } - public Mqttv5ClientManager(MqttConnectionOptions connectionOptions, String clientId) { super(clientId); Assert.notNull(connectionOptions, "'connectionOptions' is required"); this.connectionOptions = connectionOptions; if (!this.connectionOptions.isAutomaticReconnect()) { - logger.warn("It is recommended to set 'automaticReconnect' MQTT connection option. " + + logger.info("If this `ClientManager` is used from message-driven channel adapters, " + + "it is recommended to set 'automaticReconnect' MQTT connection option. " + "Otherwise connection check and reconnect should be done manually."); } Assert.notEmpty(connectionOptions.getServerURIs(), "'serverURIs' must be provided in the 'MqttConnectionOptions'"); setUrl(connectionOptions.getServerURIs()[0]); } + public Mqttv5ClientManager(String url, String clientId) { + this(buildDefaultConnectionOptions(url), clientId); + } + + private static MqttConnectionOptions buildDefaultConnectionOptions(String url) { + Assert.notNull(url, "'url' is required"); + var connectionOptions = new MqttConnectionOptions(); + connectionOptions.setServerURIs(new String[]{ url }); + connectionOptions.setAutomaticReconnect(true); + return connectionOptions; + } + @Override public IMqttAsyncClient getClient() { return this.client; @@ -78,6 +86,15 @@ public synchronized void start() { } catch (MqttException e) { logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); + + if (this.connectionOptions.isAutomaticReconnect()) { + try { + this.client.reconnect(); + } + catch (MqttException ex) { + logger.error("MQTT client failed to re-connect.", ex); + } + } } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java index 75d4b9188bd..38d9bdbeec8 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java @@ -45,6 +45,7 @@ * @author Artem Bilan * @author Trung Pham * @author Mikhail Polivakha + * @author Artem Vozhdayenko * * @since 4.0 * @@ -59,9 +60,9 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends Message */ public static final long DEFAULT_COMPLETION_TIMEOUT = 30_000L; - private final String url; + private String url; - private final String clientId; + private String clientId; private final Set topics; @@ -79,14 +80,26 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends Message public AbstractMqttMessageDrivenChannelAdapter(@Nullable String url, String clientId, String... topic) { Assert.hasText(clientId, "'clientId' cannot be null or empty"); - Assert.notNull(topic, "'topics' cannot be null"); - Assert.noNullElements(topic, "'topics' cannot have null elements"); this.url = url; this.clientId = clientId; - this.topics = new LinkedHashSet<>(); + this.topics = initTopics(topic); + } + + AbstractMqttMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { + Assert.notNull(clientManager, "'clientManager' cannot be null"); + this.clientManager = clientManager; + this.topics = initTopics(topic); + } + + private Set initTopics(String[] topic) { + Assert.notNull(topic, "'topics' cannot be null"); + Assert.noNullElements(topic, "'topics' cannot have null elements"); + final Set initialTopics = new LinkedHashSet<>(); + int defaultQos = 1; for (String t : topic) { - this.topics.add(new Topic(t, 1)); + initialTopics.add(new Topic(t, defaultQos)); } + return initialTopics; } public void setConverter(MqttMessageConverter converter) { @@ -94,11 +107,6 @@ public void setConverter(MqttMessageConverter converter) { this.converter = converter; } - public void setClientManager(ClientManager clientManager) { - Assert.notNull(clientManager, "'clientManager' cannot be null"); - this.clientManager = clientManager; - } - public ClientManager getClientManager() { return this.clientManager; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index f5320d5a1e3..8e03abde6ec 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -33,6 +33,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.acks.SimpleAcknowledgment; +import org.springframework.integration.mqtt.core.ClientManager; import org.springframework.integration.mqtt.core.ConsumerStopAction; import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; import org.springframework.integration.mqtt.core.MqttPahoClientFactory; @@ -57,6 +58,7 @@ * * @author Gary Russell * @author Artem Bilan + * @author Artem Vozhdayenko * * @since 4.0 * @@ -130,6 +132,32 @@ public MqttPahoMessageDrivenChannelAdapter(String url, String clientId, String.. this(url, clientId, new DefaultMqttPahoClientFactory(), topic); } + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectOptions}. + * @param clientManager The client manager. + * @param connectOptions The connection options. + * @param topic The topic(s). + */ + public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, + MqttConnectOptions connectOptions, String... topic) { + + super(clientManager, topic); + var factory = new DefaultMqttPahoClientFactory(); + factory.setConnectionOptions(connectOptions); + this.clientFactory = factory; + } + + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection). + * @param clientManager The client manager. + * @param topic The topic(s). + */ + public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { + this(clientManager, new MqttConnectOptions(), topic); + } + /** * Set the completion timeout when disconnecting. Not settable using the namespace. * Default {@value #DISCONNECT_COMPLETION_TIMEOUT} milliseconds. @@ -279,12 +307,12 @@ private synchronized void connectAndSubscribe() throws MqttException { // NOSONA if (this.consumerStopAction == null) { this.consumerStopAction = ConsumerStopAction.UNSUBSCRIBE_CLEAN; } - Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null, - "If no 'url' provided, connectionOptions.getServerURIs() must not be null"); IMqttAsyncClient clientInstance; if (getClientManager() == null) { + Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null, + "If no 'url' provided, connectionOptions.getServerURIs() must not be null"); this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); this.client.setCallback(this); clientInstance = this.client; diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 4b0a6dd6401..e0db50a5891 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -39,6 +39,7 @@ import org.springframework.integration.acks.SimpleAcknowledgment; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.mapping.HeaderMapper; +import org.springframework.integration.mqtt.core.ClientManager; import org.springframework.integration.mqtt.core.MqttComponent; import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent; import org.springframework.integration.mqtt.event.MqttProtocolErrorEvent; @@ -70,6 +71,7 @@ * @author Artem Bilan * @author Mikhail Polivakha * @author Lucas Bowler + * @author Artem Vozhdayenko * * @since 5.5.5 * @@ -92,9 +94,7 @@ public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDr public Mqttv5PahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) { super(url, clientId, topic); - this.connectionOptions = new MqttConnectionOptions(); - this.connectionOptions.setServerURIs(new String[]{ url }); - this.connectionOptions.setAutomaticReconnect(true); + this.connectionOptions = buildDefaultConnectionOptions(url); } public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOptions, String clientId, @@ -109,6 +109,45 @@ public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOpt } } + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectionOptions}. + * @param connectionOptions The connection options. + * @param clientManager The client manager. + * @param topic The topic(s). + */ + public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOptions, + ClientManager clientManager, String... topic) { + + super(clientManager, topic); + this.connectionOptions = connectionOptions; + if (!this.connectionOptions.isAutomaticReconnect()) { + logger.warn("It is recommended to set 'automaticReconnect' MQTT client option. " + + "Otherwise the current channel adapter restart should be used explicitly, " + + "e.g. via handling 'MqttConnectionFailedEvent' on client disconnection."); + } + } + + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection). + * @param clientManager The client manager. + * @param topic The topic(s). + */ + public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { + this(buildDefaultConnectionOptions(null), clientManager, topic); + } + + private static MqttConnectionOptions buildDefaultConnectionOptions(@Nullable String url) { + final MqttConnectionOptions connectionOptions; + connectionOptions = new MqttConnectionOptions(); + if (url != null) { + connectionOptions.setServerURIs(new String[]{ url }); + } + connectionOptions.setAutomaticReconnect(true); + return connectionOptions; + } + @Override public MqttConnectionOptions getConnectionInfo() { return this.connectionOptions; @@ -358,7 +397,11 @@ private void subscribeToAll() { MqttSubscription[] subscriptions = IntStream.range(0, topics.length) .mapToObj(i -> new MqttSubscription(topics[i], requestedQos[i])) .toArray(MqttSubscription[]::new); - getClientManager().getClient().subscribe(subscriptions, new MessageListener()); + MessageListener[] listeners = IntStream.range(0, topics.length) + .mapToObj(t -> new MessageListener()) + .toArray(MessageListener[]::new); + getClientManager().getClient().subscribe(subscriptions, null, null, listeners, null) + .waitForCompletion(getCompletionTimeout()); } String message = "Connected and subscribed to " + Arrays.toString(topics); logger.debug(message); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java index 3da372f4a49..43dc7faf366 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java @@ -41,6 +41,7 @@ * * @author Gary Russell * @author Artem Bilan + * @author Artem Vozhdayenko * * @since 4.0 * @@ -63,9 +64,9 @@ public abstract class AbstractMqttMessageHandler extends AbstractMessageHandl private final AtomicBoolean running = new AtomicBoolean(); - private final String url; + private String url; - private final String clientId; + private String clientId; private long completionTimeout = DEFAULT_COMPLETION_TIMEOUT; @@ -97,6 +98,11 @@ public AbstractMqttMessageHandler(@Nullable String url, String clientId) { this.clientId = clientId; } + AbstractMqttMessageHandler(ClientManager clientManager) { + Assert.notNull(clientManager, "'clientManager' cannot be null or empty"); + this.clientManager = clientManager; + } + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -301,11 +307,6 @@ protected ClientManager getClientManager() { return this.clientManager; } - public void setClientManager(ClientManager clientManager) { - Assert.notNull(clientManager, "'clientManager' cannot be null"); - this.clientManager = clientManager; - } - @Override protected void onInit() { super.onInit(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java index 8c10a744d34..442a1ce1f9b 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java @@ -24,6 +24,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.integration.mqtt.core.ClientManager; import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; import org.springframework.integration.mqtt.core.MqttPahoClientFactory; import org.springframework.integration.mqtt.core.MqttPahoComponent; @@ -48,6 +49,7 @@ * * @author Gary Russell * @author Artem Bilan + * @author Artem Vozhdayenko * * @since 4.0 * @@ -97,6 +99,28 @@ public MqttPahoMessageHandler(String url, String clientId) { this(url, clientId, new DefaultMqttPahoClientFactory()); } + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectOptions}. + * @param clientManager The client manager. + * @param connectOptions The connection options. + */ + public MqttPahoMessageHandler(ClientManager clientManager, MqttConnectOptions connectOptions) { + super(clientManager); + var factory = new DefaultMqttPahoClientFactory(); + factory.setConnectionOptions(connectOptions); + this.clientFactory = factory; + } + + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection). + * @param clientManager The client manager. + */ + public MqttPahoMessageHandler(ClientManager clientManager) { + this(clientManager, new MqttConnectOptions()); + } + /** * Set to true if you don't want to block when sending messages. Default false. * When true, message sent/delivered events will be published for reception diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index 486361105f4..06cd06338c8 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -34,6 +34,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.mapping.HeaderMapper; +import org.springframework.integration.mqtt.core.ClientManager; import org.springframework.integration.mqtt.core.MqttComponent; import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent; import org.springframework.integration.mqtt.event.MqttMessageDeliveredEvent; @@ -52,6 +53,7 @@ * * @author Artem Bilan * @author Lucas Bowler + * @author Artem Vozhdayenko * * @since 5.5.5 */ @@ -73,9 +75,7 @@ public class Mqttv5PahoMessageHandler extends AbstractMqttMessageHandler clientManager, + MqttConnectionOptions connectionOptions) { + super(clientManager); + this.connectionOptions = connectionOptions; + } + + public Mqttv5PahoMessageHandler(ClientManager clientManager) { + this(clientManager, buildDefaultConnectionOptions(null)); + } + + private static MqttConnectionOptions buildDefaultConnectionOptions(@Nullable String url) { + final MqttConnectionOptions connectionOptions; + connectionOptions = new MqttConnectionOptions(); + if (url != null) { + connectionOptions.setServerURIs(new String[]{ url }); + } + connectionOptions.setAutomaticReconnect(true); + return connectionOptions; + } + private static String obtainServerUrlFromOptions(MqttConnectionOptions connectionOptions) { Assert.notNull(connectionOptions, "'connectionOptions' must not be null"); diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java index c8ebf1e9835..427d78b28f3 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java @@ -18,50 +18,62 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.nio.charset.StandardCharsets; + import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; import org.springframework.integration.mqtt.core.MqttPahoClientFactory; import org.springframework.integration.mqtt.core.Mqttv3ClientManager; import org.springframework.integration.mqtt.core.Mqttv5ClientManager; +import org.springframework.integration.mqtt.event.MqttSubscribedEvent; import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; import org.springframework.integration.mqtt.inbound.Mqttv5PahoMessageDrivenChannelAdapter; import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler; import org.springframework.integration.mqtt.outbound.Mqttv5PahoMessageHandler; import org.springframework.integration.mqtt.support.MqttHeaders; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.condition.LongRunningTest; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** * @author Artem Vozhdayenko + * @since 6.0 */ +@LongRunningTest class ClientManagerBackToBackTests implements MosquittoContainerTest { - static final String SHARED_CLIENT_ID = "shared-client-id"; - - public static final String TOPIC_NAME = "test-topic"; - @Test void testSameV3ClientIdWorksForPubAndSub() { - testSubscribeAndPublish(Mqttv3Config.class); + testSubscribeAndPublish(Mqttv3Config.class, Mqttv3Config.TOPIC_NAME); } @Test void testSameV5ClientIdWorksForPubAndSub() { - testSubscribeAndPublish(Mqttv5Config.class); + testSubscribeAndPublish(Mqttv5Config.class, Mqttv5Config.TOPIC_NAME); + } + + @Test + void testV3ClientManagerReconnect() { + testSubscribeAndPublish(Mqttv3ConfigWithDisconnect.class, Mqttv3ConfigWithDisconnect.TOPIC_NAME); } - private void testSubscribeAndPublish(Class configClass) { + @Test + void testV5ClientManagerReconnect() { + testSubscribeAndPublish(Mqttv5ConfigWithDisconnect.class, Mqttv5ConfigWithDisconnect.TOPIC_NAME); + } + + private void testSubscribeAndPublish(Class configClass, String topicName) { try (var ctx = new AnnotationConfigApplicationContext(configClass)) { // given var input = ctx.getBean("mqttOutFlow.input", MessageChannel.class); @@ -69,12 +81,18 @@ private void testSubscribeAndPublish(Class configClass) { String testPayload = "foo"; // when - input.send(MessageBuilder.withPayload(testPayload).setHeader(MqttHeaders.TOPIC, TOPIC_NAME).build()); - Message receive = output.receive(10_000); + input.send(MessageBuilder.withPayload(testPayload).setHeader(MqttHeaders.TOPIC, topicName).build()); + Message receive = output.receive(20_000); // then assertThat(receive).isNotNull(); - assertThat(receive.getPayload()).isEqualTo(testPayload); + Object payload = receive.getPayload(); + if (payload instanceof String sp) { + assertThat(sp).isEqualTo(testPayload); + } + else { + assertThat(payload).isEqualTo(testPayload.getBytes(StandardCharsets.UTF_8)); + } } } @@ -82,14 +100,51 @@ private void testSubscribeAndPublish(Class configClass) { @EnableIntegration public static class Mqttv3Config { + static final String TOPIC_NAME = "test-topic-v3"; + + @Bean + public Mqttv3ClientManager mqttv3ClientManager(MqttPahoClientFactory pahoClientFactory) { + return new Mqttv3ClientManager(pahoClientFactory, "client-manager-client-id-v3"); + } + + @Bean + public MqttPahoClientFactory pahoClientFactory() { + var pahoClientFactory = new DefaultMqttPahoClientFactory(); + MqttConnectOptions connectionOptions = new MqttConnectOptions(); + connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); + connectionOptions.setAutomaticReconnect(true); + pahoClientFactory.setConnectionOptions(connectionOptions); + return pahoClientFactory; + } + + @Bean + public IntegrationFlow mqttOutFlow(Mqttv3ClientManager mqttv3ClientManager) { + return f -> f.handle(new MqttPahoMessageHandler(mqttv3ClientManager)); + } + + @Bean + public IntegrationFlow mqttInFlow(Mqttv3ClientManager mqttv3ClientManager) { + return IntegrationFlow.from(new MqttPahoMessageDrivenChannelAdapter(mqttv3ClientManager, TOPIC_NAME)) + .channel(c -> c.queue("fromMqttChannel")) + .get(); + } + + } + + @Configuration + @EnableIntegration + public static class Mqttv3ConfigWithDisconnect { + + static final String TOPIC_NAME = "test-topic-v3-reconnect"; + @Bean - public TaskScheduler taskScheduler() { - return new ThreadPoolTaskScheduler(); + public ClientV3Disconnector disconnector(Mqttv3ClientManager mqttv3ClientManager) { + return new ClientV3Disconnector(mqttv3ClientManager); } @Bean public Mqttv3ClientManager mqttv3ClientManager(MqttPahoClientFactory pahoClientFactory) { - return new Mqttv3ClientManager(pahoClientFactory, SHARED_CLIENT_ID); + return new Mqttv3ClientManager(pahoClientFactory, "client-manager-client-id-v3-reconnect"); } @Bean @@ -97,26 +152,44 @@ public MqttPahoClientFactory pahoClientFactory() { var pahoClientFactory = new DefaultMqttPahoClientFactory(); MqttConnectOptions connectionOptions = new MqttConnectOptions(); connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); + connectionOptions.setAutomaticReconnect(true); pahoClientFactory.setConnectionOptions(connectionOptions); return pahoClientFactory; } @Bean - public IntegrationFlow mqttOutFlow(MqttPahoClientFactory pahoClientFactory, - Mqttv3ClientManager mqttv3ClientManager) { + public IntegrationFlow mqttOutFlow() { + return f -> f.handle(new MqttPahoMessageHandler(MosquittoContainerTest.mqttUrl(), "old-client-v3")); + } - var mqttHandler = new MqttPahoMessageHandler(SHARED_CLIENT_ID, pahoClientFactory); - mqttHandler.setClientManager(mqttv3ClientManager); - return f -> f.handle(mqttHandler); + @Bean + public IntegrationFlow mqttInFlow(Mqttv3ClientManager mqttv3ClientManager) { + return IntegrationFlow.from(new MqttPahoMessageDrivenChannelAdapter(mqttv3ClientManager, TOPIC_NAME)) + .channel(c -> c.queue("fromMqttChannel")) + .get(); } + } + + @Configuration + @EnableIntegration + public static class Mqttv5Config { + + static final String TOPIC_NAME = "test-topic-v5"; + @Bean - public IntegrationFlow mqttInFlow(MqttPahoClientFactory pahoClientFactory, - Mqttv3ClientManager mqttv3ClientManager) { + public Mqttv5ClientManager mqttv5ClientManager() { + return new Mqttv5ClientManager(MosquittoContainerTest.mqttUrl(), "client-manager-client-id-v5"); + } - var mqttAdapter = new MqttPahoMessageDrivenChannelAdapter(SHARED_CLIENT_ID, pahoClientFactory, TOPIC_NAME); - mqttAdapter.setClientManager(mqttv3ClientManager); - return IntegrationFlow.from(mqttAdapter) + @Bean + public IntegrationFlow mqttOutFlow(Mqttv5ClientManager mqttv5ClientManager) { + return f -> f.handle(new Mqttv5PahoMessageHandler(mqttv5ClientManager)); + } + + @Bean + public IntegrationFlow mqttInFlow(Mqttv5ClientManager mqttv5ClientManager) { + return IntegrationFlow.from(new Mqttv5PahoMessageDrivenChannelAdapter(mqttv5ClientManager, TOPIC_NAME)) .channel(c -> c.queue("fromMqttChannel")) .get(); } @@ -125,34 +198,72 @@ public IntegrationFlow mqttInFlow(MqttPahoClientFactory pahoClientFactory, @Configuration @EnableIntegration - public static class Mqttv5Config { + public static class Mqttv5ConfigWithDisconnect { + + static final String TOPIC_NAME = "test-topic-v5-reconnect"; @Bean - public TaskScheduler taskScheduler() { - return new ThreadPoolTaskScheduler(); + public ClientV5Disconnector disconnector(Mqttv5ClientManager mqttv5ClientManager) { + return new ClientV5Disconnector(mqttv5ClientManager); } @Bean public Mqttv5ClientManager mqttv5ClientManager() { - return new Mqttv5ClientManager(MosquittoContainerTest.mqttUrl(), SHARED_CLIENT_ID); + return new Mqttv5ClientManager(MosquittoContainerTest.mqttUrl(), "client-manager-client-id-v5-reconnect"); } @Bean public IntegrationFlow mqttOutFlow(Mqttv5ClientManager mqttv5ClientManager) { - var mqttHandler = new Mqttv5PahoMessageHandler(MosquittoContainerTest.mqttUrl(), SHARED_CLIENT_ID); - mqttHandler.setClientManager(mqttv5ClientManager); // todo: add into ctor - return f -> f.handle(mqttHandler); + return f -> f.handle(new Mqttv5PahoMessageHandler(MosquittoContainerTest.mqttUrl(), "old-client-v5")); } @Bean public IntegrationFlow mqttInFlow(Mqttv5ClientManager mqttv5ClientManager) { - var mqttAdapter = new Mqttv5PahoMessageDrivenChannelAdapter(MosquittoContainerTest.mqttUrl(), SHARED_CLIENT_ID, TOPIC_NAME); - mqttAdapter.setClientManager(mqttv5ClientManager); - return IntegrationFlow.from(mqttAdapter) + return IntegrationFlow.from(new Mqttv5PahoMessageDrivenChannelAdapter(mqttv5ClientManager, TOPIC_NAME)) .channel(c -> c.queue("fromMqttChannel")) .get(); } } + public static class ClientV3Disconnector { + + private final Mqttv3ClientManager clientManager; + + ClientV3Disconnector(Mqttv3ClientManager clientManager) { + this.clientManager = clientManager; + } + + @EventListener + public void handleSubscribedEvent(MqttSubscribedEvent e) { + try { + this.clientManager.getClient().disconnectForcibly(); + } + catch (MqttException ex) { + throw new IllegalStateException("could not disconnect the client!"); + } + } + + } + + public static class ClientV5Disconnector { + + private final Mqttv5ClientManager clientManager; + + ClientV5Disconnector(Mqttv5ClientManager clientManager) { + this.clientManager = clientManager; + } + + @EventListener + public void handleSubscribedEvent(MqttSubscribedEvent e) { + try { + this.clientManager.getClient().disconnectForcibly(); + } + catch (org.eclipse.paho.mqttv5.common.MqttException ex) { + throw new IllegalStateException("could not disconnect the client!"); + } + } + + } + } diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java index 5bfcd58d61f..b4cc065c429 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java @@ -102,6 +102,7 @@ /** * @author Gary Russell * @author Artem Bilan + * @author Artem Vozhdayenko * * @since 4.0 * @@ -221,9 +222,7 @@ void testClientManagerIsNotConnectedAndClosedInHandler() throws Exception { var deliveryToken = mock(MqttDeliveryToken.class); given(client.publish(anyString(), any(MqttMessage.class))).willReturn(deliveryToken); - var handler = new MqttPahoMessageHandler("foo", "bar", - new DefaultMqttPahoClientFactory()); - handler.setClientManager(clientManager); + var handler = new MqttPahoMessageHandler(clientManager); handler.setDefaultTopic("mqtt-foo"); handler.setBeanFactory(mock(BeanFactory.class)); handler.afterPropertiesSet(); @@ -254,9 +253,7 @@ void testClientManagerIsNotConnectedAndClosedInAdapter() throws Exception { given(client.subscribe(any(String[].class), any(int[].class), any())) .willReturn(subscribeToken); - var adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", - new DefaultMqttPahoClientFactory(), "mqtt-foo"); - adapter.setClientManager(clientManager); + var adapter = new MqttPahoMessageDrivenChannelAdapter(clientManager, "mqtt-foo"); ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.initialize(); adapter.setTaskScheduler(taskScheduler); From 4cc094f452f60b8ecacfd632bf9f500e65a30f3c Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Sun, 31 Jul 2022 15:49:41 +0300 Subject: [PATCH 5/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Some fixes and improvements after another code review iteration: * Rearrange the code according to the code style guides * Move client instance to `AbstractClientManager` with `isRunning` method * Fix abstract adapter/handler fields visibility and `final`ize them where we can * Send application event if automatic reconnect is not enabled for the client manager --- .../mqtt/core/AbstractMqttClientManager.java | 46 ++++++++++++++----- .../mqtt/core/Mqttv3ClientManager.java | 22 +++------ .../mqtt/core/Mqttv5ClientManager.java | 24 ++++------ ...stractMqttMessageDrivenChannelAdapter.java | 13 ++++-- .../MqttPahoMessageDrivenChannelAdapter.java | 31 ++++++------- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 20 ++++---- .../outbound/AbstractMqttMessageHandler.java | 11 +++-- .../mqtt/outbound/MqttPahoMessageHandler.java | 28 +++++------ .../outbound/Mqttv5PahoMessageHandler.java | 8 ++-- 9 files changed, 110 insertions(+), 93 deletions(-) diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index f692339bd16..eda1e249443 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -19,6 +19,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.util.Assert; /** @@ -28,9 +30,11 @@ * * @since 6.0 */ -public abstract class AbstractMqttClientManager implements ClientManager { +public abstract class AbstractMqttClientManager implements ClientManager, ApplicationEventPublisherAware { - protected final Log logger = LogFactory.getLog(this.getClass()); + protected final Log logger = LogFactory.getLog(this.getClass()); // NOSONAR + + private ApplicationEventPublisher applicationEventPublisher; private boolean manualAcks; @@ -38,30 +42,50 @@ public abstract class AbstractMqttClientManager implements ClientManager { private final String clientId; + volatile T client; + AbstractMqttClientManager(String clientId) { Assert.notNull(clientId, "'clientId' is required"); this.clientId = clientId; } - @Override - public boolean isManualAcks() { - return this.manualAcks; - } - - public void setManualAcks(boolean manualAcks) { + protected void setManualAcks(boolean manualAcks) { this.manualAcks = manualAcks; } - public String getUrl() { + protected String getUrl() { return this.url; } - public void setUrl(String url) { + protected void setUrl(String url) { this.url = url; } - public String getClientId() { + protected String getClientId() { return this.clientId; } + protected ApplicationEventPublisher getApplicationEventPublisher() { + return this.applicationEventPublisher; + } + + @Override + public boolean isManualAcks() { + return this.manualAcks; + } + + @Override + public T getClient() { + return this.client; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + Assert.notNull(applicationEventPublisher, "'applicationEventPublisher' cannot be null"); + this.applicationEventPublisher = applicationEventPublisher; + } + + public synchronized boolean isRunning() { + return this.client != null; + } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index 96b4bac3883..be9066a3cb6 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -23,6 +23,7 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent; import org.springframework.util.Assert; /** @@ -33,7 +34,9 @@ public class Mqttv3ClientManager extends AbstractMqttClientManager extends Message */ public static final long DEFAULT_COMPLETION_TIMEOUT = 30_000L; - private String url; + private final String url; - private String clientId; + private final String clientId; private final Set topics; @@ -74,7 +74,7 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends Message private MqttMessageConverter converter; - protected ClientManager clientManager; + private final ClientManager clientManager; protected final Lock topicLock = new ReentrantLock(); // NOSONAR @@ -83,12 +83,15 @@ public AbstractMqttMessageDrivenChannelAdapter(@Nullable String url, String clie this.url = url; this.clientId = clientId; this.topics = initTopics(topic); + this.clientManager = null; } AbstractMqttMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { Assert.notNull(clientManager, "'clientManager' cannot be null"); this.clientManager = clientManager; this.topics = initTopics(topic); + this.url = null; + this.clientId = null; } private Set initTopics(String[] topic) { @@ -107,7 +110,8 @@ public void setConverter(MqttMessageConverter converter) { this.converter = converter; } - public ClientManager getClientManager() { + @Nullable + protected ClientManager getClientManager() { return this.clientManager; } @@ -155,6 +159,7 @@ protected String getUrl() { return this.url; } + @Nullable protected String getClientId() { return this.clientId; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index 8e03abde6ec..217ea439f0b 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -89,6 +89,16 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv private volatile ConsumerStopAction consumerStopAction; + /** + * Use this constructor when you don't need additional {@link MqttConnectOptions}. + * @param url The URL. + * @param clientId The client id. + * @param topic The topic(s). + */ + public MqttPahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) { + this(url, clientId, new DefaultMqttPahoClientFactory(), topic); + } + /** * Use this constructor for a single url (although it may be overridden if the server * URI(s) are provided by the {@link MqttConnectOptions#getServerURIs()} provided by @@ -121,15 +131,14 @@ public MqttPahoMessageDrivenChannelAdapter(String clientId, MqttPahoClientFactor this.clientFactory = clientFactory; } - /** - * Use this constructor when you don't need additional {@link MqttConnectOptions}. - * @param url The URL. - * @param clientId The client id. + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection). + * @param clientManager The client manager. * @param topic The topic(s). */ - public MqttPahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) { - this(url, clientId, new DefaultMqttPahoClientFactory(), topic); + public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { + this(clientManager, new MqttConnectOptions(), topic); } /** @@ -148,16 +157,6 @@ public MqttPahoMessageDrivenChannelAdapter(ClientManager clien this.clientFactory = factory; } - /** - * Use this constructor when you need to use a single {@link ClientManager} - * (for instance, to reuse an MQTT connection). - * @param clientManager The client manager. - * @param topic The topic(s). - */ - public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { - this(clientManager, new MqttConnectOptions(), topic); - } - /** * Set the completion timeout when disconnecting. Not settable using the namespace. * Default {@value #DISCONNECT_COMPLETION_TIMEOUT} milliseconds. diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index e0db50a5891..0ef1ced4222 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -109,6 +109,16 @@ public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOpt } } + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection). + * @param clientManager The client manager. + * @param topic The topic(s). + */ + public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { + this(buildDefaultConnectionOptions(null), clientManager, topic); + } + /** * Use this constructor when you need to use a single {@link ClientManager} * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectionOptions}. @@ -128,16 +138,6 @@ public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOpt } } - /** - * Use this constructor when you need to use a single {@link ClientManager} - * (for instance, to reuse an MQTT connection). - * @param clientManager The client manager. - * @param topic The topic(s). - */ - public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { - this(buildDefaultConnectionOptions(null), clientManager, topic); - } - private static MqttConnectionOptions buildDefaultConnectionOptions(@Nullable String url) { final MqttConnectionOptions connectionOptions; connectionOptions = new MqttConnectionOptions(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java index 43dc7faf366..6da4f814f37 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java @@ -64,9 +64,9 @@ public abstract class AbstractMqttMessageHandler extends AbstractMessageHandl private final AtomicBoolean running = new AtomicBoolean(); - private String url; + private final String url; - private String clientId; + private final String clientId; private long completionTimeout = DEFAULT_COMPLETION_TIMEOUT; @@ -90,17 +90,20 @@ public abstract class AbstractMqttMessageHandler extends AbstractMessageHandl private int clientInstance; - protected ClientManager clientManager; + private final ClientManager clientManager; public AbstractMqttMessageHandler(@Nullable String url, String clientId) { Assert.hasText(clientId, "'clientId' cannot be null or empty"); this.url = url; this.clientId = clientId; + this.clientManager = null; } AbstractMqttMessageHandler(ClientManager clientManager) { Assert.notNull(clientManager, "'clientManager' cannot be null or empty"); this.clientManager = clientManager; + this.url = null; + this.clientId = null; } @Override @@ -253,6 +256,7 @@ protected String getUrl() { return this.url; } + @Nullable public String getClientId() { return this.clientId; } @@ -303,6 +307,7 @@ protected long getDisconnectCompletionTimeout() { return this.disconnectCompletionTimeout; } + @Nullable protected ClientManager getClientManager() { return this.clientManager; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java index 442a1ce1f9b..14edad76452 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java @@ -65,6 +65,15 @@ public class MqttPahoMessageHandler extends AbstractMqttMessageHandler clientManager) { + this(clientManager, new MqttConnectOptions()); } /** @@ -112,15 +121,6 @@ public MqttPahoMessageHandler(ClientManager clientManager, Mqt this.clientFactory = factory; } - /** - * Use this constructor when you need to use a single {@link ClientManager} - * (for instance, to reuse an MQTT connection). - * @param clientManager The client manager. - */ - public MqttPahoMessageHandler(ClientManager clientManager) { - this(clientManager, new MqttConnectOptions()); - } - /** * Set to true if you don't want to block when sending messages. Default false. * When true, message sent/delivered events will be published for reception diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index 06cd06338c8..cc832ad5c0b 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -83,16 +83,16 @@ public Mqttv5PahoMessageHandler(MqttConnectionOptions connectionOptions, String this.connectionOptions = connectionOptions; } + public Mqttv5PahoMessageHandler(ClientManager clientManager) { + this(clientManager, buildDefaultConnectionOptions(null)); + } + public Mqttv5PahoMessageHandler(ClientManager clientManager, MqttConnectionOptions connectionOptions) { super(clientManager); this.connectionOptions = connectionOptions; } - public Mqttv5PahoMessageHandler(ClientManager clientManager) { - this(clientManager, buildDefaultConnectionOptions(null)); - } - private static MqttConnectionOptions buildDefaultConnectionOptions(@Nullable String url) { final MqttConnectionOptions connectionOptions; connectionOptions = new MqttConnectionOptions(); From f3fba7d18f6bc912aaf968894a2cbe9ff4616d73 Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Sat, 6 Aug 2022 21:22:33 +0300 Subject: [PATCH 6/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Other fixes and improvements after code review: * Changes around fields, methods, ctors visibility * Removed contradictory ctors * Reduce amount of unnecessary `getClientManager() != null` checks in logic and make it as similar as possible for client manager and the old approach * Use auto-reconnect where possible * Remove manual reconnect trigger and rely on events instead to know where to subscribe * Do not close the connection in adapter to be able to use reconnect logic without lose of subscriptions * Make `ClientManager` extend `MqttComponent` so that it knows about connection options as part of its contract * Remove not relevant auto test cases (relying on connection close or manual reconnect) * Other code style smaller changes --- ...MqttMessageDrivenChannelAdapterParser.java | 3 +- .../mqtt/core/AbstractMqttClientManager.java | 60 +++- .../integration/mqtt/core/ClientManager.java | 16 +- .../mqtt/core/Mqttv3ClientManager.java | 53 ++-- .../mqtt/core/Mqttv5ClientManager.java | 49 ++-- ...stractMqttMessageDrivenChannelAdapter.java | 24 +- .../MqttPahoMessageDrivenChannelAdapter.java | 257 +++++++----------- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 142 +++++----- .../outbound/AbstractMqttMessageHandler.java | 11 +- .../mqtt/outbound/MqttPahoMessageHandler.java | 17 +- .../outbound/Mqttv5PahoMessageHandler.java | 17 +- .../mqtt/config/spring-integration-mqtt.xsd | 9 - .../mqtt/ClientManagerBackToBackTests.java | 62 ++++- .../integration/mqtt/MqttAdapterTests.java | 203 +++----------- ...ttv5BackToBackAutomaticReconnectTests.java | 71 +++-- ...rivenChannelAdapterParserTests-context.xml | 1 - ...essageDrivenChannelAdapterParserTests.java | 3 +- 17 files changed, 450 insertions(+), 548 deletions(-) diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java index c0f05904be1..d8b335af06a 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,6 @@ protected AbstractBeanDefinition doParse(Element element, ParserContext parserCo builder.addPropertyReference("outputChannel", channelName); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "qos"); - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "recovery-interval"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "manual-acks"); return builder.getBeanDefinition(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index eda1e249443..f5badf32bbf 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -16,37 +16,49 @@ package org.springframework.integration.mqtt.core; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.SmartLifecycle; +import org.springframework.integration.mqtt.inbound.AbstractMqttMessageDrivenChannelAdapter; import org.springframework.util.Assert; /** * @param MQTT client type + * @param MQTT connection options type (v5 or v3) * * @author Artem Vozhdayenko * * @since 6.0 */ -public abstract class AbstractMqttClientManager implements ClientManager, ApplicationEventPublisherAware { +public abstract class AbstractMqttClientManager implements ClientManager, ApplicationEventPublisherAware { protected final Log logger = LogFactory.getLog(this.getClass()); // NOSONAR - private ApplicationEventPublisher applicationEventPublisher; + private final Set connectCallbacks; + + private final String clientId; private boolean manualAcks; + private ApplicationEventPublisher applicationEventPublisher; + private String url; - private final String clientId; + private volatile T client; - volatile T client; + private String beanName; AbstractMqttClientManager(String clientId) { Assert.notNull(clientId, "'clientId' is required"); this.clientId = clientId; + this.connectCallbacks = Collections.synchronizedSet(new HashSet<>()); } protected void setManualAcks(boolean manualAcks) { @@ -69,6 +81,14 @@ protected ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } + protected synchronized void setClient(T client) { + this.client = client; + } + + protected Set getCallbacks() { + return Collections.unmodifiableSet(this.connectCallbacks); + } + @Override public boolean isManualAcks() { return this.manualAcks; @@ -85,7 +105,39 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv this.applicationEventPublisher = applicationEventPublisher; } + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public String getBeanName() { + return this.beanName; + } + + /** + * The phase of component autostart in {@link SmartLifecycle}. + * If the custom one is required, note that for the correct behavior it should be less than phase of + * {@link AbstractMqttMessageDrivenChannelAdapter} implementations. + * @return {@link SmartLifecycle} autostart phase + */ + @Override + public int getPhase() { + return 0; + } + + @Override + public void addCallback(ConnectCallback connectCallback) { + this.connectCallbacks.add(connectCallback); + } + + @Override + public boolean removeCallback(ConnectCallback connectCallback) { + return this.connectCallbacks.remove(connectCallback); + } + public synchronized boolean isRunning() { return this.client != null; } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java index a2de51b7813..b9e4a5a5a02 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java @@ -24,15 +24,29 @@ * Using this manager in multiple MQTT integrations will preserve a single connection. * * @param MQTT client type + * @param MQTT connection options type (v5 or v3) * * @author Artem Vozhdayenko * * @since 6.0 */ -public interface ClientManager extends SmartLifecycle { +public interface ClientManager extends SmartLifecycle, MqttComponent { T getClient(); boolean isManualAcks(); + void addCallback(ConnectCallback connectCallback); + + boolean removeCallback(ConnectCallback connectCallback); + + /** + * A contract for a custom callback if needed by a usage. + */ + interface ConnectCallback { + + void connectComplete(boolean isReconnect); + + } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index be9066a3cb6..bdc1766673a 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -18,7 +18,7 @@ import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; @@ -30,7 +30,8 @@ * @author Artem Vozhdayenko * @since 6.0 */ -public class Mqttv3ClientManager extends AbstractMqttClientManager implements MqttCallback { +public class Mqttv3ClientManager extends AbstractMqttClientManager + implements MqttCallbackExtended { private final MqttPahoClientFactory clientFactory; @@ -65,11 +66,12 @@ private static MqttPahoClientFactory buildDefaultClientFactory(String url) { @Override public synchronized void start() { - if (this.client == null) { + if (getClient() == null) { try { - this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); - this.client.setManualAcks(isManualAcks()); - this.client.setCallback(this); + var client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); + client.setManualAcks(isManualAcks()); + client.setCallback(this); + setClient(client); } catch (MqttException e) { throw new IllegalStateException("could not start client manager", e); @@ -77,50 +79,49 @@ public synchronized void start() { } try { MqttConnectOptions options = this.clientFactory.getConnectionOptions(); - this.client.connect(options).waitForCompletion(options.getConnectionTimeout()); + getClient().connect(options).waitForCompletion(options.getConnectionTimeout()); } catch (MqttException e) { - logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); - - if (this.clientFactory.getConnectionOptions().isAutomaticReconnect()) { - try { - this.client.reconnect(); - } - catch (MqttException ex) { - logger.error("MQTT client failed to re-connect.", ex); - } - } - else if (getApplicationEventPublisher() != null) { - getApplicationEventPublisher().publishEvent(new MqttConnectionFailedEvent(this, e)); + logger.error("could not start client manager, client_id=" + getClientId(), e); + + var applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); } } } @Override public synchronized void stop() { - if (this.client == null) { + var client = getClient(); + if (client == null) { return; } try { - this.client.disconnectForcibly(this.clientFactory.getConnectionOptions().getConnectionTimeout()); + client.disconnectForcibly(this.clientFactory.getConnectionOptions().getConnectionTimeout()); } catch (MqttException e) { logger.error("could not disconnect from the client", e); } finally { try { - this.client.close(); + client.close(); } catch (MqttException e) { logger.error("could not close the client", e); } - this.client = null; + setClient(null); } } @Override public synchronized void connectionLost(Throwable cause) { - logger.error("connection lost, client_id=" + this.client.getClientId(), cause); + logger.error("connection lost, client_id=" + getClientId(), cause); + } + + @Override + public void connectComplete(boolean reconnect, String serverURI) { + getCallbacks().forEach(callback -> callback.connectComplete(reconnect)); } @Override @@ -133,4 +134,8 @@ public void deliveryComplete(IMqttDeliveryToken token) { // nor this manager concern } + @Override + public MqttConnectOptions getConnectionInfo() { + return this.clientFactory.getConnectionOptions(); + } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java index a6c51d489f4..b3c6ed3f7a4 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java @@ -33,7 +33,8 @@ * @author Artem Vozhdayenko * @since 6.0 */ -public class Mqttv5ClientManager extends AbstractMqttClientManager implements MqttCallback { +public class Mqttv5ClientManager extends AbstractMqttClientManager + implements MqttCallback { private final MqttConnectionOptions connectionOptions; @@ -64,57 +65,52 @@ private static MqttConnectionOptions buildDefaultConnectionOptions(String url) { @Override public synchronized void start() { - if (this.client == null) { + if (getClient() == null) { try { - this.client = new MqttAsyncClient(getUrl(), getClientId()); - this.client.setManualAcks(isManualAcks()); - this.client.setCallback(this); + var client = new MqttAsyncClient(getUrl(), getClientId()); + client.setManualAcks(isManualAcks()); + client.setCallback(this); + setClient(client); } catch (MqttException e) { throw new IllegalStateException("could not start client manager", e); } } try { - this.client.connect(this.connectionOptions) + getClient().connect(this.connectionOptions) .waitForCompletion(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { - logger.error("could not start client manager, client_id=" + this.client.getClientId(), e); - - if (this.connectionOptions.isAutomaticReconnect()) { - try { - this.client.reconnect(); - } - catch (MqttException ex) { - logger.error("MQTT client failed to re-connect.", ex); - } - } - else if (getApplicationEventPublisher() != null) { - getApplicationEventPublisher().publishEvent(new MqttConnectionFailedEvent(this, e)); + logger.error("could not start client manager, client_id=" + getClientId(), e); + + var applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); } } } @Override public synchronized void stop() { - if (this.client == null) { + var client = getClient(); + if (client == null) { return; } try { - this.client.disconnectForcibly(this.connectionOptions.getConnectionTimeout()); + client.disconnectForcibly(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { logger.error("could not disconnect from the client", e); } finally { try { - this.client.close(); + client.close(); } catch (MqttException e) { logger.error("could not close the client", e); } - this.client = null; + setClient(null); } } @@ -130,10 +126,7 @@ public void deliveryComplete(IMqttToken token) { @Override public void connectComplete(boolean reconnect, String serverURI) { - if (logger.isInfoEnabled()) { - logger.info("MQTT connect complete to " + serverURI); - } - // probably makes sense to use custom callbacks in the future + getCallbacks().forEach(callback -> callback.connectComplete(reconnect)); } @Override @@ -153,4 +146,8 @@ public void mqttErrorOccurred(MqttException exception) { logger.error("MQTT error occurred", exception); } + @Override + public MqttConnectionOptions getConnectionInfo() { + return this.connectionOptions; + } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java index 09225c6f8d8..bccf102dbe3 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java @@ -40,6 +40,7 @@ * Abstract class for MQTT Message-Driven Channel Adapters. * * @param MQTT Client type + * @param MQTT connection options type (v5 or v3) * * @author Gary Russell * @author Artem Bilan @@ -52,7 +53,7 @@ */ @ManagedResource @IntegrationManagedResource -public abstract class AbstractMqttMessageDrivenChannelAdapter extends MessageProducerSupport +public abstract class AbstractMqttMessageDrivenChannelAdapter extends MessageProducerSupport implements ApplicationEventPublisherAware { /** @@ -60,12 +61,16 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends Message */ public static final long DEFAULT_COMPLETION_TIMEOUT = 30_000L; + protected final Lock topicLock = new ReentrantLock(); // NOSONAR + private final String url; private final String clientId; private final Set topics; + private final ClientManager clientManager; + private long completionTimeout = DEFAULT_COMPLETION_TIMEOUT; private boolean manualAcks; @@ -74,9 +79,7 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends Message private MqttMessageConverter converter; - private final ClientManager clientManager; - - protected final Lock topicLock = new ReentrantLock(); // NOSONAR + private ClientManager.ConnectCallback clientManagerConnectCallback; public AbstractMqttMessageDrivenChannelAdapter(@Nullable String url, String clientId, String... topic) { Assert.hasText(clientId, "'clientId' cannot be null or empty"); @@ -86,7 +89,7 @@ public AbstractMqttMessageDrivenChannelAdapter(@Nullable String url, String clie this.clientManager = null; } - AbstractMqttMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { + public AbstractMqttMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { Assert.notNull(clientManager, "'clientManager' cannot be null"); this.clientManager = clientManager; this.topics = initTopics(topic); @@ -111,10 +114,19 @@ public void setConverter(MqttMessageConverter converter) { } @Nullable - protected ClientManager getClientManager() { + protected ClientManager getClientManager() { return this.clientManager; } + @Nullable + protected ClientManager.ConnectCallback getClientManagerCallback() { + return this.clientManagerConnectCallback; + } + + protected void setClientManagerCallback(ClientManager.ConnectCallback clientManagerConnectCallback) { + this.clientManagerConnectCallback = clientManagerConnectCallback; + } + /** * Set the QoS for each topic; a single value will apply to all topics otherwise * the correct number of qos values must be provided. diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index 217ea439f0b..e08af66d056 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -16,16 +16,14 @@ package org.springframework.integration.mqtt.inbound; -import java.time.Instant; import java.util.Arrays; -import java.util.concurrent.ScheduledFuture; import java.util.stream.Stream; import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.IMqttMessageListener; import org.eclipse.paho.client.mqttv3.IMqttToken; -import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; @@ -63,28 +61,21 @@ * @since 4.0 * */ -public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter - implements MqttCallback, MqttPahoComponent { +public class MqttPahoMessageDrivenChannelAdapter + extends AbstractMqttMessageDrivenChannelAdapter + implements MqttCallbackExtended, MqttPahoComponent { /** * The default disconnect completion timeout in milliseconds. */ public static final long DISCONNECT_COMPLETION_TIMEOUT = 5_000L; - private static final int DEFAULT_RECOVERY_INTERVAL = 10_000; - private final MqttPahoClientFactory clientFactory; - private int recoveryInterval = DEFAULT_RECOVERY_INTERVAL; - private long disconnectCompletionTimeout = DISCONNECT_COMPLETION_TIMEOUT; private volatile IMqttAsyncClient client; - private volatile ScheduledFuture reconnectFuture; - - private volatile boolean connected; - private volatile boolean cleanSession; private volatile ConsumerStopAction consumerStopAction; @@ -136,25 +127,15 @@ public MqttPahoMessageDrivenChannelAdapter(String clientId, MqttPahoClientFactor * (for instance, to reuse an MQTT connection). * @param clientManager The client manager. * @param topic The topic(s). + * @since 6.0 */ - public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { - this(clientManager, new MqttConnectOptions(), topic); - } - - /** - * Use this constructor when you need to use a single {@link ClientManager} - * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectOptions}. - * @param clientManager The client manager. - * @param connectOptions The connection options. - * @param topic The topic(s). - */ - public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, - MqttConnectOptions connectOptions, String... topic) { - + public MqttPahoMessageDrivenChannelAdapter(ClientManager clientManager, + String... topic) { super(clientManager, topic); var factory = new DefaultMqttPahoClientFactory(); - factory.setConnectionOptions(connectOptions); + factory.setConnectionOptions(clientManager.getConnectionInfo()); this.clientFactory = factory; + setClientManagerCallback(new AdapterConnectCallback()); } /** @@ -167,16 +148,6 @@ public synchronized void setDisconnectCompletionTimeout(long completionTimeout) this.disconnectCompletionTimeout = completionTimeout; } - /** - * The time (ms) to wait between reconnection attempts. - * Default {@value #DEFAULT_RECOVERY_INTERVAL}. - * @param recoveryInterval the interval. - * @since 4.2.2 - */ - public synchronized void setRecoveryInterval(int recoveryInterval) { - this.recoveryInterval = recoveryInterval; - } - @Override public MqttConnectOptions getConnectionInfo() { MqttConnectOptions options = this.clientFactory.getConnectionOptions(); @@ -197,61 +168,68 @@ protected void onInit() { DefaultPahoMessageConverter pahoMessageConverter = new DefaultPahoMessageConverter(); pahoMessageConverter.setBeanFactory(getBeanFactory()); setConverter(pahoMessageConverter); + } + var clientManager = getClientManager(); + if (clientManager != null) { + clientManager.addCallback(getClientManagerCallback()); } } @Override protected void doStart() { - Assert.state(getTaskScheduler() != null, "A 'taskScheduler' is required"); try { - connectAndSubscribe(); + connect(); } catch (Exception ex) { - logger.error(ex, "Exception while connecting and subscribing, retrying"); - scheduleReconnect(); + logger.error(ex, "Exception while connecting"); + var applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex)); + } } } @Override protected synchronized void doStop() { - cancelReconnect(); - if (getClientManager() != null) { - unsubscribe(getClientManager().getClient()); - } - if (this.client != null) { - unsubscribe(this.client); - try { - this.client.disconnectForcibly(this.disconnectCompletionTimeout); - } - catch (MqttException ex) { - logger.error(ex, "Exception while disconnecting"); - } - - this.client.setCallback(null); - - try { - this.client.close(); - } - catch (MqttException ex) { - logger.error(ex, "Exception while closing"); - } - this.connected = false; - this.client = null; - } - } - - private void unsubscribe(IMqttAsyncClient clientInstance) { try { if (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_ALWAYS) || (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_CLEAN) && this.cleanSession)) { - clientInstance.unsubscribe(getTopic()); + this.client.unsubscribe(getTopic()); } } + catch (MqttException ex1) { + logger.error(ex1, "Exception while unsubscribing"); + } + + if (getClientManager() != null) { + return; + } + + try { + this.client.disconnectForcibly(this.disconnectCompletionTimeout); + } catch (MqttException ex) { - logger.error(ex, "Exception while unsubscribing"); + logger.error(ex, "Exception while disconnecting"); + } + } + + @Override + public void destroy() { + super.destroy(); + var clientManager = getClientManager(); + if (clientManager != null) { + clientManager.removeCallback(getClientManagerCallback()); + } + else { + try { + this.client.close(); + } + catch (MqttException e) { + logger.error(e, "Could not close client"); + } } } @@ -261,11 +239,7 @@ public void addTopic(String topic, int qos) { try { super.addTopic(topic, qos); if (this.client != null && this.client.isConnected()) { - this.client.subscribe(topic, qos); - } - var theClientManager = getClientManager(); - if (theClientManager != null) { - theClientManager.getClient().subscribe(topic, qos, new MessageListener()) + this.client.subscribe(topic, qos, new MessageListener()) .waitForCompletion(getCompletionTimeout()); } } @@ -283,11 +257,7 @@ public void removeTopic(String... topic) { this.topicLock.lock(); try { if (this.client != null && this.client.isConnected()) { - this.client.unsubscribe(topic); - } - var theClientManager = getClientManager(); - if (theClientManager != null) { - theClientManager.getClient().unsubscribe(topic).waitForCompletion(getCompletionTimeout()); + this.client.unsubscribe(topic).waitForCompletion(getCompletionTimeout()); } super.removeTopic(topic); } @@ -299,7 +269,7 @@ public void removeTopic(String... topic) { } } - private synchronized void connectAndSubscribe() throws MqttException { // NOSONAR + private synchronized void connect() throws MqttException { // NOSONAR MqttConnectOptions connectionOptions = this.clientFactory.getConnectionOptions(); this.cleanSession = connectionOptions.isCleanSession(); this.consumerStopAction = this.clientFactory.getConsumerStopAction(); @@ -307,37 +277,38 @@ private synchronized void connectAndSubscribe() throws MqttException { // NOSONA this.consumerStopAction = ConsumerStopAction.UNSUBSCRIBE_CLEAN; } - IMqttAsyncClient clientInstance; - - if (getClientManager() == null) { + long completionTimeout = getCompletionTimeout(); + var clientManager = getClientManager(); + if (clientManager == null) { Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null, "If no 'url' provided, connectionOptions.getServerURIs() must not be null"); this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); this.client.setCallback(this); - clientInstance = this.client; + this.client.connect(connectionOptions).waitForCompletion(completionTimeout); + this.client.setManualAcks(isManualAcks()); } else { - clientInstance = getClientManager().getClient(); + this.client = clientManager.getClient(); + } + } + + private void subscribe() { + var clientManager = getClientManager(); + if (clientManager != null && this.client == null) { + this.client = clientManager.getClient(); } this.topicLock.lock(); String[] topics = getTopic(); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); try { - long completionTimeout = getCompletionTimeout(); - if (getClientManager() == null) { - clientInstance.connect(connectionOptions).waitForCompletion(completionTimeout); - clientInstance.setManualAcks(isManualAcks()); - } if (topics.length > 0) { int[] requestedQos = getQos(); MessageListener[] listeners = Stream.of(topics) .map(t -> new MessageListener()) .toArray(MessageListener[]::new); - IMqttToken subscribeToken = getClientManager() == null ? - clientInstance.subscribe(topics, requestedQos) : - clientInstance.subscribe(topics, requestedQos, listeners); - subscribeToken.waitForCompletion(completionTimeout); + IMqttToken subscribeToken = this.client.subscribe(topics, requestedQos, listeners); + subscribeToken.waitForCompletion(getCompletionTimeout()); int[] grantedQos = subscribeToken.getGrantedQos(); if (grantedQos.length == 1 && grantedQos[0] == 0x80) { throw new MqttException(MqttException.REASON_CODE_SUBSCRIBE_FAILED); @@ -350,25 +321,12 @@ private synchronized void connectAndSubscribe() throws MqttException { // NOSONA if (applicationEventPublisher != null) { applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex)); } - logger.error(ex, () -> "Error connecting or subscribing to " + Arrays.toString(topics)); - if (this.client != null) { // Could be reset during event handling before - this.client.disconnectForcibly(this.disconnectCompletionTimeout); - try { - this.client.setCallback(null); - this.client.close(); - } - catch (MqttException e1) { - // NOSONAR - } - this.client = null; - } - throw ex; + logger.error(ex, () -> "Error subscribing to " + Arrays.toString(topics)); } finally { this.topicLock.unlock(); } - if (getClientManager() != null || this.client.isConnected()) { - this.connected = true; + if (this.client.isConnected()) { String message = "Connected and subscribed to " + Arrays.toString(topics); logger.debug(message); if (applicationEventPublisher != null) { @@ -388,60 +346,10 @@ private void warnInvalidQosForSubscription(String[] topics, int[] requestedQos, } } - private synchronized void cancelReconnect() { - if (this.reconnectFuture != null) { - this.reconnectFuture.cancel(false); - this.reconnectFuture = null; - } - } - - private synchronized void scheduleReconnect() { - if (getClientManager() != null) { - return; - } - - cancelReconnect(); - if (isActive()) { - try { - this.reconnectFuture = getTaskScheduler() - .schedule(() -> { - try { - logger.debug("Attempting reconnect"); - synchronized (MqttPahoMessageDrivenChannelAdapter.this) { - if (!MqttPahoMessageDrivenChannelAdapter.this.connected) { - connectAndSubscribe(); - MqttPahoMessageDrivenChannelAdapter.this.reconnectFuture = null; - } - } - } - catch (MqttException ex) { - logger.error(ex, "Exception while connecting and subscribing"); - scheduleReconnect(); - } - }, Instant.now().plusMillis(this.recoveryInterval)); - } - catch (Exception ex) { - logger.error(ex, "Failed to schedule reconnect"); - } - } - } - @Override public synchronized void connectionLost(Throwable cause) { if (isRunning()) { - this.logger.error(() -> "Lost connection: " + cause.getMessage() + "; retrying..."); - this.connected = false; - if (this.client != null) { - try { - this.client.setCallback(null); - this.client.close(); - } - catch (MqttException e) { - // NOSONAR - } - } - this.client = null; - scheduleReconnect(); + this.logger.error(() -> "Lost connection: " + cause.getMessage()); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); if (applicationEventPublisher != null) { applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, cause)); @@ -454,9 +362,8 @@ public void messageArrived(String topic, MqttMessage mqttMessage) { AbstractIntegrationMessageBuilder builder = toMessageBuilder(topic, mqttMessage); if (builder != null) { if (isManualAcks()) { - var theClient = this.client != null ? this.client : getClientManager().getClient(); builder.setHeader(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, - new AcknowledgmentImpl(mqttMessage.getId(), mqttMessage.getQos(), theClient)); + new AcknowledgmentImpl(mqttMessage.getId(), mqttMessage.getQos(), this.client)); } Message message = builder.build(); try { @@ -504,6 +411,13 @@ private AbstractIntegrationMessageBuilder toMessageBuilder(String topic, Mqtt public void deliveryComplete(IMqttDeliveryToken token) { } + @Override + public void connectComplete(boolean reconnect, String serverURI) { + if (!reconnect) { + subscribe(); + } + } + /** * Used to complete message arrival when {@link #isManualAcks()} is true. * @@ -548,6 +462,9 @@ public void acknowledge() { private class MessageListener implements IMqttMessageListener { + MessageListener() { + } + @Override public void messageArrived(String topic, MqttMessage mqttMessage) { MqttPahoMessageDrivenChannelAdapter.this.messageArrived(topic, mqttMessage); @@ -555,4 +472,16 @@ public void messageArrived(String topic, MqttMessage mqttMessage) { } + private class AdapterConnectCallback implements ClientManager.ConnectCallback { + + AdapterConnectCallback() { + } + + @Override + public void connectComplete(boolean isReconnect) { + MqttPahoMessageDrivenChannelAdapter.this.connectComplete(isReconnect, null); + } + + } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 0ef1ced4222..682fc46aebb 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -76,7 +76,8 @@ * @since 5.5.5 * */ -public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter +public class Mqttv5PahoMessageDrivenChannelAdapter + extends AbstractMqttMessageDrivenChannelAdapter implements MqttCallback, MqttComponent { private final MqttConnectionOptions connectionOptions; @@ -114,28 +115,13 @@ public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOpt * (for instance, to reuse an MQTT connection). * @param clientManager The client manager. * @param topic The topic(s). + * @since 6.0 */ - public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager clientManager, String... topic) { - this(buildDefaultConnectionOptions(null), clientManager, topic); - } - - /** - * Use this constructor when you need to use a single {@link ClientManager} - * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectionOptions}. - * @param connectionOptions The connection options. - * @param clientManager The client manager. - * @param topic The topic(s). - */ - public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOptions, - ClientManager clientManager, String... topic) { - + public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager clientManager, + String... topic) { super(clientManager, topic); - this.connectionOptions = connectionOptions; - if (!this.connectionOptions.isAutomaticReconnect()) { - logger.warn("It is recommended to set 'automaticReconnect' MQTT client option. " + - "Otherwise the current channel adapter restart should be used explicitly, " + - "e.g. via handling 'MqttConnectionFailedEvent' on client disconnection."); - } + this.connectionOptions = clientManager.getConnectionInfo(); + setClientManagerCallback(new AdapterConnectCallback()); } private static MqttConnectionOptions buildDefaultConnectionOptions(@Nullable String url) { @@ -185,7 +171,8 @@ public void setHeaderMapper(HeaderMapper headerMapper) { @Override protected void onInit() { super.onInit(); - if (getClientManager() == null && this.mqttClient == null) { + var clientManager = getClientManager(); + if (clientManager == null && this.mqttClient == null) { try { this.mqttClient = new MqttAsyncClient(getUrl(), getClientId(), this.persistence); this.mqttClient.setCallback(this); @@ -200,35 +187,29 @@ protected void onInit() { .getBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME, SmartMessageConverter.class)); } + if (clientManager != null) { + clientManager.addCallback(getClientManagerCallback()); + } } @Override protected void doStart() { - if (getClientManager() != null) { - subscribeToAll(); - return; - } - ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); - try { - this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); - } - catch (MqttException ex) { - if (this.connectionOptions.isAutomaticReconnect()) { - try { - this.mqttClient.reconnect(); - } - catch (MqttException e) { - logger.error(ex, "MQTT client failed to connect. Never happens."); - } + var clientManager = getClientManager(); + if (clientManager == null) { + try { + this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); } - else { + catch (MqttException ex) { if (applicationEventPublisher != null) { applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex)); } logger.error(ex, "MQTT client failed to connect."); } } + else { + this.mqttClient = clientManager.getClient(); + } } @Override @@ -236,13 +217,12 @@ protected void doStop() { this.topicLock.lock(); String[] topics = getTopic(); try { - var theClientManager = getClientManager(); - if (theClientManager != null) { - theClientManager.getClient().unsubscribe(topics).waitForCompletion(getCompletionTimeout()); - } if (this.mqttClient != null && this.mqttClient.isConnected()) { this.mqttClient.unsubscribe(topics).waitForCompletion(getCompletionTimeout()); - this.mqttClient.disconnect().waitForCompletion(getCompletionTimeout()); + + if (getClientManager() == null) { + this.mqttClient.disconnect().waitForCompletion(getCompletionTimeout()); + } } } catch (MqttException ex) { @@ -256,14 +236,18 @@ protected void doStop() { @Override public void destroy() { super.destroy(); + var clientManager = getClientManager(); try { - if (this.mqttClient != null) { + if (clientManager == null && this.mqttClient != null) { this.mqttClient.close(true); } } catch (MqttException ex) { logger.error(ex, "Failed to close 'MqttAsyncClient'"); } + if (clientManager != null) { + clientManager.removeCallback(getClientManagerCallback()); + } } @Override @@ -272,11 +256,7 @@ public void addTopic(String topic, int qos) { try { super.addTopic(topic, qos); if (this.mqttClient != null && this.mqttClient.isConnected()) { - this.mqttClient.subscribe(topic, qos).waitForCompletion(getCompletionTimeout()); - } - var theClientManager = getClientManager(); - if (theClientManager != null) { - theClientManager.getClient().subscribe(new MqttSubscription(topic, qos), new MessageListener()) + this.mqttClient.subscribe(new MqttSubscription(topic, qos), new MessageListener()) .waitForCompletion(getCompletionTimeout()); } } @@ -295,10 +275,6 @@ public void removeTopic(String... topic) { if (this.mqttClient != null && this.mqttClient.isConnected()) { this.mqttClient.unsubscribe(topic).waitForCompletion(getCompletionTimeout()); } - var theClientManager = getClientManager(); - if (theClientManager != null) { - theClientManager.getClient().unsubscribe(topic).waitForCompletion(getCompletionTimeout()); - } super.removeTopic(topic); } catch (MqttException ex) { @@ -370,19 +346,16 @@ public void deliveryComplete(IMqttToken token) { @Override public void connectComplete(boolean reconnect, String serverURI) { - if (!reconnect) { - subscribeToAll(); + if (reconnect) { + return; + } + var clientManager = getClientManager(); + if (clientManager != null && this.mqttClient == null) { + this.mqttClient = clientManager.getClient(); } - } - - @Override - public void authPacketArrived(int reasonCode, MqttProperties properties) { - - } - private void subscribeToAll() { - ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); String[] topics = getTopic(); + ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); this.topicLock.lock(); try { if (topics.length == 0) { @@ -390,19 +363,14 @@ private void subscribeToAll() { } int[] requestedQos = getQos(); - if (this.mqttClient != null) { - this.mqttClient.subscribe(topics, requestedQos).waitForCompletion(getCompletionTimeout()); - } - if (getClientManager() != null) { - MqttSubscription[] subscriptions = IntStream.range(0, topics.length) - .mapToObj(i -> new MqttSubscription(topics[i], requestedQos[i])) - .toArray(MqttSubscription[]::new); - MessageListener[] listeners = IntStream.range(0, topics.length) - .mapToObj(t -> new MessageListener()) - .toArray(MessageListener[]::new); - getClientManager().getClient().subscribe(subscriptions, null, null, listeners, null) - .waitForCompletion(getCompletionTimeout()); - } + MqttSubscription[] subscriptions = IntStream.range(0, topics.length) + .mapToObj(i -> new MqttSubscription(topics[i], requestedQos[i])) + .toArray(MqttSubscription[]::new); + MessageListener[] listeners = IntStream.range(0, topics.length) + .mapToObj(t -> new MessageListener()) + .toArray(MessageListener[]::new); + this.mqttClient.subscribe(subscriptions, null, null, listeners, null) + .waitForCompletion(getCompletionTimeout()); String message = "Connected and subscribed to " + Arrays.toString(topics); logger.debug(message); if (applicationEventPublisher != null) { @@ -420,6 +388,11 @@ private void subscribeToAll() { } } + @Override + public void authPacketArrived(int reasonCode, MqttProperties properties) { + + } + private static String obtainServerUrlFromOptions(MqttConnectionOptions connectionOptions) { Assert.notNull(connectionOptions, "'connectionOptions' must not be null"); String[] serverURIs = connectionOptions.getServerURIs(); @@ -465,6 +438,9 @@ public void acknowledge() { private class MessageListener implements IMqttMessageListener { + MessageListener() { + } + @Override public void messageArrived(String topic, MqttMessage message) { Mqttv5PahoMessageDrivenChannelAdapter.this.messageArrived(topic, message); @@ -472,4 +448,16 @@ public void messageArrived(String topic, MqttMessage message) { } + private class AdapterConnectCallback implements ClientManager.ConnectCallback { + + AdapterConnectCallback() { + } + + @Override + public void connectComplete(boolean isReconnect) { + Mqttv5PahoMessageDrivenChannelAdapter.this.connectComplete(isReconnect, null); + } + + } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java index 6da4f814f37..387db0497fe 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java @@ -38,6 +38,7 @@ * Abstract class for MQTT outbound channel adapters. * * @param MQTT Client type + * @param MQTT connection options type (v5 or v3) * * @author Gary Russell * @author Artem Bilan @@ -46,7 +47,7 @@ * @since 4.0 * */ -public abstract class AbstractMqttMessageHandler extends AbstractMessageHandler +public abstract class AbstractMqttMessageHandler extends AbstractMessageHandler implements ManageableLifecycle, ApplicationEventPublisherAware { /** @@ -68,6 +69,8 @@ public abstract class AbstractMqttMessageHandler extends AbstractMessageHandl private final String clientId; + private final ClientManager clientManager; + private long completionTimeout = DEFAULT_COMPLETION_TIMEOUT; private long disconnectCompletionTimeout = DISCONNECT_COMPLETION_TIMEOUT; @@ -90,8 +93,6 @@ public abstract class AbstractMqttMessageHandler extends AbstractMessageHandl private int clientInstance; - private final ClientManager clientManager; - public AbstractMqttMessageHandler(@Nullable String url, String clientId) { Assert.hasText(clientId, "'clientId' cannot be null or empty"); this.url = url; @@ -99,7 +100,7 @@ public AbstractMqttMessageHandler(@Nullable String url, String clientId) { this.clientManager = null; } - AbstractMqttMessageHandler(ClientManager clientManager) { + public AbstractMqttMessageHandler(ClientManager clientManager) { Assert.notNull(clientManager, "'clientManager' cannot be null or empty"); this.clientManager = clientManager; this.url = null; @@ -308,7 +309,7 @@ protected long getDisconnectCompletionTimeout() { } @Nullable - protected ClientManager getClientManager() { + protected ClientManager getClientManager() { return this.clientManager; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java index 14edad76452..4fa3802b996 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java @@ -54,7 +54,7 @@ * @since 4.0 * */ -public class MqttPahoMessageHandler extends AbstractMqttMessageHandler +public class MqttPahoMessageHandler extends AbstractMqttMessageHandler implements MqttCallback, MqttPahoComponent { private final MqttPahoClientFactory clientFactory; @@ -103,21 +103,12 @@ public MqttPahoMessageHandler(String clientId, MqttPahoClientFactory clientFacto * Use this constructor when you need to use a single {@link ClientManager} * (for instance, to reuse an MQTT connection). * @param clientManager The client manager. + * @since 6.0 */ - public MqttPahoMessageHandler(ClientManager clientManager) { - this(clientManager, new MqttConnectOptions()); - } - - /** - * Use this constructor when you need to use a single {@link ClientManager} - * (for instance, to reuse an MQTT connection) and a specific {@link MqttConnectOptions}. - * @param clientManager The client manager. - * @param connectOptions The connection options. - */ - public MqttPahoMessageHandler(ClientManager clientManager, MqttConnectOptions connectOptions) { + public MqttPahoMessageHandler(ClientManager clientManager) { super(clientManager); var factory = new DefaultMqttPahoClientFactory(); - factory.setConnectionOptions(connectOptions); + factory.setConnectionOptions(clientManager.getConnectionInfo()); this.clientFactory = factory; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index cc832ad5c0b..998160eefc2 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -57,7 +57,7 @@ * * @since 5.5.5 */ -public class Mqttv5PahoMessageHandler extends AbstractMqttMessageHandler +public class Mqttv5PahoMessageHandler extends AbstractMqttMessageHandler implements MqttCallback, MqttComponent { private final MqttConnectionOptions connectionOptions; @@ -83,14 +83,15 @@ public Mqttv5PahoMessageHandler(MqttConnectionOptions connectionOptions, String this.connectionOptions = connectionOptions; } - public Mqttv5PahoMessageHandler(ClientManager clientManager) { - this(clientManager, buildDefaultConnectionOptions(null)); - } - - public Mqttv5PahoMessageHandler(ClientManager clientManager, - MqttConnectionOptions connectionOptions) { + /** + * Use this constructor when you need to use a single {@link ClientManager} + * (for instance, to reuse an MQTT connection). + * @param clientManager The client manager. + * @since 6.0 + */ + public Mqttv5PahoMessageHandler(ClientManager clientManager) { super(clientManager); - this.connectionOptions = connectionOptions; + this.connectionOptions = clientManager.getConnectionInfo(); } private static MqttConnectionOptions buildDefaultConnectionOptions(@Nullable String url) { diff --git a/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/spring-integration-mqtt.xsd b/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/spring-integration-mqtt.xsd index 2a31b8c9f07..3d18126d39f 100644 --- a/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/spring-integration-mqtt.xsd +++ b/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/spring-integration-mqtt.xsd @@ -66,15 +66,6 @@ - - - - - - diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java index 427d78b28f3..bd8bfaf7e63 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; @@ -54,31 +56,36 @@ class ClientManagerBackToBackTests implements MosquittoContainerTest { @Test - void testSameV3ClientIdWorksForPubAndSub() { - testSubscribeAndPublish(Mqttv3Config.class, Mqttv3Config.TOPIC_NAME); + void testSameV3ClientIdWorksForPubAndSub() throws Exception { + testSubscribeAndPublish(Mqttv3Config.class, Mqttv3Config.TOPIC_NAME, Mqttv3Config.subscribedLatch); } @Test - void testSameV5ClientIdWorksForPubAndSub() { - testSubscribeAndPublish(Mqttv5Config.class, Mqttv5Config.TOPIC_NAME); + void testSameV5ClientIdWorksForPubAndSub() throws Exception { + testSubscribeAndPublish(Mqttv5Config.class, Mqttv5Config.TOPIC_NAME, Mqttv5Config.subscribedLatch); } @Test - void testV3ClientManagerReconnect() { - testSubscribeAndPublish(Mqttv3ConfigWithDisconnect.class, Mqttv3ConfigWithDisconnect.TOPIC_NAME); + void testV3ClientManagerReconnect() throws Exception { + testSubscribeAndPublish(Mqttv3ConfigWithDisconnect.class, Mqttv3ConfigWithDisconnect.TOPIC_NAME, + Mqttv3ConfigWithDisconnect.subscribedLatch); } @Test - void testV5ClientManagerReconnect() { - testSubscribeAndPublish(Mqttv5ConfigWithDisconnect.class, Mqttv5ConfigWithDisconnect.TOPIC_NAME); + void testV5ClientManagerReconnect() throws Exception { + testSubscribeAndPublish(Mqttv5ConfigWithDisconnect.class, Mqttv5ConfigWithDisconnect.TOPIC_NAME, + Mqttv5ConfigWithDisconnect.subscribedLatch); } - private void testSubscribeAndPublish(Class configClass, String topicName) { + private void testSubscribeAndPublish(Class configClass, String topicName, CountDownLatch subscribedLatch) + throws Exception { + try (var ctx = new AnnotationConfigApplicationContext(configClass)) { // given var input = ctx.getBean("mqttOutFlow.input", MessageChannel.class); var output = ctx.getBean("fromMqttChannel", PollableChannel.class); String testPayload = "foo"; + assertThat(subscribedLatch.await(20, TimeUnit.SECONDS)).isTrue(); // when input.send(MessageBuilder.withPayload(testPayload).setHeader(MqttHeaders.TOPIC, topicName).build()); @@ -102,6 +109,13 @@ public static class Mqttv3Config { static final String TOPIC_NAME = "test-topic-v3"; + static final CountDownLatch subscribedLatch = new CountDownLatch(1); + + @EventListener + public void onSubscribed(MqttSubscribedEvent e) { + subscribedLatch.countDown(); + } + @Bean public Mqttv3ClientManager mqttv3ClientManager(MqttPahoClientFactory pahoClientFactory) { return new Mqttv3ClientManager(pahoClientFactory, "client-manager-client-id-v3"); @@ -137,9 +151,16 @@ public static class Mqttv3ConfigWithDisconnect { static final String TOPIC_NAME = "test-topic-v3-reconnect"; + static final CountDownLatch subscribedLatch = new CountDownLatch(1); + + @EventListener + public void onSubscribed(MqttSubscribedEvent e) { + subscribedLatch.countDown(); + } + @Bean - public ClientV3Disconnector disconnector(Mqttv3ClientManager mqttv3ClientManager) { - return new ClientV3Disconnector(mqttv3ClientManager); + public ClientV3Disconnector disconnector(Mqttv3ClientManager clientManager) { + return new ClientV3Disconnector(clientManager); } @Bean @@ -177,6 +198,13 @@ public static class Mqttv5Config { static final String TOPIC_NAME = "test-topic-v5"; + static final CountDownLatch subscribedLatch = new CountDownLatch(1); + + @EventListener + public void onSubscribed(MqttSubscribedEvent e) { + subscribedLatch.countDown(); + } + @Bean public Mqttv5ClientManager mqttv5ClientManager() { return new Mqttv5ClientManager(MosquittoContainerTest.mqttUrl(), "client-manager-client-id-v5"); @@ -202,9 +230,16 @@ public static class Mqttv5ConfigWithDisconnect { static final String TOPIC_NAME = "test-topic-v5-reconnect"; + static final CountDownLatch subscribedLatch = new CountDownLatch(1); + + @EventListener + public void onSubscribed(MqttSubscribedEvent e) { + subscribedLatch.countDown(); + } + @Bean - public ClientV5Disconnector disconnector(Mqttv5ClientManager mqttv5ClientManager) { - return new ClientV5Disconnector(mqttv5ClientManager); + public ClientV5Disconnector clientV3Disconnector(Mqttv5ClientManager clientManager) { + return new ClientV5Disconnector(clientManager); } @Bean @@ -226,6 +261,7 @@ public IntegrationFlow mqttInFlow(Mqttv5ClientManager mqttv5ClientManager) { } + public static class ClientV3Disconnector { private final Mqttv3ClientManager clientManager; diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java index b4cc065c429..138c81ddd0e 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java @@ -17,7 +17,6 @@ package org.springframework.integration.mqtt; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -33,28 +32,24 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.time.Instant; import java.util.Properties; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import javax.net.SocketFactory; import org.aopalliance.intercept.MethodInterceptor; -import org.assertj.core.api.Condition; import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttToken; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttException; @@ -62,7 +57,6 @@ import org.eclipse.paho.client.mqttv3.MqttToken; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.springframework.aop.framework.ProxyFactoryBean; @@ -95,8 +89,6 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.GenericMessage; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.ReflectionUtils; /** @@ -118,16 +110,6 @@ public class MqttAdapterTests { this.alwaysComplete = (IMqttToken) pfb.getObject(); } - @Test - public void testCloseOnBadConnectIn() throws Exception { - final IMqttAsyncClient client = mock(IMqttAsyncClient.class); - MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); - willThrow(new MqttException(0)).given(client).connect(any()); - adapter.start(); - verify(client).close(); - adapter.stop(); - } - @Test public void testCloseOnBadConnectOut() throws Exception { final IMqttAsyncClient client = mock(IMqttAsyncClient.class); @@ -189,7 +171,7 @@ public void testOutboundOptionsApplied() throws Exception { connectCalled.set(true); return token; }).given(client).connect(any(MqttConnectOptions.class)); - willReturn(token).given(client).subscribe(any(String[].class), any(int[].class)); + willReturn(token).given(client).subscribe(any(String[].class), any(int[].class), any()); final MqttDeliveryToken deliveryToken = mock(MqttDeliveryToken.class); final AtomicBoolean publishCalled = new AtomicBoolean(); @@ -216,6 +198,7 @@ public void testOutboundOptionsApplied() throws Exception { void testClientManagerIsNotConnectedAndClosedInHandler() throws Exception { // given var clientManager = mock(Mqttv3ClientManager.class); + when(clientManager.getConnectionInfo()).thenReturn(new MqttConnectOptions()); var client = mock(MqttAsyncClient.class); given(clientManager.getClient()).willReturn(client); @@ -230,7 +213,6 @@ void testClientManagerIsNotConnectedAndClosedInHandler() throws Exception { // when handler.handleMessage(new GenericMessage<>("Hello, world!")); - handler.connectionLost(new IllegalStateException()); handler.stop(); // then @@ -245,6 +227,7 @@ void testClientManagerIsNotConnectedAndClosedInHandler() throws Exception { void testClientManagerIsNotConnectedAndClosedInAdapter() throws Exception { // given var clientManager = mock(Mqttv3ClientManager.class); + when(clientManager.getConnectionInfo()).thenReturn(new MqttConnectOptions()); var client = mock(MqttAsyncClient.class); given(clientManager.getClient()).willReturn(client); @@ -254,15 +237,12 @@ void testClientManagerIsNotConnectedAndClosedInAdapter() throws Exception { .willReturn(subscribeToken); var adapter = new MqttPahoMessageDrivenChannelAdapter(clientManager, "mqtt-foo"); - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - taskScheduler.initialize(); - adapter.setTaskScheduler(taskScheduler); adapter.setBeanFactory(mock(BeanFactory.class)); adapter.afterPropertiesSet(); // when adapter.start(); - adapter.connectionLost(new IllegalStateException()); + adapter.connectComplete(false, null); adapter.stop(); // then @@ -297,18 +277,8 @@ public void testInboundOptionsApplied() throws Exception { willReturn(client).given(factory).getAsyncClientInstance(anyString(), anyString()); final AtomicBoolean connectCalled = new AtomicBoolean(); - final AtomicBoolean failConnection = new AtomicBoolean(); - final CountDownLatch waitToFail = new CountDownLatch(1); - final CountDownLatch failInProcess = new CountDownLatch(1); - final CountDownLatch goodConnection = new CountDownLatch(2); - final MqttException reconnectException = new MqttException(MqttException.REASON_CODE_SERVER_CONNECT_ERROR); IMqttToken token = mock(IMqttToken.class); willAnswer(invocation -> { - if (failConnection.get()) { - failInProcess.countDown(); - waitToFail.await(10, TimeUnit.SECONDS); - throw reconnectException; - } MqttConnectOptions options = invocation.getArgument(0); assertThat(options.getConnectionTimeout()).isEqualTo(23); assertThat(options.getKeepAliveInterval()).isEqualTo(45); @@ -320,17 +290,16 @@ public void testInboundOptionsApplied() throws Exception { assertThat(new String(options.getWillMessage().getPayload())).isEqualTo("bar"); assertThat(options.getWillMessage().getQos()).isEqualTo(2); connectCalled.set(true); - goodConnection.countDown(); return token; }).given(client).connect(any(MqttConnectOptions.class)); - given(client.subscribe(any(String[].class), any(int[].class))).willReturn(token); + given(client.subscribe(any(String[].class), any(int[].class), any())).willReturn(token); given(token.getGrantedQos()).willReturn(new int[]{ 2 }); - final AtomicReference callback = new AtomicReference<>(); + final AtomicReference callback = new AtomicReference<>(); willAnswer(invocation -> { callback.set(invocation.getArgument(0)); return null; - }).given(client).setCallback(any(MqttCallback.class)); + }).given(client).setCallback(any(MqttCallbackExtended.class)); given(client.isConnected()).willReturn(true); @@ -341,9 +310,6 @@ public void testInboundOptionsApplied() throws Exception { adapter.setOutputChannel(outputChannel); QueueChannel errorChannel = new QueueChannel(); adapter.setErrorChannel(errorChannel); - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - taskScheduler.initialize(); - adapter.setTaskScheduler(taskScheduler); adapter.setBeanFactory(mock(BeanFactory.class)); ApplicationEventPublisher applicationEventPublisher = mock(ApplicationEventPublisher.class); final BlockingQueue events = new LinkedBlockingQueue<>(); @@ -352,9 +318,9 @@ public void testInboundOptionsApplied() throws Exception { return null; }).given(applicationEventPublisher).publishEvent(any(MqttIntegrationEvent.class)); adapter.setApplicationEventPublisher(applicationEventPublisher); - adapter.setRecoveryInterval(500); adapter.afterPropertiesSet(); adapter.start(); + adapter.connectComplete(false, null); verify(client, times(1)).connect(any(MqttConnectOptions.class)); assertThat(connectCalled.get()).isTrue(); @@ -402,32 +368,6 @@ public Message toMessage(Object payload, MessageHeaders headers) { IllegalStateException exception = (IllegalStateException) errorMessage.getPayload(); assertThat(exception).hasMessage("'MqttMessageConverter' returned 'null'"); assertThat(errorMessage.getOriginalMessage().getPayload()).isSameAs(message); - - // lose connection and make first reconnect fail - failConnection.set(true); - RuntimeException e = new RuntimeException("foo"); - adapter.connectionLost(e); - - event = events.poll(10, TimeUnit.SECONDS); - assertThat(event).isInstanceOf(MqttConnectionFailedEvent.class); - assertThat(e).isSameAs(event.getCause()); - - assertThat(failInProcess.await(10, TimeUnit.SECONDS)).isTrue(); - waitToFail.countDown(); - failConnection.set(false); - event = events.poll(10, TimeUnit.SECONDS); - assertThat(event).isInstanceOf(MqttConnectionFailedEvent.class); - assertThat(reconnectException).isSameAs(event.getCause()); - - // reconnect can now succeed; however, we might have other failures on a slow server (500ms retry). - assertThat(goodConnection.await(10, TimeUnit.SECONDS)).isTrue(); - int n = 0; - while (!(event instanceof MqttSubscribedEvent) && n++ < 20) { - event = events.poll(10, TimeUnit.SECONDS); - } - assertThat(event).isInstanceOf(MqttSubscribedEvent.class); - assertThat(((MqttSubscribedEvent) event).getMessage()).isEqualTo("Connected and subscribed to [baz, fix]"); - taskScheduler.destroy(); } @Test @@ -436,6 +376,7 @@ public void testStopActionDefault() throws Exception { MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, null); adapter.start(); + adapter.connectComplete(false, null); adapter.stop(); verifyUnsubscribe(client); } @@ -446,6 +387,7 @@ public void testStopActionDefaultNotClean() throws Exception { MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, false, null); adapter.start(); + adapter.connectComplete(false, null); adapter.stop(); verifyNotUnsubscribe(client); } @@ -457,15 +399,9 @@ public void testStopActionAlways() throws Exception { ConsumerStopAction.UNSUBSCRIBE_ALWAYS); adapter.start(); + adapter.connectComplete(false, null); adapter.stop(); verifyUnsubscribe(client); - - adapter.connectionLost(new RuntimeException("Intentional")); - - TaskScheduler taskScheduler = TestUtils.getPropertyValue(adapter, "taskScheduler", TaskScheduler.class); - - verify(taskScheduler, never()) - .schedule(any(Runnable.class), any(Instant.class)); } @Test @@ -474,6 +410,7 @@ public void testStopActionNever() throws Exception { MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); adapter.start(); + adapter.connectComplete(false, null); adapter.stop(); verifyNotUnsubscribe(client); } @@ -499,39 +436,6 @@ public void testCustomExpressions() { ctx.close(); } - @Test - public void testReconnect() throws Exception { - final IMqttAsyncClient client = mock(IMqttAsyncClient.class); - MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); - adapter.setRecoveryInterval(10); - LogAccessor logger = spy(TestUtils.getPropertyValue(adapter, "logger", LogAccessor.class)); - new DirectFieldAccessor(adapter).setPropertyValue("logger", logger); - given(logger.isDebugEnabled()).willReturn(true); - final AtomicInteger attemptingReconnectCount = new AtomicInteger(); - willAnswer(i -> { - if (attemptingReconnectCount.getAndIncrement() == 0) { - adapter.connectionLost(new RuntimeException("while schedule running")); - } - i.callRealMethod(); - return null; - }).given(logger).debug("Attempting reconnect"); - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - taskScheduler.initialize(); - adapter.setTaskScheduler(taskScheduler); - adapter.start(); - adapter.connectionLost(new RuntimeException("initial")); - verify(client).close(); - Thread.sleep(1000); - // the following assertion should be equalTo, but leq to protect against a slow CI server - assertThat(attemptingReconnectCount.get()).isLessThanOrEqualTo(2); - AtomicReference failed = new AtomicReference<>(); - adapter.setApplicationEventPublisher(failed::set); - adapter.connectionLost(new IllegalStateException()); - assertThat(failed.get()).isInstanceOf(MqttConnectionFailedEvent.class); - adapter.stop(); - taskScheduler.destroy(); - } - @Test public void testSubscribeFailure() throws Exception { DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); @@ -557,7 +461,7 @@ public void testSubscribeFailure() throws Exception { IMqttToken token = mock(IMqttToken.class); given(token.getGrantedQos()).willReturn(new int[]{ 0x80 }); - willReturn(token).given(client).subscribe(any(String[].class), any(int[].class)); + willReturn(token).given(client).subscribe(any(String[].class), any(int[].class), any()); MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", factory, "baz", "fix"); @@ -565,14 +469,18 @@ public void testSubscribeFailure() throws Exception { ReflectionUtils.doWithMethods(MqttPahoMessageDrivenChannelAdapter.class, m -> { m.setAccessible(true); method.set(m); - }, m -> m.getName().equals("connectAndSubscribe")); + }, m -> m.getName().equals("connect")); + assertThat(method.get()).isNotNull(); + method.get().invoke(adapter); + ReflectionUtils.doWithMethods(MqttPahoMessageDrivenChannelAdapter.class, m -> { + m.setAccessible(true); + method.set(m); + }, m -> m.getName().equals("subscribe")); assertThat(method.get()).isNotNull(); - Condition subscribeFailed = new Condition<>(ex -> - ((MqttException) ex.getCause()).getReasonCode() == MqttException.REASON_CODE_SUBSCRIBE_FAILED, - "expected the reason code to be REASON_CODE_SUBSCRIBE_FAILED"); - assertThatExceptionOfType(InvocationTargetException.class).isThrownBy(() -> method.get().invoke(adapter)) - .withCauseInstanceOf(MqttException.class) - .is(subscribeFailed); + ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); + adapter.setApplicationEventPublisher(eventPublisher); + method.get().invoke(adapter); + verify(eventPublisher).publishEvent(any(MqttConnectionFailedEvent.class)); } @Test @@ -600,7 +508,7 @@ public void testDifferentQos() throws Exception { IMqttToken token = mock(IMqttToken.class); given(token.getGrantedQos()).willReturn(new int[]{ 2, 0 }); - willReturn(token).given(client).subscribe(any(String[].class), any(int[].class)); + willReturn(token).given(client).subscribe(any(String[].class), any(int[].class), any()); MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", factory, "baz", "fix"); @@ -608,7 +516,13 @@ public void testDifferentQos() throws Exception { ReflectionUtils.doWithMethods(MqttPahoMessageDrivenChannelAdapter.class, m -> { m.setAccessible(true); method.set(m); - }, m -> m.getName().equals("connectAndSubscribe")); + }, m -> m.getName().equals("connect")); + assertThat(method.get()).isNotNull(); + method.get().invoke(adapter); + ReflectionUtils.doWithMethods(MqttPahoMessageDrivenChannelAdapter.class, m -> { + m.setAccessible(true); + method.set(m); + }, m -> m.getName().equals("subscribe")); assertThat(method.get()).isNotNull(); LogAccessor logger = spy(TestUtils.getPropertyValue(adapter, "logger", LogAccessor.class)); new DirectFieldAccessor(adapter).setPropertyValue("logger", logger); @@ -625,48 +539,6 @@ public void testDifferentQos() throws Exception { verify(client).disconnectForcibly(5_000L); } - @Test - public void testNoNPEOnReconnectAndStopRaceCondition() throws Exception { - final IMqttAsyncClient client = mock(IMqttAsyncClient.class); - MqttPahoMessageDrivenChannelAdapter adapter = buildAdapterIn(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER); - adapter.setRecoveryInterval(10); - - MqttException mqttException = new MqttException(MqttException.REASON_CODE_SUBSCRIBE_FAILED); - - willThrow(mqttException) - .given(client) - .subscribe(any(), any()); - - LogAccessor logger = spy(TestUtils.getPropertyValue(adapter, "logger", LogAccessor.class)); - new DirectFieldAccessor(adapter).setPropertyValue("logger", logger); - CountDownLatch exceptionLatch = new CountDownLatch(1); - ArgumentCaptor mqttExceptionArgumentCaptor = ArgumentCaptor.forClass(MqttException.class); - willAnswer(i -> { - exceptionLatch.countDown(); - return null; - }) - .given(logger) - .error(mqttExceptionArgumentCaptor.capture(), eq("Exception while connecting and subscribing")); - - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - taskScheduler.initialize(); - adapter.setTaskScheduler(taskScheduler); - - adapter.setApplicationEventPublisher(event -> { - if (event instanceof MqttConnectionFailedEvent) { - adapter.destroy(); - } - }); - adapter.start(); - - assertThat(exceptionLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(mqttExceptionArgumentCaptor.getValue()) - .isNotNull() - .isSameAs(mqttException); - - taskScheduler.destroy(); - } - private MqttPahoMessageDrivenChannelAdapter buildAdapterIn(final IMqttAsyncClient client, Boolean cleanSession, ConsumerStopAction action) throws MqttException { @@ -690,12 +562,11 @@ public IMqttAsyncClient getAsyncClientInstance(String uri, String clientId) { given(client.isConnected()).willReturn(true); IMqttToken token = mock(IMqttToken.class); given(client.connect(any(MqttConnectOptions.class))).willReturn(token); - given(client.subscribe(any(String[].class), any(int[].class))).willReturn(token); + given(client.subscribe(any(String[].class), any(int[].class), any())).willReturn(token); given(token.getGrantedQos()).willReturn(new int[]{ 2 }); MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("client", factory, "foo"); adapter.setApplicationEventPublisher(mock(ApplicationEventPublisher.class)); adapter.setOutputChannel(new NullChannel()); - adapter.setTaskScheduler(mock(TaskScheduler.class)); adapter.afterPropertiesSet(); return adapter; } @@ -721,14 +592,14 @@ public IMqttAsyncClient getAsyncClientInstance(String uri, String clientId) { private void verifyUnsubscribe(IMqttAsyncClient client) throws Exception { verify(client).connect(any(MqttConnectOptions.class)); - verify(client).subscribe(any(String[].class), any(int[].class)); + verify(client).subscribe(any(String[].class), any(int[].class), any()); verify(client).unsubscribe(any(String[].class)); verify(client).disconnectForcibly(anyLong()); } private void verifyNotUnsubscribe(IMqttAsyncClient client) throws Exception { verify(client).connect(any(MqttConnectOptions.class)); - verify(client).subscribe(any(String[].class), any(int[].class)); + verify(client).subscribe(any(String[].class), any(int[].class), any()); verify(client, never()).unsubscribe(any(String[].class)); verify(client).disconnectForcibly(anyLong()); } diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackAutomaticReconnectTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackAutomaticReconnectTests.java index 266eae5bb01..e4d8253a13b 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackAutomaticReconnectTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackAutomaticReconnectTests.java @@ -17,17 +17,16 @@ package org.springframework.integration.mqtt; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import java.net.UnknownHostException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.eclipse.paho.mqttv5.client.IMqttAsyncClient; import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder; -import org.eclipse.paho.mqttv5.common.MqttException; import org.junit.jupiter.api.Test; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -42,7 +41,6 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.PollableChannel; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -73,23 +71,13 @@ public class Mqttv5BackToBackAutomaticReconnectTests implements MosquittoContain @Test public void testReconnectionWhenFirstConnectionFails() throws InterruptedException { - Message testMessage = - MessageBuilder.withPayload("testPayload") - .setHeader(MqttHeaders.TOPIC, "siTest") - .build(); - - assertThatExceptionOfType(MessageHandlingException.class) - .isThrownBy(() -> this.mqttOutFlowInput.send(testMessage)) - .withCauseExactlyInstanceOf(MqttException.class) - .withRootCauseExactlyInstanceOf(UnknownHostException.class); - - connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); - assertThat(this.config.subscribeLatch.await(10, TimeUnit.SECONDS)).isTrue(); - this.mqttOutFlowInput.send(testMessage); + this.mqttOutFlowInput.send(MessageBuilder.withPayload("testPayload") + .setHeader(MqttHeaders.TOPIC, "siTest") + .build()); - Message receive = this.fromMqttChannel.receive(10_000); + Message receive = this.fromMqttChannel.receive(20_000); assertThat(receive).isNotNull(); } @@ -104,7 +92,7 @@ public static class Config { @Bean public MqttConnectionOptions mqttConnectOptions() { return new MqttConnectionOptionsBuilder() - .serverURI("wss://badMqttUrl") + .serverURI(MosquittoContainerTest.mqttUrl()) .automaticReconnect(true) .connectionTimeout(1) .build(); @@ -120,21 +108,50 @@ public IntegrationFlow mqttOutFlow() { @Bean - public IntegrationFlow mqttInFlow() { - Mqttv5PahoMessageDrivenChannelAdapter messageProducer = - new Mqttv5PahoMessageDrivenChannelAdapter(mqttConnectOptions(), "mqttv5SIin", "siTest"); - messageProducer.setPayloadType(String.class); - - return IntegrationFlow.from(messageProducer) + public IntegrationFlow mqttInFlow(Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter) { + mqttAdapter.setPayloadType(String.class); + return IntegrationFlow.from(mqttAdapter) .channel(c -> c.queue("fromMqttChannel")) .get(); } - @EventListener(MqttSubscribedEvent.class) - void mqttEvents() { + @Bean + public Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter() { + return new Mqttv5PahoMessageDrivenChannelAdapter(mqttConnectOptions(), "mqttv5SIin", "siTest"); + } + + @EventListener + void mqttEvents(MqttSubscribedEvent e) { this.subscribeLatch.countDown(); } + @Bean + public ClientV5Disconnector disconnector(Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter) { + return new ClientV5Disconnector(mqttAdapter); + } + + } + + public static class ClientV5Disconnector { + + private final Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter; + + ClientV5Disconnector(Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter) { + this.mqttAdapter = mqttAdapter; + } + + @EventListener + public void handleSubscribedEvent(MqttSubscribedEvent e) { + // not ideal, but no idea what can be the better way of disconnecting the client + try { + ((IMqttAsyncClient) new DirectFieldAccessor(mqttAdapter).getPropertyValue("mqttClient")) + .disconnect(); + } + catch (Exception ex) { + throw new IllegalStateException("could not disconnect the client!"); + } + } + } } diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml index d8da6b510fb..e0e92a480e1 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml @@ -16,7 +16,6 @@ client-id="foo" url="tcp://localhost:1883" client-factory="clientFactory" - recovery-interval="5000" channel="out" /> Date: Tue, 9 Aug 2022 18:12:55 +0300 Subject: [PATCH 7/9] GH-3685: Share MQTT connection across components Fixes spring-projects/spring-integration#3685 Other fixes and improvements after code review: * Get manual `reconnect` invocation back for v3/v5 adapters and client managers (see bug GH-3822 for a reasoning) * Remove unnecessary getters/setter for a listener and use adapter class as listener instead * Optimize MessageListener: remove redundant inner class and use a single method reference instead of N instances per each subscribe * Javadocs improvements --- .../mqtt/core/AbstractMqttClientManager.java | 20 +++++- .../integration/mqtt/core/ClientManager.java | 3 + .../mqtt/core/Mqttv3ClientManager.java | 17 ++++- .../mqtt/core/Mqttv5ClientManager.java | 16 ++++- ...stractMqttMessageDrivenChannelAdapter.java | 13 +--- .../MqttPahoMessageDrivenChannelAdapter.java | 61 +++++++--------- ...Mqttv5PahoMessageDrivenChannelAdapter.java | 64 +++++++---------- .../outbound/AbstractMqttMessageHandler.java | 1 + .../outbound/Mqttv5PahoMessageHandler.java | 3 +- ...ttv5BackToBackAutomaticReconnectTests.java | 71 +++++++------------ 10 files changed, 131 insertions(+), 138 deletions(-) diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index f5badf32bbf..4d475ed7dee 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -41,10 +41,14 @@ public abstract class AbstractMqttClientManager implements ClientManager connectCallbacks; private final String clientId; + private int phase = DEFAULT_MANAGER_PHASE; + private boolean manualAcks; private ApplicationEventPublisher applicationEventPublisher; @@ -86,7 +90,7 @@ protected synchronized void setClient(T client) { } protected Set getCallbacks() { - return Collections.unmodifiableSet(this.connectCallbacks); + return this.connectCallbacks; } @Override @@ -119,11 +123,13 @@ public String getBeanName() { * The phase of component autostart in {@link SmartLifecycle}. * If the custom one is required, note that for the correct behavior it should be less than phase of * {@link AbstractMqttMessageDrivenChannelAdapter} implementations. + * The default phase is {@link #DEFAULT_MANAGER_PHASE}. * @return {@link SmartLifecycle} autostart phase + * @see #setPhase */ @Override public int getPhase() { - return 0; + return this.phase; } @Override @@ -140,4 +146,14 @@ public synchronized boolean isRunning() { return this.client != null; } + /** + * Sets the phase of component autostart in {@link SmartLifecycle}. + * If the custom one is required, note that for the correct behavior it should be less than phase of + * {@link AbstractMqttMessageDrivenChannelAdapter} implementations. + * @see #getPhase + */ + public void setPhase(int phase) { + this.phase = phase; + } + } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java index b9e4a5a5a02..bbb909c9615 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/ClientManager.java @@ -42,6 +42,9 @@ public interface ClientManager extends SmartLifecycle, MqttComponent { /** * A contract for a custom callback if needed by a usage. + * + * @see org.eclipse.paho.mqttv5.client.MqttCallback#connectComplete + * @see org.eclipse.paho.client.mqttv3.MqttCallbackExtended#connectComplete */ interface ConnectCallback { diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index bdc1766673a..4c585262302 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -84,9 +84,20 @@ public synchronized void start() { catch (MqttException e) { logger.error("could not start client manager, client_id=" + getClientId(), e); - var applicationEventPublisher = getApplicationEventPublisher(); - if (applicationEventPublisher != null) { - applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); + // See GH-3822 + if (getConnectionInfo().isAutomaticReconnect()) { + try { + getClient().reconnect(); + } + catch (MqttException re) { + logger.error("MQTT client failed to connect. Never happens.", re); + } + } + else { + var applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); + } } } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java index b3c6ed3f7a4..f647a132ad7 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java @@ -83,9 +83,19 @@ public synchronized void start() { catch (MqttException e) { logger.error("could not start client manager, client_id=" + getClientId(), e); - var applicationEventPublisher = getApplicationEventPublisher(); - if (applicationEventPublisher != null) { - applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); + if (getConnectionInfo().isAutomaticReconnect()) { + try { + getClient().reconnect(); + } + catch (MqttException re) { + logger.error("MQTT client failed to connect. Never happens.", re); + } + } + else { + var applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null) { + applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); + } } } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java index bccf102dbe3..b7f18bae1f3 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java @@ -54,7 +54,7 @@ @ManagedResource @IntegrationManagedResource public abstract class AbstractMqttMessageDrivenChannelAdapter extends MessageProducerSupport - implements ApplicationEventPublisherAware { + implements ApplicationEventPublisherAware, ClientManager.ConnectCallback { /** * The default completion timeout in milliseconds. @@ -79,8 +79,6 @@ public abstract class AbstractMqttMessageDrivenChannelAdapter extends Mess private MqttMessageConverter converter; - private ClientManager.ConnectCallback clientManagerConnectCallback; - public AbstractMqttMessageDrivenChannelAdapter(@Nullable String url, String clientId, String... topic) { Assert.hasText(clientId, "'clientId' cannot be null or empty"); this.url = url; @@ -118,15 +116,6 @@ protected ClientManager getClientManager() { return this.clientManager; } - @Nullable - protected ClientManager.ConnectCallback getClientManagerCallback() { - return this.clientManagerConnectCallback; - } - - protected void setClientManagerCallback(ClientManager.ConnectCallback clientManagerConnectCallback) { - this.clientManagerConnectCallback = clientManagerConnectCallback; - } - /** * Set the QoS for each topic; a single value will apply to all topics otherwise * the correct number of qos values must be provided. diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index e08af66d056..fcea4f0cea6 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -135,7 +135,6 @@ public MqttPahoMessageDrivenChannelAdapter(ClientManager 0) { int[] requestedQos = getQos(); - MessageListener[] listeners = Stream.of(topics) - .map(t -> new MessageListener()) - .toArray(MessageListener[]::new); + IMqttMessageListener listener = this::messageArrived; + IMqttMessageListener[] listeners = Stream.of(topics) + .map(t -> listener) + .toArray(IMqttMessageListener[]::new); IMqttToken subscribeToken = this.client.subscribe(topics, requestedQos, listeners); subscribeToken.waitForCompletion(getCompletionTimeout()); int[] grantedQos = subscribeToken.getGrantedQos(); @@ -411,6 +421,11 @@ private AbstractIntegrationMessageBuilder toMessageBuilder(String topic, Mqtt public void deliveryComplete(IMqttDeliveryToken token) { } + @Override + public void connectComplete(boolean isReconnect) { + connectComplete(isReconnect, getUrl()); + } + @Override public void connectComplete(boolean reconnect, String serverURI) { if (!reconnect) { @@ -460,28 +475,4 @@ public void acknowledge() { } - private class MessageListener implements IMqttMessageListener { - - MessageListener() { - } - - @Override - public void messageArrived(String topic, MqttMessage mqttMessage) { - MqttPahoMessageDrivenChannelAdapter.this.messageArrived(topic, mqttMessage); - } - - } - - private class AdapterConnectCallback implements ClientManager.ConnectCallback { - - AdapterConnectCallback() { - } - - @Override - public void connectComplete(boolean isReconnect) { - MqttPahoMessageDrivenChannelAdapter.this.connectComplete(isReconnect, null); - } - - } - } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 682fc46aebb..4d4e26ca4aa 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -121,12 +121,10 @@ public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager new MqttSubscription(topics[i], requestedQos[i])) .toArray(MqttSubscription[]::new); - MessageListener[] listeners = IntStream.range(0, topics.length) - .mapToObj(t -> new MessageListener()) - .toArray(MessageListener[]::new); + IMqttMessageListener listener = this::messageArrived; + IMqttMessageListener[] listeners = IntStream.range(0, topics.length) + .mapToObj(t -> listener) + .toArray(IMqttMessageListener[]::new); this.mqttClient.subscribe(subscriptions, null, null, listeners, null) .waitForCompletion(getCompletionTimeout()); String message = "Connected and subscribed to " + Arrays.toString(topics); @@ -436,28 +450,4 @@ public void acknowledge() { } - private class MessageListener implements IMqttMessageListener { - - MessageListener() { - } - - @Override - public void messageArrived(String topic, MqttMessage message) { - Mqttv5PahoMessageDrivenChannelAdapter.this.messageArrived(topic, message); - } - - } - - private class AdapterConnectCallback implements ClientManager.ConnectCallback { - - AdapterConnectCallback() { - } - - @Override - public void connectComplete(boolean isReconnect) { - Mqttv5PahoMessageDrivenChannelAdapter.this.connectComplete(isReconnect, null); - } - - } - } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java index 387db0497fe..bf5b6362772 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/AbstractMqttMessageHandler.java @@ -103,6 +103,7 @@ public AbstractMqttMessageHandler(@Nullable String url, String clientId) { public AbstractMqttMessageHandler(ClientManager clientManager) { Assert.notNull(clientManager, "'clientManager' cannot be null or empty"); this.clientManager = clientManager; + clientManager.getConnectionInfo(); this.url = null; this.clientId = null; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index 998160eefc2..eec254445bc 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -95,8 +95,7 @@ public Mqttv5PahoMessageHandler(ClientManager testMessage = + MessageBuilder.withPayload("testPayload") + .setHeader(MqttHeaders.TOPIC, "siTest") + .build(); + + assertThatExceptionOfType(MessageHandlingException.class) + .isThrownBy(() -> this.mqttOutFlowInput.send(testMessage)) + .withCauseExactlyInstanceOf(MqttException.class) + .withRootCauseExactlyInstanceOf(UnknownHostException.class); + + connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); + assertThat(this.config.subscribeLatch.await(10, TimeUnit.SECONDS)).isTrue(); - this.mqttOutFlowInput.send(MessageBuilder.withPayload("testPayload") - .setHeader(MqttHeaders.TOPIC, "siTest") - .build()); + this.mqttOutFlowInput.send(testMessage); - Message receive = this.fromMqttChannel.receive(20_000); + Message receive = this.fromMqttChannel.receive(10_000); assertThat(receive).isNotNull(); } @@ -92,7 +104,7 @@ public static class Config { @Bean public MqttConnectionOptions mqttConnectOptions() { return new MqttConnectionOptionsBuilder() - .serverURI(MosquittoContainerTest.mqttUrl()) + .serverURI("wss://badMqttUrl") .automaticReconnect(true) .connectionTimeout(1) .build(); @@ -108,50 +120,21 @@ public IntegrationFlow mqttOutFlow() { @Bean - public IntegrationFlow mqttInFlow(Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter) { - mqttAdapter.setPayloadType(String.class); - return IntegrationFlow.from(mqttAdapter) + public IntegrationFlow mqttInFlow() { + Mqttv5PahoMessageDrivenChannelAdapter messageProducer = + new Mqttv5PahoMessageDrivenChannelAdapter(mqttConnectOptions(), "mqttv5SIin", "siTest"); + messageProducer.setPayloadType(String.class); + + return IntegrationFlow.from(messageProducer) .channel(c -> c.queue("fromMqttChannel")) .get(); } - @Bean - public Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter() { - return new Mqttv5PahoMessageDrivenChannelAdapter(mqttConnectOptions(), "mqttv5SIin", "siTest"); - } - - @EventListener - void mqttEvents(MqttSubscribedEvent e) { + @EventListener(MqttSubscribedEvent.class) + void mqttEvents() { this.subscribeLatch.countDown(); } - @Bean - public ClientV5Disconnector disconnector(Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter) { - return new ClientV5Disconnector(mqttAdapter); - } - - } - - public static class ClientV5Disconnector { - - private final Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter; - - ClientV5Disconnector(Mqttv5PahoMessageDrivenChannelAdapter mqttAdapter) { - this.mqttAdapter = mqttAdapter; - } - - @EventListener - public void handleSubscribedEvent(MqttSubscribedEvent e) { - // not ideal, but no idea what can be the better way of disconnecting the client - try { - ((IMqttAsyncClient) new DirectFieldAccessor(mqttAdapter).getPropertyValue("mqttClient")) - .disconnect(); - } - catch (Exception ex) { - throw new IllegalStateException("could not disconnect the client!"); - } - } - } } From b5da36be55ce31eef58530e7b219a82acc4b7290 Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Wed, 10 Aug 2022 15:25:40 +0300 Subject: [PATCH 8/9] * Add Javadocs to abstract client manager * Extract common callback add/rm logic to abstract adapter class * Small code cleanups/fixes related to code style & simplicity, ctor inits and unnecessary methods; eliminate unnecessary logs noise * Remove `@LongRunningTest` for `ClientManagerBackToBackTests` as test run time is ~6-7 secs --- .../mqtt/core/AbstractMqttClientManager.java | 6 ++-- .../mqtt/core/Mqttv3ClientManager.java | 5 +-- .../mqtt/core/Mqttv5ClientManager.java | 5 +-- ...stractMqttMessageDrivenChannelAdapter.java | 18 ++++++++++- .../MqttPahoMessageDrivenChannelAdapter.java | 11 +------ ...Mqttv5PahoMessageDrivenChannelAdapter.java | 26 ++++----------- .../outbound/Mqttv5PahoMessageHandler.java | 32 +++++++------------ .../mqtt/ClientManagerBackToBackTests.java | 2 -- 8 files changed, 45 insertions(+), 60 deletions(-) diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index 4d475ed7dee..fc40b5634bb 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -30,6 +30,9 @@ import org.springframework.util.Assert; /** + * Abstract class for MQTT client managers which can be a base for any common v3/v5 client manager implementation. + * Contains some basic utility and implementation-agnostic fields and methods. + * * @param MQTT client type * @param MQTT connection options type (v5 or v3) * @@ -43,7 +46,7 @@ public abstract class AbstractMqttClientManager implements ClientManager connectCallbacks; + private final Set connectCallbacks = Collections.synchronizedSet(new HashSet<>()); private final String clientId; @@ -62,7 +65,6 @@ public abstract class AbstractMqttClientManager implements ClientManager()); } protected void setManualAcks(boolean manualAcks) { diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index 4c585262302..9745fc6395d 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -82,8 +82,6 @@ public synchronized void start() { getClient().connect(options).waitForCompletion(options.getConnectionTimeout()); } catch (MqttException e) { - logger.error("could not start client manager, client_id=" + getClientId(), e); - // See GH-3822 if (getConnectionInfo().isAutomaticReconnect()) { try { @@ -98,6 +96,9 @@ public synchronized void start() { if (applicationEventPublisher != null) { applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); } + else { + logger.error("could not start client manager, client_id=" + getClientId(), e); + } } } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java index f647a132ad7..a4a17617850 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java @@ -81,8 +81,6 @@ public synchronized void start() { .waitForCompletion(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { - logger.error("could not start client manager, client_id=" + getClientId(), e); - if (getConnectionInfo().isAutomaticReconnect()) { try { getClient().reconnect(); @@ -96,6 +94,9 @@ public synchronized void start() { if (applicationEventPublisher != null) { applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e)); } + else { + logger.error("could not start client manager, client_id=" + getClientId(), e); + } } } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java index b7f18bae1f3..bf0bd3eda04 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/AbstractMqttMessageDrivenChannelAdapter.java @@ -95,7 +95,7 @@ public AbstractMqttMessageDrivenChannelAdapter(ClientManager clientManager this.clientId = null; } - private Set initTopics(String[] topic) { + private static Set initTopics(String[] topic) { Assert.notNull(topic, "'topics' cannot be null"); Assert.noNullElements(topic, "'topics' cannot have null elements"); final Set initialTopics = new LinkedHashSet<>(); @@ -185,6 +185,22 @@ public String[] getTopic() { } } + @Override + protected void onInit() { + super.onInit(); + if (this.clientManager != null) { + this.clientManager.addCallback(this); + } + } + + @Override + public void destroy() { + super.destroy(); + if (this.clientManager != null) { + this.clientManager.removeCallback(this); + } + } + @Override public String getComponentType() { return "mqtt:inbound-channel-adapter"; diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index fcea4f0cea6..c2bb555ac0d 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -168,11 +168,6 @@ protected void onInit() { pahoMessageConverter.setBeanFactory(getBeanFactory()); setConverter(pahoMessageConverter); } - - var clientManager = getClientManager(); - if (clientManager != null) { - clientManager.addCallback(this); - } } @Override @@ -228,11 +223,7 @@ protected synchronized void doStop() { @Override public void destroy() { super.destroy(); - var clientManager = getClientManager(); - if (clientManager != null) { - clientManager.removeCallback(this); - } - else { + if (getClientManager() == null) { try { this.client.close(); } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 4d4e26ca4aa..8245a5d7e5f 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -95,7 +95,10 @@ public class Mqttv5PahoMessageDrivenChannelAdapter public Mqttv5PahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) { super(url, clientId, topic); - this.connectionOptions = buildDefaultConnectionOptions(url); + Assert.hasText(url, "'url' cannot be null or empty"); + this.connectionOptions = new MqttConnectionOptions(); + this.connectionOptions.setServerURIs(new String[]{ url }); + this.connectionOptions.setAutomaticReconnect(true); } public Mqttv5PahoMessageDrivenChannelAdapter(MqttConnectionOptions connectionOptions, String clientId, @@ -123,15 +126,6 @@ public Mqttv5PahoMessageDrivenChannelAdapter(ClientManager headerMapper) { @Override protected void onInit() { super.onInit(); - var clientManager = getClientManager(); - if (clientManager == null && this.mqttClient == null) { + if (getClientManager() == null && this.mqttClient == null) { try { this.mqttClient = new MqttAsyncClient(getUrl(), getClientId(), this.persistence); this.mqttClient.setCallback(this); @@ -185,9 +178,6 @@ protected void onInit() { .getBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME, SmartMessageConverter.class)); } - if (clientManager != null) { - clientManager.addCallback(this); - } } @Override @@ -244,18 +234,14 @@ protected void doStop() { @Override public void destroy() { super.destroy(); - var clientManager = getClientManager(); try { - if (clientManager == null && this.mqttClient != null) { + if (getClientManager() == null && this.mqttClient != null) { this.mqttClient.close(true); } } catch (MqttException ex) { logger.error(ex, "Failed to close 'MqttAsyncClient'"); } - if (clientManager != null) { - clientManager.removeCallback(this); - } } @Override diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index eec254445bc..637a9bb9cba 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -75,7 +75,10 @@ public class Mqttv5PahoMessageHandler extends AbstractMqttMessageHandler message) { Assert.isInstanceOf(MqttMessage.class, mqttMessage, "The 'mqttMessage' must be an instance of 'MqttMessage'"); long completionTimeout = getCompletionTimeout(); try { - IMqttAsyncClient theClient; - if (getClientManager() != null) { - theClient = getClientManager().getClient(); - } - else { - if (!this.mqttClient.isConnected()) { - this.mqttClient.connect(this.connectionOptions).waitForCompletion(completionTimeout); - } - theClient = this.mqttClient; + if (!this.mqttClient.isConnected()) { + this.mqttClient.connect(this.connectionOptions).waitForCompletion(completionTimeout); } - IMqttToken token = theClient.publish(topic, (MqttMessage) mqttMessage); + IMqttToken token = this.mqttClient.publish(topic, (MqttMessage) mqttMessage); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); if (!this.async) { token.waitForCompletion(completionTimeout); // NOSONAR (sync) diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java index bd8bfaf7e63..94026309709 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java @@ -43,7 +43,6 @@ import org.springframework.integration.mqtt.outbound.Mqttv5PahoMessageHandler; import org.springframework.integration.mqtt.support.MqttHeaders; import org.springframework.integration.support.MessageBuilder; -import org.springframework.integration.test.condition.LongRunningTest; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; @@ -52,7 +51,6 @@ * @author Artem Vozhdayenko * @since 6.0 */ -@LongRunningTest class ClientManagerBackToBackTests implements MosquittoContainerTest { @Test From 318fd824b8e44f63f1af44520a0a5986835fe06e Mon Sep 17 00:00:00 2001 From: Artem Vozhdayenko Date: Thu, 11 Aug 2022 12:10:12 +0300 Subject: [PATCH 9/9] * Remove client factory as dependency for v3 client manager and use plain connection properties and client persistence instead * Add missed javadocs * Other code style & cleanup improvements --- .../mqtt/core/AbstractMqttClientManager.java | 23 ++++++-- .../mqtt/core/Mqttv3ClientManager.java | 52 ++++++++++++------- .../mqtt/core/Mqttv5ClientManager.java | 26 ++++++++-- .../MqttPahoMessageDrivenChannelAdapter.java | 10 +--- .../outbound/Mqttv5PahoMessageHandler.java | 12 ++--- .../mqtt/ClientManagerBackToBackTests.java | 24 ++------- 6 files changed, 85 insertions(+), 62 deletions(-) diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java index fc40b5634bb..a22efb8ad74 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/AbstractMqttClientManager.java @@ -35,12 +35,13 @@ * * @param MQTT client type * @param MQTT connection options type (v5 or v3) + * @param

MQTT client persistence type (for v5 or v3) * * @author Artem Vozhdayenko * * @since 6.0 */ -public abstract class AbstractMqttClientManager implements ClientManager, ApplicationEventPublisherAware { +public abstract class AbstractMqttClientManager implements ClientManager, ApplicationEventPublisherAware { protected final Log logger = LogFactory.getLog(this.getClass()); // NOSONAR @@ -56,12 +57,14 @@ public abstract class AbstractMqttClientManager implements ClientManager getCallbacks() { return this.connectCallbacks; } @@ -149,7 +164,7 @@ public synchronized boolean isRunning() { } /** - * Sets the phase of component autostart in {@link SmartLifecycle}. + * Set the phase of component autostart in {@link SmartLifecycle}. * If the custom one is required, note that for the correct behavior it should be less than phase of * {@link AbstractMqttMessageDrivenChannelAdapter} implementations. * @see #getPhase diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index 9745fc6395d..c0f17d87e2c 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java @@ -18,7 +18,9 @@ import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; +import org.eclipse.paho.client.mqttv3.MqttClientPersistence; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; @@ -27,23 +29,28 @@ import org.springframework.util.Assert; /** + * A client manager implementation for MQTT v3 protocol. Requires a client ID and server URI. + * If needed, the connection options may be overridden and passed as a {@link MqttConnectOptions} dependency. + * By default, automatic reconnect is used. If it is required to be turned off, one should listen for + * {@link MqttConnectionFailedEvent} and reconnect the MQTT client manually. + * * @author Artem Vozhdayenko * @since 6.0 */ -public class Mqttv3ClientManager extends AbstractMqttClientManager +public class Mqttv3ClientManager + extends AbstractMqttClientManager implements MqttCallbackExtended { - private final MqttPahoClientFactory clientFactory; + private final MqttConnectOptions connectionOptions; public Mqttv3ClientManager(String url, String clientId) { - this(buildDefaultClientFactory(url), clientId); + this(buildDefaultConnectionOptions(url), clientId); } - public Mqttv3ClientManager(MqttPahoClientFactory clientFactory, String clientId) { + public Mqttv3ClientManager(MqttConnectOptions connectionOptions, String clientId) { super(clientId); - Assert.notNull(clientFactory, "'clientFactory' is required"); - this.clientFactory = clientFactory; - MqttConnectOptions connectionOptions = clientFactory.getConnectionOptions(); + Assert.notNull(connectionOptions, "'connectionOptions' is required"); + this.connectionOptions = connectionOptions; String[] serverURIs = connectionOptions.getServerURIs(); Assert.notEmpty(serverURIs, "'serverURIs' must be provided in the 'MqttConnectionOptions'"); setUrl(serverURIs[0]); @@ -54,32 +61,27 @@ public Mqttv3ClientManager(MqttPahoClientFactory clientFactory, String clientId) } } - private static MqttPahoClientFactory buildDefaultClientFactory(String url) { + private static MqttConnectOptions buildDefaultConnectionOptions(String url) { Assert.notNull(url, "'url' is required"); MqttConnectOptions connectOptions = new MqttConnectOptions(); connectOptions.setServerURIs(new String[]{ url }); connectOptions.setAutomaticReconnect(true); - DefaultMqttPahoClientFactory defaultFactory = new DefaultMqttPahoClientFactory(); - defaultFactory.setConnectionOptions(connectOptions); - return defaultFactory; + return connectOptions; } @Override public synchronized void start() { if (getClient() == null) { try { - var client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); - client.setManualAcks(isManualAcks()); - client.setCallback(this); - setClient(client); + setClient(createClient()); } catch (MqttException e) { throw new IllegalStateException("could not start client manager", e); } } try { - MqttConnectOptions options = this.clientFactory.getConnectionOptions(); - getClient().connect(options).waitForCompletion(options.getConnectionTimeout()); + getClient().connect(this.connectionOptions) + .waitForCompletion(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { // See GH-3822 @@ -110,7 +112,7 @@ public synchronized void stop() { return; } try { - client.disconnectForcibly(this.clientFactory.getConnectionOptions().getConnectionTimeout()); + client.disconnectForcibly(this.connectionOptions.getConnectionTimeout()); } catch (MqttException e) { logger.error("could not disconnect from the client", e); @@ -148,6 +150,18 @@ public void deliveryComplete(IMqttDeliveryToken token) { @Override public MqttConnectOptions getConnectionInfo() { - return this.clientFactory.getConnectionOptions(); + return this.connectionOptions; + } + + private IMqttAsyncClient createClient() throws MqttException { + var persistence = getPersistence(); + var url = getUrl(); + var clientId = getClientId(); + var client = persistence == null ? + new MqttAsyncClient(url, clientId) : + new MqttAsyncClient(url, clientId, persistence); + client.setManualAcks(isManualAcks()); + client.setCallback(this); + return client; } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java index a4a17617850..3caed4c3ae3 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java @@ -20,6 +20,7 @@ import org.eclipse.paho.mqttv5.client.IMqttToken; import org.eclipse.paho.mqttv5.client.MqttAsyncClient; import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttClientPersistence; import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; import org.eclipse.paho.mqttv5.common.MqttException; @@ -30,10 +31,16 @@ import org.springframework.util.Assert; /** + * A client manager implementation for MQTT v5 protocol. Requires a client ID and server URI. + * If needed, the connection options may be overridden and passed as a {@link MqttConnectionOptions} dependency. + * By default, automatic reconnect is used. If it is required to be turned off, one should listen for + * {@link MqttConnectionFailedEvent} and reconnect the MQTT client manually. + * * @author Artem Vozhdayenko * @since 6.0 */ -public class Mqttv5ClientManager extends AbstractMqttClientManager +public class Mqttv5ClientManager + extends AbstractMqttClientManager implements MqttCallback { private final MqttConnectionOptions connectionOptions; @@ -67,10 +74,7 @@ private static MqttConnectionOptions buildDefaultConnectionOptions(String url) { public synchronized void start() { if (getClient() == null) { try { - var client = new MqttAsyncClient(getUrl(), getClientId()); - client.setManualAcks(isManualAcks()); - client.setCallback(this); - setClient(client); + setClient(createClient()); } catch (MqttException e) { throw new IllegalStateException("could not start client manager", e); @@ -161,4 +165,16 @@ public void mqttErrorOccurred(MqttException exception) { public MqttConnectionOptions getConnectionInfo() { return this.connectionOptions; } + + private MqttAsyncClient createClient() throws MqttException { + var persistence = getPersistence(); + var url = getUrl(); + var clientId = getClientId(); + var client = persistence == null ? + new MqttAsyncClient(url, clientId) + : new MqttAsyncClient(url, clientId, persistence); + client.setManualAcks(isManualAcks()); + client.setCallback(this); + return client; + } } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index c2bb555ac0d..b38bc7aa700 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -277,14 +277,13 @@ private synchronized void connect() throws MqttException { // NOSONAR this.consumerStopAction = ConsumerStopAction.UNSUBSCRIBE_CLEAN; } - long completionTimeout = getCompletionTimeout(); var clientManager = getClientManager(); if (clientManager == null) { Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null, "If no 'url' provided, connectionOptions.getServerURIs() must not be null"); this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId()); this.client.setCallback(this); - this.client.connect(connectionOptions).waitForCompletion(completionTimeout); + this.client.connect(connectionOptions).waitForCompletion(getCompletionTimeout()); this.client.setManualAcks(isManualAcks()); } else { @@ -293,11 +292,6 @@ private synchronized void connect() throws MqttException { // NOSONAR } private void subscribe() { - var clientManager = getClientManager(); - if (clientManager != null && this.client == null) { - this.client = clientManager.getClient(); - } - this.topicLock.lock(); String[] topics = getTopic(); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); @@ -311,7 +305,7 @@ private void subscribe() { IMqttToken subscribeToken = this.client.subscribe(topics, requestedQos, listeners); subscribeToken.waitForCompletion(getCompletionTimeout()); int[] grantedQos = subscribeToken.getGrantedQos(); - if (grantedQos.length == 1 && grantedQos[0] == 0x80) { + if (grantedQos.length == 1 && grantedQos[0] == 0x80) { // NOSONAR throw new MqttException(MqttException.REASON_CODE_SUBSCRIBE_FAILED); } warnInvalidQosForSubscription(topics, requestedQos, grantedQos); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index 637a9bb9cba..9c165ed1440 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -168,12 +168,12 @@ protected void onInit() { protected void doStart() { try { var clientManager = getClientManager(); - if (this.mqttClient != null) { - this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); - } - else if (clientManager != null) { + if (clientManager != null) { this.mqttClient = clientManager.getClient(); } + else { + this.mqttClient.connect(this.connectionOptions).waitForCompletion(getCompletionTimeout()); + } } catch (MqttException ex) { logger.error(ex, "MQTT client failed to connect."); @@ -183,7 +183,7 @@ else if (clientManager != null) { @Override protected void doStop() { try { - if (this.mqttClient != null) { + if (getClientManager() == null) { this.mqttClient.disconnect().waitForCompletion(getDisconnectCompletionTimeout()); } } @@ -196,7 +196,7 @@ protected void doStop() { public void destroy() { super.destroy(); try { - if (this.mqttClient != null) { + if (getClientManager() == null) { this.mqttClient.close(true); } } diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java index 94026309709..a92ba0d8771 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ClientManagerBackToBackTests.java @@ -32,8 +32,6 @@ import org.springframework.context.event.EventListener; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; -import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; -import org.springframework.integration.mqtt.core.MqttPahoClientFactory; import org.springframework.integration.mqtt.core.Mqttv3ClientManager; import org.springframework.integration.mqtt.core.Mqttv5ClientManager; import org.springframework.integration.mqtt.event.MqttSubscribedEvent; @@ -115,18 +113,11 @@ public void onSubscribed(MqttSubscribedEvent e) { } @Bean - public Mqttv3ClientManager mqttv3ClientManager(MqttPahoClientFactory pahoClientFactory) { - return new Mqttv3ClientManager(pahoClientFactory, "client-manager-client-id-v3"); - } - - @Bean - public MqttPahoClientFactory pahoClientFactory() { - var pahoClientFactory = new DefaultMqttPahoClientFactory(); + public Mqttv3ClientManager mqttv3ClientManager() { MqttConnectOptions connectionOptions = new MqttConnectOptions(); connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); connectionOptions.setAutomaticReconnect(true); - pahoClientFactory.setConnectionOptions(connectionOptions); - return pahoClientFactory; + return new Mqttv3ClientManager(connectionOptions, "client-manager-client-id-v3"); } @Bean @@ -162,18 +153,11 @@ public ClientV3Disconnector disconnector(Mqttv3ClientManager clientManager) { } @Bean - public Mqttv3ClientManager mqttv3ClientManager(MqttPahoClientFactory pahoClientFactory) { - return new Mqttv3ClientManager(pahoClientFactory, "client-manager-client-id-v3-reconnect"); - } - - @Bean - public MqttPahoClientFactory pahoClientFactory() { - var pahoClientFactory = new DefaultMqttPahoClientFactory(); + public Mqttv3ClientManager mqttv3ClientManager() { MqttConnectOptions connectionOptions = new MqttConnectOptions(); connectionOptions.setServerURIs(new String[]{ MosquittoContainerTest.mqttUrl() }); connectionOptions.setAutomaticReconnect(true); - pahoClientFactory.setConnectionOptions(connectionOptions); - return pahoClientFactory; + return new Mqttv3ClientManager(connectionOptions, "client-manager-client-id-v3-reconnect"); } @Bean