Polish WebSocket/STOMP Java config

Ensure configuration provided for WebSocketHandler's (eg interceptors,
or HandshakeHandler) are passed on to the SockJsService if congiured.

Better separate Servlet-specific parts of the configuration to make it
more obvious where non-Servlet alternatives could fit in.

Add more tests.

Improve WebSocket integration tests.
This commit is contained in:
Rossen Stoyanchev
2013-08-30 23:23:56 -04:00
parent 5d69700515
commit e21bbdd933
19 changed files with 1105 additions and 366 deletions

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2002-2013 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
*
* http://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.web.socket.server.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.DefaultHandshakeHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler;
/**
* Base class for {@link WebSocketHandlerRegistration}s that gathers all the configuration
* options but allows sub-classes to put together the actual HTTP request mappings.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractWebSocketHandlerRegistration<M> implements WebSocketHandlerRegistration {
private MultiValueMap<WebSocketHandler, String> handlerMap = new LinkedMultiValueMap<WebSocketHandler, String>();
private HandshakeInterceptor[] interceptors;
private HandshakeHandler handshakeHandler;
private SockJsServiceRegistration sockJsServiceRegistration;
private final TaskScheduler sockJsTaskScheduler;
public AbstractWebSocketHandlerRegistration(TaskScheduler defaultTaskScheduler) {
this.sockJsTaskScheduler = defaultTaskScheduler;
}
@Override
public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) {
Assert.notNull(handler);
Assert.notEmpty(paths);
this.handlerMap.put(handler, Arrays.asList(paths));
return this;
}
@Override
public WebSocketHandlerRegistration setHandshakeHandler(HandshakeHandler handshakeHandler) {
this.handshakeHandler = handshakeHandler;
return this;
}
public HandshakeHandler getHandshakeHandler() {
return handshakeHandler;
}
@Override
public WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors) {
this.interceptors = interceptors;
return this;
}
protected HandshakeInterceptor[] getInterceptors() {
return this.interceptors;
}
/**
* @param interceptors the interceptors to set
*/
public void setInterceptors(HandshakeInterceptor[] interceptors) {
this.interceptors = interceptors;
}
@Override
public SockJsServiceRegistration withSockJS() {
this.sockJsServiceRegistration = new SockJsServiceRegistration(this.sockJsTaskScheduler);
this.sockJsServiceRegistration.setInterceptors(this.interceptors);
if (this.handshakeHandler != null) {
WebSocketTransportHandler transportHandler = new WebSocketTransportHandler(this.handshakeHandler);
this.sockJsServiceRegistration.setTransportHandlerOverrides(transportHandler);
}
return this.sockJsServiceRegistration;
}
final M getMappings() {
M mappings = createMappings();
if (this.sockJsServiceRegistration != null) {
SockJsService sockJsService = this.sockJsServiceRegistration.getSockJsService(getAllPrefixes());
for (WebSocketHandler wsHandler : this.handlerMap.keySet()) {
for (String path : this.handlerMap.get(wsHandler)) {
String pathPattern = path.endsWith("/") ? path + "**" : path + "/**";
addSockJsServiceMapping(mappings, sockJsService, wsHandler, pathPattern);
}
}
}
else {
HandshakeHandler handshakeHandler = getOrCreateHandshakeHandler();
for (WebSocketHandler wsHandler : this.handlerMap.keySet()) {
for (String path : this.handlerMap.get(wsHandler)) {
addWebSocketHandlerMapping(mappings, wsHandler, handshakeHandler, this.interceptors, path);
}
}
}
return mappings;
}
private final String[] getAllPrefixes() {
List<String> all = new ArrayList<String>();
for (List<String> prefixes: this.handlerMap.values()) {
all.addAll(prefixes);
}
return all.toArray(new String[all.size()]);
}
private HandshakeHandler getOrCreateHandshakeHandler() {
return (this.handshakeHandler != null) ? this.handshakeHandler : new DefaultHandshakeHandler();
}
protected abstract M createMappings();
protected abstract void addSockJsServiceMapping(M mappings, SockJsService sockJsService,
WebSocketHandler handler, String pathPattern);
protected abstract void addWebSocketHandlerMapping(M mappings, WebSocketHandler wsHandler,
HandshakeHandler handshakeHandler, HandshakeInterceptor[] interceptors, String path);
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2002-2013 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
*
* http://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.web.socket.server.config;
import java.util.Arrays;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsService;
/**
* A helper class for configuring {@link WebSocketHandler} request handling
* including SockJS fallback options.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ServletWebSocketHandlerRegistration
extends AbstractWebSocketHandlerRegistration<MultiValueMap<HttpRequestHandler, String>> {
public ServletWebSocketHandlerRegistration(TaskScheduler sockJsTaskScheduler) {
super(sockJsTaskScheduler);
}
@Override
protected MultiValueMap<HttpRequestHandler, String> createMappings() {
return new LinkedMultiValueMap<HttpRequestHandler, String>();
}
@Override
protected void addSockJsServiceMapping(MultiValueMap<HttpRequestHandler, String> mappings,
SockJsService sockJsService, WebSocketHandler handler, String pathPattern) {
SockJsHttpRequestHandler httpHandler = new SockJsHttpRequestHandler(sockJsService, handler);
mappings.add(httpHandler, pathPattern);
}
@Override
protected void addWebSocketHandlerMapping(MultiValueMap<HttpRequestHandler, String> mappings,
WebSocketHandler wsHandler, HandshakeHandler handshakeHandler,
HandshakeInterceptor[] interceptors, String path) {
WebSocketHttpRequestHandler httpHandler = new WebSocketHttpRequestHandler(wsHandler, handshakeHandler);
if (!ObjectUtils.isEmpty(interceptors)) {
httpHandler.setHandshakeInterceptors(Arrays.asList(interceptors));
}
mappings.add(httpHandler, path);
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2002-2013 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
*
* http://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.web.socket.server.config;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
/**
* A {@link WebSocketHandlerRegistry} that maps {@link WebSocketHandler}s to URLs for use
* in a Servlet container.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ServletWebSocketHandlerRegistry implements WebSocketHandlerRegistry {
private final List<ServletWebSocketHandlerRegistration> registrations =
new ArrayList<ServletWebSocketHandlerRegistration>();
private TaskScheduler sockJsTaskScheduler;
public ServletWebSocketHandlerRegistry(ThreadPoolTaskScheduler sockJsTaskScheduler) {
this.sockJsTaskScheduler = sockJsTaskScheduler;
}
@Override
public WebSocketHandlerRegistration addHandler(WebSocketHandler webSocketHandler, String... paths) {
ServletWebSocketHandlerRegistration r = new ServletWebSocketHandlerRegistration(this.sockJsTaskScheduler);
r.addHandler(webSocketHandler, paths);
this.registrations.add(r);
return r;
}
/**
* Returns a {@link HandlerMapping} with mapped {@link HttpRequestHandler}s.
*/
AbstractHandlerMapping getHandlerMapping() {
Map<String, Object> urlMap = new LinkedHashMap<String, Object>();
for (ServletWebSocketHandlerRegistration registration : this.registrations) {
MultiValueMap<HttpRequestHandler, String> mappings = registration.getMappings();
for (HttpRequestHandler httpHandler : mappings.keySet()) {
for (String pattern : mappings.get(httpHandler)) {
urlMap.put(pattern, httpHandler);
}
}
}
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setUrlMap(urlMap);
return hm;
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.web.socket.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
/**
@@ -32,9 +33,11 @@ public class WebSocketConfigurationSupport {
@Bean
public HandlerMapping webSocketHandlerMapping() {
WebSocketHandlerRegistry registry = new WebSocketHandlerRegistry(defaultSockJsTaskScheduler());
ServletWebSocketHandlerRegistry registry = new ServletWebSocketHandlerRegistry(defaultSockJsTaskScheduler());
registerWebSocketHandlers(registry);
return registry.getHandlerMapping();
AbstractHandlerMapping hm = registry.getHandlerMapping();
hm.setOrder(1);
return hm;
}
protected void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
@@ -42,7 +45,20 @@ public class WebSocketConfigurationSupport {
/**
* The default TaskScheduler to use if none is configured via
* {@link SockJsServiceRegistration#setTaskScheduler()}
* {@link SockJsServiceRegistration#setTaskScheduler()}, i.e.
* <pre class="code">
* &#064;Configuration
* &#064;EnableWebSocket
* public class WebSocketConfig implements WebSocketConfigurer {
*
* public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
* registry.addHandler(myWsHandler(), "/echo").withSockJS().setTaskScheduler(myScheduler());
* }
*
* // ...
*
* }
* </pre>
*/
@Bean
public ThreadPoolTaskScheduler defaultSockJsTaskScheduler() {

View File

@@ -16,110 +16,37 @@
package org.springframework.web.socket.server.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.DefaultHandshakeHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsService;
/**
* A helper class for configuring {@link WebSocketHandler} request handling
* including SockJS fallback options.
* Provides methods for configuring a WebSocket handler.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class WebSocketHandlerRegistration {
public interface WebSocketHandlerRegistration {
private MultiValueMap<WebSocketHandler, String> handlerMap =
new LinkedMultiValueMap<WebSocketHandler, String>();
/**
* Add more handlers that will share the same configuration (interceptors, SockJS
* config, etc)
*/
WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths);
private final List<HandshakeInterceptor> interceptors = new ArrayList<HandshakeInterceptor>();
/**
* Configure interceptors for the handshake request.
*/
WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors);
private HandshakeHandler handshakeHandler;
/**
* Configure the HandshakeHandler to use.
*/
WebSocketHandlerRegistration setHandshakeHandler(HandshakeHandler handshakeHandler);
private SockJsServiceRegistration sockJsServiceRegistration;
/**
* Enable SockJS fallback options.
*/
SockJsServiceRegistration withSockJS();
private final TaskScheduler defaultTaskScheduler;
public WebSocketHandlerRegistration(TaskScheduler defaultTaskScheduler) {
this.defaultTaskScheduler = defaultTaskScheduler;
}
public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) {
Assert.notNull(handler);
Assert.notEmpty(paths);
this.handlerMap.put(handler, Arrays.asList(paths));
return this;
}
public WebSocketHandlerRegistration setHandshakeHandler(HandshakeHandler handshakeHandler) {
this.handshakeHandler = handshakeHandler;
return this;
}
public HandshakeHandler getHandshakeHandler() {
return handshakeHandler;
}
public void addInterceptors(HandshakeInterceptor... interceptors) {
this.interceptors.addAll(Arrays.asList(interceptors));
}
public SockJsServiceRegistration withSockJS() {
this.sockJsServiceRegistration = new SockJsServiceRegistration(this.defaultTaskScheduler);
this.sockJsServiceRegistration.setInterceptors(
this.interceptors.toArray(new HandshakeInterceptor[this.interceptors.size()]));
return this.sockJsServiceRegistration;
}
MultiValueMap<HttpRequestHandler, String> getMappings() {
MultiValueMap<HttpRequestHandler, String> mappings = new LinkedMultiValueMap<HttpRequestHandler, String>();
if (this.sockJsServiceRegistration == null) {
HandshakeHandler handshakeHandler = getOrCreateHandshakeHandler();
for (WebSocketHandler handler : this.handlerMap.keySet()) {
for (String path : this.handlerMap.get(handler)) {
WebSocketHttpRequestHandler httpHandler = new WebSocketHttpRequestHandler(handler, handshakeHandler);
httpHandler.setHandshakeInterceptors(this.interceptors);
mappings.add(httpHandler, path);
}
}
}
else {
SockJsService sockJsService = this.sockJsServiceRegistration.getSockJsService(getAllPrefixes());
for (WebSocketHandler handler : this.handlerMap.keySet()) {
for (String path : this.handlerMap.get(handler)) {
SockJsHttpRequestHandler httpHandler = new SockJsHttpRequestHandler(sockJsService, handler);
mappings.add(httpHandler, path.endsWith("/") ? path + "**" : path + "/**");
}
}
}
return mappings;
}
private HandshakeHandler getOrCreateHandshakeHandler() {
return (this.handshakeHandler != null) ? this.handshakeHandler : new DefaultHandshakeHandler();
}
private final String[] getAllPrefixes() {
List<String> all = new ArrayList<String>();
for (List<String> prefixes: this.handlerMap.values()) {
all.addAll(prefixes);
}
return all.toArray(new String[all.size()]);
}
}
}

View File

@@ -16,76 +16,19 @@
package org.springframework.web.socket.server.config;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
/**
* A helper class for configuring {@link WebSocketHandler} request handling.
* Provides methods for configuring {@link WebSocketHandler} request mappings.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class WebSocketHandlerRegistry {
private final List<WebSocketHandlerRegistration> registrations = new ArrayList<WebSocketHandlerRegistration>();
private int order = 1;
private TaskScheduler defaultTaskScheduler;
public WebSocketHandlerRegistry(ThreadPoolTaskScheduler defaultSockJsTaskScheduler) {
this.defaultTaskScheduler = defaultSockJsTaskScheduler;
}
public WebSocketHandlerRegistration addHandler(WebSocketHandler wsHandler, String... paths) {
WebSocketHandlerRegistration r = new WebSocketHandlerRegistration(this.defaultTaskScheduler);
r.addHandler(wsHandler, paths);
this.registrations.add(r);
return r;
}
protected List<WebSocketHandlerRegistration> getRegistrations() {
return this.registrations;
}
public interface WebSocketHandlerRegistry {
/**
* Specify the order to use for WebSocket {@link HandlerMapping} relative to other
* handler mappings configured in the Spring MVC configuration. The default value is 1.
* Configure a WebSocketHandler at the specified URL paths.
*/
public void setOrder(int order) {
this.order = order;
}
WebSocketHandlerRegistration addHandler(WebSocketHandler webSocketHandler, String... paths);
/**
* Returns a handler mapping with the mapped ViewControllers; or {@code null} in case of no registrations.
*/
AbstractHandlerMapping getHandlerMapping() {
Map<String, Object> urlMap = new LinkedHashMap<String, Object>();
for (WebSocketHandlerRegistration registration : this.registrations) {
MultiValueMap<HttpRequestHandler, String> mappings = registration.getMappings();
for (HttpRequestHandler httpHandler : mappings.keySet()) {
for (String pattern : mappings.get(httpHandler)) {
urlMap.put(pattern, httpHandler);
}
}
}
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setOrder(this.order);
hm.setUrlMap(urlMap);
return hm;
}
}
}

View File

@@ -61,6 +61,10 @@ public class WebSocketTransportHandler extends TransportHandlerSupport
return TransportType.WEBSOCKET;
}
public HandshakeHandler getHandshakeHandler() {
return this.handshakeHandler;
}
@Override
public AbstractSockJsSession createSession(String sessionId, WebSocketHandler wsHandler,
Map<String, Object> attributes) {