Skip to content

Commit 4c0da58

Browse files
committed
Add Java config support for WebSocket and STOMP
Issue: SPR-10835
1 parent 4b6a9ac commit 4c0da58

31 files changed

+2236
-290
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ project("spring-messaging") {
317317
compile(project(":spring-core"))
318318
compile(project(":spring-context"))
319319
optional(project(":spring-websocket"))
320+
optional(project(":spring-webmvc"))
320321
optional("com.fasterxml.jackson.core:jackson-databind:2.2.0")
321322
optional("org.projectreactor:reactor-core:1.0.0.M2")
322323
optional("org.projectreactor:reactor-tcp:1.0.0.M2")

spring-messaging/src/main/java/org/springframework/messaging/handler/websocket/SubProtocolWebSocketHandler.java

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.messaging.handler.websocket;
1818

1919
import java.util.Arrays;
20+
import java.util.HashSet;
2021
import java.util.List;
2122
import java.util.Map;
23+
import java.util.Set;
2224
import java.util.TreeMap;
2325
import java.util.concurrent.ConcurrentHashMap;
2426

@@ -79,21 +81,25 @@ public SubProtocolWebSocketHandler(MessageChannel outputChannel) {
7981
public void setProtocolHandlers(List<SubProtocolHandler> protocolHandlers) {
8082
this.protocolHandlers.clear();
8183
for (SubProtocolHandler handler: protocolHandlers) {
82-
List<String> protocols = handler.getSupportedProtocols();
83-
if (CollectionUtils.isEmpty(protocols)) {
84-
logger.warn("No sub-protocols, ignoring handler " + handler);
85-
continue;
86-
}
87-
for (String protocol: protocols) {
88-
SubProtocolHandler replaced = this.protocolHandlers.put(protocol, handler);
89-
if (replaced != null) {
90-
throw new IllegalStateException("Failed to map handler " + handler
91-
+ " to protocol '" + protocol + "', it is already mapped to handler " + replaced);
92-
}
93-
}
84+
addProtocolHandler(handler);
9485
}
95-
if ((this.protocolHandlers.size() == 1) &&(this.defaultProtocolHandler == null)) {
96-
this.defaultProtocolHandler = this.protocolHandlers.values().iterator().next();
86+
}
87+
88+
/**
89+
* Register a sub-protocol handler.
90+
*/
91+
public void addProtocolHandler(SubProtocolHandler handler) {
92+
List<String> protocols = handler.getSupportedProtocols();
93+
if (CollectionUtils.isEmpty(protocols)) {
94+
logger.warn("No sub-protocols, ignoring handler " + handler);
95+
return;
96+
}
97+
for (String protocol: protocols) {
98+
SubProtocolHandler replaced = this.protocolHandlers.put(protocol, handler);
99+
if ((replaced != null) && (replaced != handler) ) {
100+
throw new IllegalStateException("Failed to map handler " + handler
101+
+ " to protocol '" + protocol + "', it is already mapped to handler " + replaced);
102+
}
97103
}
98104
}
99105

@@ -128,10 +134,10 @@ public SubProtocolHandler getDefaultProtocolHandler() {
128134
@Override
129135
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
130136
this.sessions.put(session.getId(), session);
131-
getProtocolHandler(session).afterSessionStarted(session, this.outputChannel);
137+
findProtocolHandler(session).afterSessionStarted(session, this.outputChannel);
132138
}
133139

134-
protected final SubProtocolHandler getProtocolHandler(WebSocketSession session) {
140+
protected final SubProtocolHandler findProtocolHandler(WebSocketSession session) {
135141
SubProtocolHandler handler;
136142
String protocol = session.getAcceptedProtocol();
137143
if (!StringUtils.isEmpty(protocol)) {
@@ -140,16 +146,26 @@ protected final SubProtocolHandler getProtocolHandler(WebSocketSession session)
140146
"No handler for sub-protocol '" + protocol + "', handlers=" + this.protocolHandlers);
141147
}
142148
else {
143-
handler = this.defaultProtocolHandler;
144-
Assert.state(handler != null,
145-
"No sub-protocol was requested and a default sub-protocol handler was not configured");
149+
if (this.defaultProtocolHandler != null) {
150+
handler = this.defaultProtocolHandler;
151+
}
152+
else {
153+
Set<SubProtocolHandler> handlers = new HashSet<SubProtocolHandler>(this.protocolHandlers.values());
154+
if (handlers.size() == 1) {
155+
handler = handlers.iterator().next();
156+
}
157+
else {
158+
throw new IllegalStateException(
159+
"No sub-protocol was requested and a default sub-protocol handler was not configured");
160+
}
161+
}
146162
}
147163
return handler;
148164
}
149165

150166
@Override
151167
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
152-
getProtocolHandler(session).handleMessageFromClient(session, message, this.outputChannel);
168+
findProtocolHandler(session).handleMessageFromClient(session, message, this.outputChannel);
153169
}
154170

155171
@Override
@@ -168,7 +184,7 @@ public void handleMessage(Message<?> message) throws MessagingException {
168184
}
169185

170186
try {
171-
getProtocolHandler(session).handleMessageToClient(session, message);
187+
findProtocolHandler(session).handleMessageToClient(session, message);
172188
}
173189
catch (Exception e) {
174190
logger.error("Failed to send message to client " + message, e);
@@ -198,7 +214,7 @@ public void handleTransportError(WebSocketSession session, Throwable exception)
198214
@Override
199215
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
200216
this.sessions.remove(session.getId());
201-
getProtocolHandler(session).afterSessionEnded(session, closeStatus, this.outputChannel);
217+
findProtocolHandler(session).afterSessionEnded(session, closeStatus, this.outputChannel);
202218
}
203219

204220
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.messaging.simp.config;
18+
19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
23+
import org.springframework.messaging.MessageChannel;
24+
import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler;
25+
26+
import reactor.util.Assert;
27+
28+
29+
/**
30+
* Base class for message broker registration classes.
31+
*
32+
* @author Rossen Stoyanchev
33+
* @since 4.0
34+
*/
35+
public abstract class AbstractBrokerRegistration {
36+
37+
private final MessageChannel webSocketReplyChannel;
38+
39+
private final String[] destinationPrefixes;
40+
41+
42+
public AbstractBrokerRegistration(MessageChannel webSocketReplyChannel, String[] destinationPrefixes) {
43+
Assert.notNull(webSocketReplyChannel, "");
44+
this.webSocketReplyChannel = webSocketReplyChannel;
45+
this.destinationPrefixes = destinationPrefixes;
46+
}
47+
48+
49+
protected MessageChannel getWebSocketReplyChannel() {
50+
return this.webSocketReplyChannel;
51+
}
52+
53+
protected Collection<String> getDestinationPrefixes() {
54+
return (this.destinationPrefixes != null)
55+
? Arrays.<String>asList(this.destinationPrefixes) : Collections.<String>emptyList();
56+
}
57+
58+
protected abstract AbstractBrokerMessageHandler getMessageHandler();
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.messaging.simp.config;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.util.CollectionUtils;
25+
26+
27+
/**
28+
* A {@link WebSocketMessageBrokerConfiguration} extension that detects beans of type
29+
* {@link WebSocketMessageBrokerConfigurer} and delegates to all of them allowing callback
30+
* style customization of the configuration provided in
31+
* {@link WebSocketMessageBrokerConfigurationSupport}.
32+
*
33+
* <p>This class is typically imported via {@link EnableWebSocketMessageBroker}.
34+
*
35+
* @author Rossen Stoyanchev
36+
* @since 4.0
37+
*/
38+
@Configuration
39+
public class DelegatingWebSocketMessageBrokerConfiguration extends WebSocketMessageBrokerConfigurationSupport {
40+
41+
private List<WebSocketMessageBrokerConfigurer> configurers = new ArrayList<WebSocketMessageBrokerConfigurer>();
42+
43+
44+
@Autowired(required=false)
45+
public void setConfigurers(List<WebSocketMessageBrokerConfigurer> configurers) {
46+
if (CollectionUtils.isEmpty(configurers)) {
47+
return;
48+
}
49+
this.configurers.addAll(configurers);
50+
}
51+
52+
@Override
53+
protected void registerStompEndpoints(StompEndpointRegistry registry) {
54+
for (WebSocketMessageBrokerConfigurer c : this.configurers) {
55+
c.registerStompEndpoints(registry);
56+
}
57+
}
58+
59+
@Override
60+
protected void configureMessageBroker(MessageBrokerConfigurer configurer) {
61+
for (WebSocketMessageBrokerConfigurer c : this.configurers) {
62+
c.configureMessageBroker(configurer);
63+
}
64+
}
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package org.springframework.messaging.simp.config;
14+
15+
import java.lang.annotation.Documented;
16+
import java.lang.annotation.ElementType;
17+
import java.lang.annotation.Retention;
18+
import java.lang.annotation.RetentionPolicy;
19+
import java.lang.annotation.Target;
20+
21+
import org.springframework.context.annotation.Import;
22+
23+
24+
/**
25+
* Add this annotation to an {@code @Configuration} class to enable broker-backed
26+
* messaging over WebSocket using a higher-level messaging sub-protocol.
27+
*
28+
* <pre class="code">
29+
* &#064;Configuration
30+
* &#064;EnableWebSocketMessageBroker
31+
* public class MyWebSocketConfig {
32+
*
33+
* }
34+
* </pre>
35+
* <p>
36+
* Customize the imported configuration by implementing the
37+
* {@link WebSocketMessageBrokerConfigurer} interface:
38+
*
39+
* <pre class="code">
40+
* &#064;Configuration
41+
* &#064;EnableWebSocketMessageBroker
42+
* public class MyConfiguration implements implements WebSocketMessageBrokerConfigurer {
43+
*
44+
* &#064;Override
45+
* public void registerStompEndpoints(StompEndpointRegistry registry) {
46+
* registry.addEndpoint("/portfolio").withSockJS();
47+
* }
48+
*
49+
* &#064;Bean
50+
* public void configureMessageBroker(MessageBrokerConfigurer configurer) {
51+
* configurer.enableStompBrokerRelay("/queue/", "/topic/");
52+
* configurer.setAnnotationMethodDestinationPrefixes("/app/");
53+
* }
54+
* }
55+
* </pre>
56+
*
57+
* @author Rossen Stoyanchev
58+
* @since 4.0
59+
*/
60+
@Retention(RetentionPolicy.RUNTIME)
61+
@Target(ElementType.TYPE)
62+
@Documented
63+
@Import(DelegatingWebSocketMessageBrokerConfiguration.class)
64+
public @interface EnableWebSocketMessageBroker {
65+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.messaging.simp.config;
18+
19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
22+
import org.springframework.messaging.MessageChannel;
23+
import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler;
24+
25+
import reactor.util.Assert;
26+
27+
28+
/**
29+
* A helper class for configuring message broker options.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 4.0
33+
*/
34+
public class MessageBrokerConfigurer {
35+
36+
private final MessageChannel webSocketReplyChannel;
37+
38+
private SimpleBrokerRegistration simpleBroker;
39+
40+
private StompBrokerRelayRegistration stompRelay;
41+
42+
private String[] annotationMethodDestinationPrefixes;
43+
44+
45+
public MessageBrokerConfigurer(MessageChannel webSocketReplyChannel) {
46+
Assert.notNull(webSocketReplyChannel);
47+
this.webSocketReplyChannel = webSocketReplyChannel;
48+
}
49+
50+
public SimpleBrokerRegistration enableSimpleBroker(String... destinationPrefixes) {
51+
this.simpleBroker = new SimpleBrokerRegistration(this.webSocketReplyChannel, destinationPrefixes);
52+
return this.simpleBroker;
53+
}
54+
55+
public StompBrokerRelayRegistration enableStompBrokerRelay(String... destinationPrefixes) {
56+
this.stompRelay = new StompBrokerRelayRegistration(this.webSocketReplyChannel, destinationPrefixes);
57+
return this.stompRelay;
58+
}
59+
60+
public MessageBrokerConfigurer setAnnotationMethodDestinationPrefixes(String... destinationPrefixes) {
61+
this.annotationMethodDestinationPrefixes = destinationPrefixes;
62+
return this;
63+
}
64+
65+
protected AbstractBrokerMessageHandler getSimpleBroker() {
66+
initSimpleBrokerIfNecessary();
67+
return (this.simpleBroker != null) ? this.simpleBroker.getMessageHandler() : null;
68+
}
69+
70+
protected void initSimpleBrokerIfNecessary() {
71+
if ((this.simpleBroker == null) && (this.stompRelay == null)) {
72+
this.simpleBroker = new SimpleBrokerRegistration(this.webSocketReplyChannel, null);
73+
}
74+
}
75+
76+
protected AbstractBrokerMessageHandler getStompBrokerRelay() {
77+
return (this.stompRelay != null) ? this.stompRelay.getMessageHandler() : null;
78+
}
79+
80+
protected Collection<String> getAnnotationMethodDestinationPrefixes() {
81+
return (this.annotationMethodDestinationPrefixes != null)
82+
? Arrays.asList(this.annotationMethodDestinationPrefixes) : null;
83+
}
84+
}

0 commit comments

Comments
 (0)