org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler Maven / Gradle / Ivy
/*
* Copyright 2002-2018 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.messaging.simp.broker;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.simp.SimpLogging;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.InterceptableChannel;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Abstract base class for a {@link MessageHandler} that broker messages to
* registered subscribers.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractBrokerMessageHandler
implements MessageHandler, ApplicationEventPublisherAware, SmartLifecycle {
protected final Log logger = SimpLogging.forLogName(getClass());
private final SubscribableChannel clientInboundChannel;
private final MessageChannel clientOutboundChannel;
private final SubscribableChannel brokerChannel;
private final Collection destinationPrefixes;
private boolean preservePublishOrder = false;
@Nullable
private ApplicationEventPublisher eventPublisher;
private AtomicBoolean brokerAvailable = new AtomicBoolean(false);
private final BrokerAvailabilityEvent availableEvent = new BrokerAvailabilityEvent(true, this);
private final BrokerAvailabilityEvent notAvailableEvent = new BrokerAvailabilityEvent(false, this);
private boolean autoStartup = true;
private volatile boolean running = false;
private final Object lifecycleMonitor = new Object();
private final ChannelInterceptor unsentDisconnectInterceptor = new UnsentDisconnectChannelInterceptor();
/**
* Constructor with no destination prefixes (matches all destinations).
* @param inboundChannel the channel for receiving messages from clients (e.g. WebSocket clients)
* @param outboundChannel the channel for sending messages to clients (e.g. WebSocket clients)
* @param brokerChannel the channel for the application to send messages to the broker
*/
public AbstractBrokerMessageHandler(SubscribableChannel inboundChannel, MessageChannel outboundChannel,
SubscribableChannel brokerChannel) {
this(inboundChannel, outboundChannel, brokerChannel, Collections.emptyList());
}
/**
* Constructor with destination prefixes to match to destinations of messages.
* @param inboundChannel the channel for receiving messages from clients (e.g. WebSocket clients)
* @param outboundChannel the channel for sending messages to clients (e.g. WebSocket clients)
* @param brokerChannel the channel for the application to send messages to the broker
* @param destinationPrefixes prefixes to use to filter out messages
*/
public AbstractBrokerMessageHandler(SubscribableChannel inboundChannel, MessageChannel outboundChannel,
SubscribableChannel brokerChannel, @Nullable Collection destinationPrefixes) {
Assert.notNull(inboundChannel, "'inboundChannel' must not be null");
Assert.notNull(outboundChannel, "'outboundChannel' must not be null");
Assert.notNull(brokerChannel, "'brokerChannel' must not be null");
this.clientInboundChannel = inboundChannel;
this.clientOutboundChannel = outboundChannel;
this.brokerChannel = brokerChannel;
destinationPrefixes = (destinationPrefixes != null ? destinationPrefixes : Collections.emptyList());
this.destinationPrefixes = Collections.unmodifiableCollection(destinationPrefixes);
}
public SubscribableChannel getClientInboundChannel() {
return this.clientInboundChannel;
}
public MessageChannel getClientOutboundChannel() {
return this.clientOutboundChannel;
}
public SubscribableChannel getBrokerChannel() {
return this.brokerChannel;
}
public Collection getDestinationPrefixes() {
return this.destinationPrefixes;
}
/**
* Whether the client must receive messages in the order of publication.
* By default messages sent to the {@code "clientOutboundChannel"} may
* not be processed in the same order because the channel is backed by a
* ThreadPoolExecutor that in turn does not guarantee processing in order.
*
When this flag is set to {@code true} messages within the same session
* will be sent to the {@code "clientOutboundChannel"} one at a time in
* order to preserve the order of publication. Enable this only if needed
* since there is some performance overhead to keep messages in order.
* @param preservePublishOrder whether to publish in order
* @since 5.1
*/
public void setPreservePublishOrder(boolean preservePublishOrder) {
OrderedMessageSender.configureOutboundChannel(this.clientOutboundChannel, preservePublishOrder);
this.preservePublishOrder = preservePublishOrder;
}
/**
* Whether to ensure messages are received in the order of publication.
* @since 5.1
*/
public boolean isPreservePublishOrder() {
return this.preservePublishOrder;
}
@Override
public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher publisher) {
this.eventPublisher = publisher;
}
@Nullable
public ApplicationEventPublisher getApplicationEventPublisher() {
return this.eventPublisher;
}
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
@Override
public boolean isAutoStartup() {
return this.autoStartup;
}
@Override
public void start() {
synchronized (this.lifecycleMonitor) {
logger.info("Starting...");
this.clientInboundChannel.subscribe(this);
this.brokerChannel.subscribe(this);
if (this.clientInboundChannel instanceof InterceptableChannel) {
((InterceptableChannel) this.clientInboundChannel).addInterceptor(0, this.unsentDisconnectInterceptor);
}
startInternal();
this.running = true;
logger.info("Started.");
}
}
protected void startInternal() {
}
@Override
public void stop() {
synchronized (this.lifecycleMonitor) {
logger.info("Stopping...");
stopInternal();
this.clientInboundChannel.unsubscribe(this);
this.brokerChannel.unsubscribe(this);
if (this.clientInboundChannel instanceof InterceptableChannel) {
((InterceptableChannel) this.clientInboundChannel).removeInterceptor(this.unsentDisconnectInterceptor);
}
this.running = false;
logger.info("Stopped.");
}
}
protected void stopInternal() {
}
@Override
public final void stop(Runnable callback) {
synchronized (this.lifecycleMonitor) {
stop();
callback.run();
}
}
/**
* Check whether this message handler is currently running.
*
Note that even when this message handler is running the
* {@link #isBrokerAvailable()} flag may still independently alternate between
* being on and off depending on the concrete sub-class implementation.
*/
@Override
public final boolean isRunning() {
return this.running;
}
/**
* Whether the message broker is currently available and able to process messages.
*
Note that this is in addition to the {@link #isRunning()} flag, which
* indicates whether this message handler is running. In other words the message
* handler must first be running and then the {@code #isBrokerAvailable()} flag
* may still independently alternate between being on and off depending on the
* concrete sub-class implementation.
*
Application components may implement
* {@code org.springframework.context.ApplicationListener<BrokerAvailabilityEvent>}
* to receive notifications when broker becomes available and unavailable.
*/
public boolean isBrokerAvailable() {
return this.brokerAvailable.get();
}
@Override
public void handleMessage(Message message) {
if (!this.running) {
if (logger.isTraceEnabled()) {
logger.trace(this + " not running yet. Ignoring " + message);
}
return;
}
handleMessageInternal(message);
}
protected abstract void handleMessageInternal(Message message);
protected boolean checkDestinationPrefix(@Nullable String destination) {
if (destination == null || CollectionUtils.isEmpty(this.destinationPrefixes)) {
return true;
}
for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) {
return true;
}
}
return false;
}
protected void publishBrokerAvailableEvent() {
boolean shouldPublish = this.brokerAvailable.compareAndSet(false, true);
if (this.eventPublisher != null && shouldPublish) {
if (logger.isInfoEnabled()) {
logger.info(this.availableEvent);
}
this.eventPublisher.publishEvent(this.availableEvent);
}
}
protected void publishBrokerUnavailableEvent() {
boolean shouldPublish = this.brokerAvailable.compareAndSet(true, false);
if (this.eventPublisher != null && shouldPublish) {
if (logger.isInfoEnabled()) {
logger.info(this.notAvailableEvent);
}
this.eventPublisher.publishEvent(this.notAvailableEvent);
}
}
/**
* Get the MessageChannel to use for sending messages to clients, possibly
* a per-session wrapper when {@code preservePublishOrder=true}.
* @since 5.1
*/
protected MessageChannel getClientOutboundChannelForSession(String sessionId) {
return this.preservePublishOrder ?
new OrderedMessageSender(getClientOutboundChannel(), logger) : getClientOutboundChannel();
}
/**
* Detect unsent DISCONNECT messages and process them anyway.
*/
private class UnsentDisconnectChannelInterceptor implements ChannelInterceptor {
@Override
public void afterSendCompletion(
Message message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
if (!sent) {
SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(message.getHeaders());
if (SimpMessageType.DISCONNECT.equals(messageType)) {
logger.debug("Detected unsent DISCONNECT message. Processing anyway.");
handleMessage(message);
}
}
}
}
}