org.springframework.integration.websocket.IntegrationWebSocketContainer Maven / Gradle / Ivy
/*
* Copyright 2014-2024 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.websocket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.SubProtocolCapable;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
/**
* The high-level 'connection factory pattern' contract over low-level Web-Socket
* configuration.
*
* Provides the composition for the internal {@link WebSocketHandler}
* implementation, which is used with native Web-Socket containers.
*
* Collects established {@link WebSocketSession}s, which can be accessed using
* {@link #getSession(String)}.
*
* Can accept the {@link WebSocketListener} to delegate {@link WebSocketSession} events
* from the internal {@link IntegrationWebSocketContainer.IntegrationWebSocketHandler}.
*
* Supported sub-protocols can be configured, but {@link WebSocketListener#getSubProtocols()}
* have a precedent.
*
* @author Artem Bilan
* @author Gary Russell
* @author Julian Koch
*
* @since 4.1
*
* @see org.springframework.integration.websocket.inbound.WebSocketInboundChannelAdapter
* @see org.springframework.integration.websocket.outbound.WebSocketOutboundMessageHandler
*/
public abstract class IntegrationWebSocketContainer implements DisposableBean {
public static final int DEFAULT_SEND_TIME_LIMIT = 10 * 1000;
public static final int DEFAULT_SEND_BUFFER_SIZE = 512 * 1024;
protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR
protected final Lock lock = new ReentrantLock();
private WebSocketHandler webSocketHandler = new IntegrationWebSocketHandler();
protected final Map sessions = new ConcurrentHashMap<>(); // NOSONAR
private final List supportedProtocols = new ArrayList<>();
private WebSocketListener messageListener;
private int sendTimeLimit = DEFAULT_SEND_TIME_LIMIT;
private int sendBufferSizeLimit = DEFAULT_SEND_BUFFER_SIZE;
@Nullable
private ConcurrentWebSocketSessionDecorator.OverflowStrategy sendBufferOverflowStrategy;
public void setSendTimeLimit(int sendTimeLimit) {
this.sendTimeLimit = sendTimeLimit;
}
public void setSendBufferSizeLimit(int sendBufferSizeLimit) {
this.sendBufferSizeLimit = sendBufferSizeLimit;
}
/**
* Set the send buffer overflow strategy.
* Concurrently generated outbound messages are buffered if sending is slow.
* This strategy determines the behavior when the buffer has reached the limit
* configured with {@link #setSendBufferSizeLimit}.
* @param overflowStrategy The {@link ConcurrentWebSocketSessionDecorator.OverflowStrategy} to use.
* @since 5.5.19
* @see ConcurrentWebSocketSessionDecorator
*/
public void setSendBufferOverflowStrategy(
@Nullable ConcurrentWebSocketSessionDecorator.OverflowStrategy overflowStrategy) {
this.sendBufferOverflowStrategy = overflowStrategy;
}
public void setMessageListener(WebSocketListener messageListener) {
Assert.state(this.messageListener == null || this.messageListener.equals(messageListener),
"'messageListener' is already configured");
this.messageListener = messageListener;
}
public void setSupportedProtocols(String... protocols) {
this.supportedProtocols.clear();
addSupportedProtocols(protocols);
}
public void addSupportedProtocols(String... protocols) {
for (String protocol : protocols) {
this.supportedProtocols.add(protocol.toLowerCase(Locale.ROOT));
}
}
/**
* Replace the default {@link WebSocketHandler} with the one provided here, e.g. via decoration factories.
* @param handler the actual {@link WebSocketHandler} to replace.
* @since 5.5.18
*/
protected void setWebSocketHandler(WebSocketHandler handler) {
this.webSocketHandler = handler;
}
public WebSocketHandler getWebSocketHandler() {
return this.webSocketHandler;
}
public List getSubProtocols() {
List protocols = new ArrayList<>();
if (this.messageListener != null) {
protocols.addAll(this.messageListener.getSubProtocols());
}
protocols.addAll(this.supportedProtocols);
return Collections.unmodifiableList(protocols);
}
public Map getSessions() {
return Collections.unmodifiableMap(this.sessions);
}
public WebSocketSession getSession(String sessionId) {
WebSocketSession session = this.sessions.get(sessionId);
Assert.notNull(session, () -> "Session not found for id '" + sessionId + "'");
return session;
}
public void closeSession(WebSocketSession session, CloseStatus closeStatus)
throws Exception { // NOSONAR
// Session may be unresponsive so clear first
session.close(closeStatus);
this.webSocketHandler.afterConnectionClosed(session, closeStatus);
}
@Override
public void destroy() {
try {
// Notify sessions to stop flushing messages
for (WebSocketSession session : this.sessions.values()) {
try {
session.close(CloseStatus.GOING_AWAY);
}
catch (Exception ex) {
this.logger.error("Failed to close session id '" + session.getId() + "': " + ex.getMessage());
}
}
}
finally {
this.sessions.clear();
}
}
private WebSocketSession decorateSession(WebSocketSession sessionToDecorate) {
if (this.sendBufferOverflowStrategy == null) {
return new ConcurrentWebSocketSessionDecorator(sessionToDecorate, this.sendTimeLimit,
this.sendBufferSizeLimit);
}
else {
return new ConcurrentWebSocketSessionDecorator(sessionToDecorate, this.sendTimeLimit,
this.sendBufferSizeLimit, this.sendBufferOverflowStrategy);
}
}
/**
* An internal {@link WebSocketHandler} implementation to be used with native
* Web-Socket containers.
*
* Delegates all operations to the wrapping {@link IntegrationWebSocketContainer}
* and its {@link WebSocketListener}.
*/
private class IntegrationWebSocketHandler implements WebSocketHandler, SubProtocolCapable {
IntegrationWebSocketHandler() {
}
@Override
public List getSubProtocols() {
return IntegrationWebSocketContainer.this.getSubProtocols();
}
@Override
public void afterConnectionEstablished(WebSocketSession sessionToDecorate)
throws Exception { // NOSONAR
WebSocketSession session = decorateSession(sessionToDecorate);
IntegrationWebSocketContainer.this.sessions.put(session.getId(), session);
if (IntegrationWebSocketContainer.this.logger.isDebugEnabled()) {
IntegrationWebSocketContainer.this.logger.debug("Started WebSocket session = " +
session.getId() + ", number of sessions = " +
IntegrationWebSocketContainer.this.sessions.size());
}
if (IntegrationWebSocketContainer.this.messageListener != null) {
IntegrationWebSocketContainer.this.messageListener.afterSessionStarted(session);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
throws Exception { // NOSONAR
WebSocketSession removed = IntegrationWebSocketContainer.this.sessions.remove(session.getId());
if (removed != null && IntegrationWebSocketContainer.this.messageListener != null) {
IntegrationWebSocketContainer.this.messageListener.afterSessionEnded(session, closeStatus);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception)
throws Exception { // NOSONAR
IntegrationWebSocketContainer.this.sessions.remove(session.getId());
ReflectionUtils.rethrowException(exception);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage> message)
throws Exception { // NOSONAR
if (IntegrationWebSocketContainer.this.messageListener != null) {
IntegrationWebSocketContainer.this.messageListener.onMessage(session, message);
}
else if (IntegrationWebSocketContainer.this.logger.isInfoEnabled()) {
IntegrationWebSocketContainer.this.logger.info("This 'WebSocketHandlerContainer' isn't " +
"configured with 'WebSocketMessageListener'." +
" Received messages are ignored. Current message is: " + message);
}
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
}