Add reactive websocket auto-configuration for Tomcat

This commit adds a `TomcatWebSocketReactiveWebServerCustomizer`
that customizes the Tomcat context to accept WebSockets connections.
Since reactive servers don't use the JSR 356 for that support,
only Tomcat customization is required for now.

This commit also reorders the server auto-configuration
so that undertow has a chance to be auto-configured before
reactor-netty, which should be a popular dependency thanks to
its HTTP client library.

The existing WebSocket infrastructure for Serlvet based containers
has been moved to a dedicated package and renamed accordingly.

Fixes gh-9113
This commit is contained in:
Brian Clozel 2017-05-12 11:29:12 +02:00
parent 1835dd7b94
commit 89c284cb13
17 changed files with 272 additions and 55 deletions

View File

@ -50,8 +50,8 @@ import org.springframework.util.ObjectUtils;
@Import({ ReactiveWebServerAutoConfiguration.BeanPostProcessorsRegistrar.class,
ReactiveWebServerConfiguration.TomcatAutoConfiguration.class,
ReactiveWebServerConfiguration.JettyAutoConfiguration.class,
ReactiveWebServerConfiguration.ReactorNettyAutoConfiguration.class,
ReactiveWebServerConfiguration.UndertowAutoConfiguration.class })
ReactiveWebServerConfiguration.UndertowAutoConfiguration.class,
ReactiveWebServerConfiguration.ReactorNettyAutoConfiguration.class})
public class ReactiveWebServerAutoConfiguration {
@ConditionalOnMissingBean

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2017 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.boot.autoconfigure.websocket.reactive;
import org.apache.catalina.Context;
import org.apache.tomcat.websocket.server.WsContextListener;
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.core.Ordered;
/**
* WebSocket customizer for {@link TomcatReactiveWebServerFactory}.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class TomcatWebSocketReactiveWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatReactiveWebServerFactory>, Ordered {
@Override
public void customize(TomcatReactiveWebServerFactory factory) {
factory.addContextCustomizers(new TomcatContextCustomizer() {
@Override
public void customize(Context context) {
context.addApplicationListener(WsContextListener.class.getName());
}
});
}
@Override
public int getOrder() {
return 0;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2012-2017 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.boot.autoconfigure.websocket.reactive;
import javax.servlet.Servlet;
import javax.websocket.server.ServerContainer;
import org.apache.catalina.startup.Tomcat;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Auto configuration for WebSocket reactive server in Tomcat, Jetty or Undertow.
* Requires the appropriate WebSocket modules to be on the classpath.
* <p>
* If Tomcat's WebSocket support is detected on the classpath we add a customizer that
* installs the Tomcat WebSocket initializer.
*
* @author Brian Clozel
*/
@Configuration
@ConditionalOnClass({ Servlet.class, ServerContainer.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
@AutoConfigureBefore(ReactiveWebServerAutoConfiguration.class)
public class WebSocketReactiveAutoConfiguration {
@Configuration
@ConditionalOnClass(name = "org.apache.tomcat.websocket.server.WsSci", value = Tomcat.class)
static class TomcatWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketReactiveWebServerCustomizer")
public TomcatWebSocketReactiveWebServerCustomizer websocketContainerCustomizer() {
return new TomcatWebSocketReactiveWebServerCustomizer();
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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.
*/
/**
* Auto-configuration for WebSocket support in reactive web servers.
*/
package org.springframework.boot.autoconfigure.websocket.reactive;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import org.eclipse.jetty.util.thread.ShutdownThread;
import org.eclipse.jetty.webapp.AbstractConfiguration;
@ -32,9 +32,9 @@ import org.springframework.core.Ordered;
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.2.0
* @since 2.0.0
*/
public class JettyWebSocketContainerCustomizer
public class JettyWebSocketServletWebServerCustomizer
implements WebServerFactoryCustomizer<JettyServletWebServerFactory>, Ordered {
@Override

View File

@ -14,9 +14,8 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import org.apache.catalina.Context;
import org.apache.tomcat.websocket.server.WsContextListener;
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
@ -30,21 +29,15 @@ import org.springframework.core.Ordered;
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.2.0
* @since 2.0.0
*/
public class TomcatWebSocketContainerCustomizer
public class TomcatWebSocketServletWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>, Ordered {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers(new TomcatContextCustomizer() {
@Override
public void customize(Context context) {
context.addApplicationListener(WsContextListener.class.getName());
}
});
factory.addContextCustomizers((TomcatContextCustomizer) context ->
context.addApplicationListener(WsContextListener.class.getName()));
}
@Override

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
@ -28,9 +28,9 @@ import org.springframework.core.Ordered;
* WebSocket customizer for {@link UndertowServletWebServerFactory}.
*
* @author Phillip Webb
* @since 1.2.0
* @since 2.0.0
*/
public class UndertowWebSocketContainerCustomizer
public class UndertowWebSocketServletWebServerCustomizer
implements WebServerFactoryCustomizer<UndertowServletWebServerFactory>, Ordered {
@Override

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import java.util.List;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import javax.servlet.Servlet;
import javax.websocket.server.ServerContainer;
@ -32,11 +32,11 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Auto configuration for websocket server in embedded Tomcat, Jetty or Undertow. Requires
* the appropriate WebSocket modules to be on the classpath.
* Auto configuration for WebSocket servlet server in embedded Tomcat, Jetty or Undertow.
* Requires the appropriate WebSocket modules to be on the classpath.
* <p>
* If Tomcat's WebSocket support is detected on the classpath we add a customizer that
* installs the Tomcat Websocket initializer. In a non-embedded server it should already
* installs the Tomcat WebSocket initializer. In a non-embedded server it should already
* be there.
* <p>
* If Jetty's WebSocket support is detected on the classpath we add a configuration that
@ -44,7 +44,7 @@ import org.springframework.context.annotation.Configuration;
* already be there.
* <p>
* If Undertow's WebSocket support is detected on the classpath we add a customizer that
* installs the Undertow Websocket DeploymentInfo Customizer. In a non-embedded server it
* installs the Undertow WebSocket DeploymentInfo Customizer. In a non-embedded server it
* should already be there.
*
* @author Dave Syer
@ -55,16 +55,16 @@ import org.springframework.context.annotation.Configuration;
@ConditionalOnClass({ Servlet.class, ServerContainer.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureBefore(ServletWebServerFactoryAutoConfiguration.class)
public class WebSocketAutoConfiguration {
public class WebSocketServletAutoConfiguration {
@Configuration
@ConditionalOnClass(name = "org.apache.tomcat.websocket.server.WsSci", value = Tomcat.class)
static class TomcatWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketContainerCustomizer")
public TomcatWebSocketContainerCustomizer websocketContainerCustomizer() {
return new TomcatWebSocketContainerCustomizer();
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
public TomcatWebSocketServletWebServerCustomizer websocketContainerCustomizer() {
return new TomcatWebSocketServletWebServerCustomizer();
}
}
@ -74,9 +74,9 @@ public class WebSocketAutoConfiguration {
static class JettyWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketContainerCustomizer")
public JettyWebSocketContainerCustomizer websocketContainerCustomizer() {
return new JettyWebSocketContainerCustomizer();
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
public JettyWebSocketServletWebServerCustomizer websocketContainerCustomizer() {
return new JettyWebSocketServletWebServerCustomizer();
}
}
@ -86,9 +86,9 @@ public class WebSocketAutoConfiguration {
static class UndertowWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketContainerCustomizer")
public UndertowWebSocketContainerCustomizer websocketContainerCustomizer() {
return new UndertowWebSocketContainerCustomizer();
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
public UndertowWebSocketServletWebServerCustomizer websocketContainerCustomizer() {
return new UndertowWebSocketServletWebServerCustomizer();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2017 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.
@ -15,6 +15,6 @@
*/
/**
* Auto-configuration for Spring WebSocket.
* Auto-configuration for WebSocket support in servlet web servers.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;

View File

@ -118,8 +118,9 @@ org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfigurati
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
# Failure analyzers

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import java.lang.reflect.Type;
import java.util.ArrayList;
@ -207,10 +207,10 @@ public class WebSocketMessagingAutoConfigurationTests {
@EnableWebSocket
@EnableConfigurationProperties
@EnableWebSocketMessageBroker
@ImportAutoConfiguration({ JacksonAutoConfiguration.class,
@ImportAutoConfiguration({JacksonAutoConfiguration.class,
ServletWebServerFactoryAutoConfiguration.class,
WebSocketMessagingAutoConfiguration.class,
DispatcherServletAutoConfiguration.class })
DispatcherServletAutoConfiguration.class})
static class WebSocketMessagingConfiguration
extends AbstractWebSocketMessageBrokerConfigurer {
@ -235,8 +235,8 @@ public class WebSocketMessagingAutoConfigurationTests {
}
@Bean
public TomcatWebSocketContainerCustomizer tomcatCustomizer() {
return new TomcatWebSocketContainerCustomizer();
public TomcatWebSocketServletWebServerCustomizer tomcatCustomizer() {
return new TomcatWebSocketServletWebServerCustomizer();
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.websocket;
package org.springframework.boot.autoconfigure.websocket.servlet;
import javax.websocket.server.ServerContainer;
@ -33,11 +33,11 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebSocketAutoConfiguration}
* Tests for {@link WebSocketServletAutoConfiguration}
*
* @author Andy Wilkinson
*/
public class WebSocketAutoConfigurationTests {
public class WebSocketServletAutoConfigurationTests {
private AnnotationConfigServletWebServerApplicationContext context;
@ -56,13 +56,13 @@ public class WebSocketAutoConfigurationTests {
@Test
public void tomcatServerContainerIsAvailableFromTheServletContext() {
serverContainerIsAvailableFromTheServletContext(TomcatConfiguration.class,
WebSocketAutoConfiguration.TomcatWebSocketConfiguration.class);
WebSocketServletAutoConfiguration.TomcatWebSocketConfiguration.class);
}
@Test
public void jettyServerContainerIsAvailableFromTheServletContext() {
serverContainerIsAvailableFromTheServletContext(JettyConfiguration.class,
WebSocketAutoConfiguration.JettyWebSocketConfiguration.class);
WebSocketServletAutoConfiguration.JettyWebSocketConfiguration.class);
}
private void serverContainerIsAvailableFromTheServletContext(

View File

@ -17,7 +17,12 @@
package org.springframework.boot.web.embedded.tomcat;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.loader.WebappLoader;
@ -48,6 +53,8 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
private String protocol = DEFAULT_PROTOCOL;
private List<TomcatContextCustomizer> tomcatContextCustomizers = new ArrayList<>();
/**
* Create a new {@link TomcatServletWebServerFactory} instance.
*/
@ -97,9 +104,20 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
context.setLoader(loader);
Tomcat.addServlet(context, "httpHandlerServlet", servlet);
context.addServletMappingDecoded("/", "httpHandlerServlet");
configureContext(context);
host.addChild(context);
}
/**
* Configure the Tomcat {@link Context}.
* @param context the Tomcat context
*/
protected void configureContext(Context context) {
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}
// Needs to be protected so it can be used by subclasses
protected void customizeConnector(Connector connector) {
int port = (getPort() >= 0 ? getPort() : 0);
@ -122,6 +140,39 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
}
}
/**
* Set {@link TomcatContextCustomizer}s that should be applied to the Tomcat
* {@link Context} . Calling this method will replace any existing customizers.
* @param tomcatContextCustomizers the customizers to set
*/
public void setTomcatContextCustomizers(
Collection<? extends TomcatContextCustomizer> tomcatContextCustomizers) {
Assert.notNull(tomcatContextCustomizers,
"TomcatContextCustomizers must not be null");
this.tomcatContextCustomizers = new ArrayList<>(tomcatContextCustomizers);
}
/**
* Returns a mutable collection of the {@link TomcatContextCustomizer}s that will be
* applied to the Tomcat {@link Context} .
* @return the listeners that will be applied
*/
public Collection<TomcatContextCustomizer> getTomcatContextCustomizers() {
return this.tomcatContextCustomizers;
}
/**
* Add {@link TomcatContextCustomizer}s that should be added to the Tomcat
* {@link Context}.
* @param tomcatContextCustomizers the customizers to add
*/
public void addContextCustomizers(
TomcatContextCustomizer... tomcatContextCustomizers) {
Assert.notNull(tomcatContextCustomizers,
"TomcatContextCustomizers must not be null");
this.tomcatContextCustomizers.addAll(Arrays.asList(tomcatContextCustomizers));
}
/**
* Factory method called to create the {@link TomcatWebServer}. Subclasses can
* override this method to return a different {@link TomcatWebServer} or apply

View File

@ -88,4 +88,19 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
return getAddress().getHostAddress();
}
public void setBufferSize(Integer bufferSize) {
this.bufferSize = bufferSize;
}
public void setIoThreads(Integer ioThreads) {
this.ioThreads = ioThreads;
}
public void setWorkerThreads(Integer workerThreads) {
this.workerThreads = workerThreads;
}
public void setDirectBuffers(Boolean directBuffers) {
this.directBuffers = directBuffers;
}
}

View File

@ -16,8 +16,18 @@
package org.springframework.boot.web.embedded.tomcat;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import java.util.Arrays;
import org.apache.catalina.Context;
import org.junit.Test;
import org.mockito.InOrder;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.http.server.reactive.HttpHandler;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link TomcatReactiveWebServerFactory}.
@ -28,8 +38,23 @@ public class TomcatReactiveWebServerFactoryTests
extends AbstractReactiveWebServerFactoryTests {
@Override
protected AbstractReactiveWebServerFactory getFactory() {
protected TomcatReactiveWebServerFactory getFactory() {
return new TomcatReactiveWebServerFactory(0);
}
@Test
public void tomcatCustomizers() throws Exception {
TomcatReactiveWebServerFactory factory = getFactory();
TomcatContextCustomizer[] listeners = new TomcatContextCustomizer[4];
for (int i = 0; i < listeners.length; i++) {
listeners[i] = mock(TomcatContextCustomizer.class);
}
factory.setTomcatContextCustomizers(Arrays.asList(listeners[0], listeners[1]));
factory.addContextCustomizers(listeners[2], listeners[3]);
this.webServer = factory.getWebServer(mock(HttpHandler.class));
InOrder ordered = inOrder((Object[]) listeners);
for (TomcatContextCustomizer listener : listeners) {
ordered.verify(listener).customize(any(Context.class));
}
}
}

View File

@ -16,7 +16,6 @@
package org.springframework.boot.web.embedded.undertow;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
/**
@ -28,7 +27,7 @@ public class UndertowReactiveWebServerFactoryTests
extends AbstractReactiveWebServerFactoryTests {
@Override
protected AbstractReactiveWebServerFactory getFactory() {
protected UndertowReactiveWebServerFactory getFactory() {
return new UndertowReactiveWebServerFactory(0);
}