From 656b509f0329a37701755c633268f0c68160d912 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 22 Feb 2017 17:13:07 +0100 Subject: [PATCH] Add support for reactive web servers auto-configuration This commit adds the auto-configuration for creating reactive `EmbeddedWebServer` instances. This adds support for the following servers: Reactor Netty, Tomcat, Jetty and Undertow. Fixes gh-8302 Fixes gh-8117 --- spring-boot-autoconfigure/pom.xml | 5 + .../DefaultReactiveWebServerCustomizer.java | 62 ++++ .../ReactiveWebServerAutoConfiguration.java | 92 +++++ .../ReactiveWebServerConfiguration.java | 45 +++ ...faultReactiveWebServerCustomizerTests.java | 63 ++++ .../webflux/MockReactiveWebServerFactory.java | 96 ++++++ ...activeWebServerAutoConfigurationTests.java | 122 +++++++ spring-boot/pom.xml | 9 + .../AbstractReactiveWebServerFactory.java | 30 ++ .../jetty/JettyReactiveWebServerFactory.java | 146 ++++++++ .../embedded/jetty/JettyWebServer.java | 236 +++++++++++++ .../ReactorNettyReactiveWebServerFactory.java | 69 ++++ .../reactor/ReactorNettyWebServer.java | 101 ++++++ .../TomcatReactiveWebServerFactory.java | 159 +++++++++ .../embedded/tomcat/TomcatWebServer.java | 314 ++++++++++++++++++ .../UndertowReactiveWebServerFactory.java | 100 ++++++ .../embedded/undertow/UndertowWebServer.java | 267 +++++++++++++++ .../JettyReactiveWebServerFactoryTests.java | 34 ++ ...torNettyReactiveWebServerFactoryTests.java | 35 ++ .../TomcatReactiveWebServerFactoryTests.java | 34 ++ ...UndertowReactiveWebServerFactoryTests.java | 34 ++ 21 files changed, 2053 insertions(+) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizer.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerConfiguration.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizerTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/MockReactiveWebServerFactory.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfigurationTests.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyWebServer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyWebServer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatWebServer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowWebServer.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactoryTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactoryTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactoryTests.java diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index ff941faba6..ddac4abdca 100755 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -100,6 +100,11 @@ de.flapdoodle.embed.mongo true + + io.projectreactor.ipc + reactor-netty + true + javax.cache cache-api diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizer.java new file mode 100644 index 0000000000..b136df8eea --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizer.java @@ -0,0 +1,62 @@ +/* + * 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.webflux; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.embedded.ConfigurableReactiveWebServer; +import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor; +import org.springframework.boot.context.embedded.ReactiveWebServerCustomizer; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.core.Ordered; + +/** + * Customizer used by an {@link ReactiveWebServerFactory} when an + * {@link EmbeddedServletContainerCustomizerBeanPostProcessor} is active. + * + * @author Brian Clozel + */ +public class DefaultReactiveWebServerCustomizer + implements ReactiveWebServerCustomizer, Ordered { + + private final ServerProperties serverProperties; + + public DefaultReactiveWebServerCustomizer(ServerProperties serverProperties) { + this.serverProperties = serverProperties; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(ConfigurableReactiveWebServer server) { + if (this.serverProperties.getPort() != null) { + server.setPort(this.serverProperties.getPort()); + } + if (this.serverProperties.getAddress() != null) { + server.setAddress(this.serverProperties.getAddress()); + } + if (this.serverProperties.getSsl() != null) { + server.setSsl(this.serverProperties.getSsl()); + } + if (this.serverProperties.getCompression() != null) { + server.setCompression(this.serverProperties.getCompression()); + } + + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfiguration.java new file mode 100644 index 0000000000..e96826b07f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * 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.webflux; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.embedded.ReactiveWebServerCustomizerBeanPostProcessor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.Ordered; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ObjectUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for a reactive web server. + * + * @author Brian Clozel + */ +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@Configuration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@EnableConfigurationProperties(ServerProperties.class) +@Import({ReactiveWebServerAutoConfiguration.BeanPostProcessorsRegistrar.class, + ReactiveWebServerConfiguration.ReactorNettyAutoConfiguration.class}) +public class ReactiveWebServerAutoConfiguration { + + @ConditionalOnMissingBean + @Bean + public DefaultReactiveWebServerCustomizer defaultReactiveWebServerCustomizer( + ServerProperties serverProperties) { + return new DefaultReactiveWebServerCustomizer(serverProperties); + } + + /** + * Registers a {@link ReactiveWebServerCustomizerBeanPostProcessor}. Registered + * via {@link ImportBeanDefinitionRegistrar} for early registration. + */ + public static class BeanPostProcessorsRegistrar + implements ImportBeanDefinitionRegistrar, BeanFactoryAware { + + private ConfigurableListableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableListableBeanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + if (this.beanFactory == null) { + return; + } + if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType( + ReactiveWebServerCustomizerBeanPostProcessor.class, true, false))) { + registry.registerBeanDefinition( + "reactiveWebServerCustomizerBeanPostProcessor", + new RootBeanDefinition(ReactiveWebServerCustomizerBeanPostProcessor.class)); + + } + } + + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerConfiguration.java new file mode 100644 index 0000000000..b90fe7d8ef --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.webflux; + +import reactor.ipc.netty.http.server.HttpServer; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.boot.context.embedded.reactor.ReactorNettyReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; + +/** + * Configuration classes for reactive web servers + *

Those should be {@code @Import} in a regular auto-configuration class + * to guarantee their order of execution. + * + * @author Brian Clozel + */ +abstract class ReactiveWebServerConfiguration { + + @ConditionalOnMissingBean(ReactiveWebServerFactory.class) + @ConditionalOnClass({HttpServer.class}) + static class ReactorNettyAutoConfiguration { + @Bean + public ReactorNettyReactiveWebServerFactory reactorNettyReactiveWebServerFactory() { + return new ReactorNettyReactiveWebServerFactory(); + } + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizerTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizerTests.java new file mode 100644 index 0000000000..5f12ef78ab --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/DefaultReactiveWebServerCustomizerTests.java @@ -0,0 +1,63 @@ +/* + * 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.webflux; + +import java.net.InetAddress; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.embedded.ConfigurableReactiveWebServer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DefaultReactiveWebServerCustomizer}. + * + * @author Brian Clozel + */ +public class DefaultReactiveWebServerCustomizerTests { + + private final ServerProperties properties = new ServerProperties(); + + private DefaultReactiveWebServerCustomizer customizer; + + + @Before + public void setup() throws Exception { + this.customizer = new DefaultReactiveWebServerCustomizer(this.properties); + } + + @Test + public void testCustomizeServerPort() throws Exception { + ConfigurableReactiveWebServer factory = mock(ConfigurableReactiveWebServer.class); + this.properties.setPort(9000); + this.customizer.customize(factory); + verify(factory).setPort(9000); + } + + @Test + public void testCustomizeServerAddress() throws Exception { + ConfigurableReactiveWebServer factory = mock(ConfigurableReactiveWebServer.class); + InetAddress address = mock(InetAddress.class); + this.properties.setAddress(address); + this.customizer.customize(factory); + verify(factory).setAddress(address); + } +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/MockReactiveWebServerFactory.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/MockReactiveWebServerFactory.java new file mode 100644 index 0000000000..1e3f63cd80 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/MockReactiveWebServerFactory.java @@ -0,0 +1,96 @@ +/* + * 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.webflux; + +import java.util.Map; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.EmbeddedWebServerException; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.http.server.reactive.HttpHandler; + +import static org.mockito.Mockito.spy; + +/** + * Mock {@link ReactiveWebServerFactory}. + * + * @author Brian Clozel + */ +public class MockReactiveWebServerFactory extends AbstractReactiveWebServerFactory { + + private MockReactiveWebServer webServer; + + @Override + public EmbeddedWebServer getReactiveHttpServer(HttpHandler httpHandler) { + this.webServer = spy(new MockReactiveWebServer(httpHandler, getPort())); + return this.webServer; + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(Map handlerMap) { + this.webServer = spy(new MockReactiveWebServer(handlerMap, getPort())); + return this.webServer; + } + + public MockReactiveWebServer getWebServer() { + return this.webServer; + } + + public static class MockReactiveWebServer implements EmbeddedWebServer { + + private final int port; + + private HttpHandler httpHandler; + + private Map httpHandlerMap; + + public MockReactiveWebServer(HttpHandler httpHandler, int port) { + this.httpHandler = httpHandler; + this.port = port; + } + + public MockReactiveWebServer(Map httpHandlerMap, int port) { + this.httpHandlerMap = httpHandlerMap; + this.port = port; + } + + public HttpHandler getHttpHandler() { + return this.httpHandler; + } + + public Map getHttpHandlerMap() { + return this.httpHandlerMap; + } + + @Override + public void start() throws EmbeddedWebServerException { + + } + + @Override + public void stop() throws EmbeddedWebServerException { + + } + + @Override + public int getPort() { + return this.port; + } + + } +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfigurationTests.java new file mode 100644 index 0000000000..9062b9e6d0 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/webflux/ReactiveWebServerAutoConfigurationTests.java @@ -0,0 +1,122 @@ +/* + * 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.webflux; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; + +import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; +import org.springframework.boot.context.embedded.ReactiveWebServerCustomizer; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.server.reactive.HttpHandler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReactiveWebServerAutoConfiguration}. + * + * @author Brian Clozel + */ +public class ReactiveWebServerAutoConfigurationTests { + + private ReactiveWebApplicationContext context; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createFromConfigClass() { + this.context = new ReactiveWebApplicationContext(BaseConfiguration.class); + + assertThat(this.context.getBeansOfType(ReactiveWebServerFactory.class)).hasSize(1); + assertThat(this.context.getBeansOfType(ReactiveWebServerCustomizer.class)).hasSize(1); + assertThat(this.context.getBeansOfType(DefaultReactiveWebServerCustomizer.class)).hasSize(1); + } + + @Test + public void missingHttpHandler() { + this.thrown.expect(ApplicationContextException.class); + this.thrown.expectMessage(Matchers.containsString("missing HttpHandler bean")); + this.context = new ReactiveWebApplicationContext(MissingHttpHandlerConfiguration.class); + } + + @Test + public void multipleHttpHandler() { + this.thrown.expect(ApplicationContextException.class); + this.thrown.expectMessage(Matchers + .containsString("multiple HttpHandler beans : httpHandler,additionalHttpHandler")); + this.context = new ReactiveWebApplicationContext(BaseConfiguration.class, TooManyHttpHandlers.class); + } + + @Test + public void customizeReactiveWebServer() { + this.context = new ReactiveWebApplicationContext(BaseConfiguration.class, + ReactiveWebServerCustomization.class); + MockReactiveWebServerFactory factory = this.context.getBean(MockReactiveWebServerFactory.class); + assertThat(factory.getPort()).isEqualTo(9000); + } + + @Configuration + @Import({MockWebServerAutoConfiguration.class, ReactiveWebServerAutoConfiguration.class}) + protected static class BaseConfiguration { + + @Bean + public HttpHandler httpHandler() { + return Mockito.mock(HttpHandler.class); + } + } + + @Configuration + @Import({MockWebServerAutoConfiguration.class, ReactiveWebServerAutoConfiguration.class}) + protected static class MissingHttpHandlerConfiguration { + + } + + @Configuration + protected static class TooManyHttpHandlers { + + @Bean + public HttpHandler additionalHttpHandler() { + return Mockito.mock(HttpHandler.class); + } + } + + @Configuration + protected static class ReactiveWebServerCustomization { + + @Bean + public ReactiveWebServerCustomizer reactiveWebServerCustomizer() { + return (server) -> server.setPort(9000); + } + } + + @Configuration + public static class MockWebServerAutoConfiguration { + + @Bean + public MockReactiveWebServerFactory mockReactiveWebServerFactory() { + return new MockReactiveWebServerFactory(); + } + } +} diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index 34ef7182fa..e53e99dfff 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -69,6 +69,11 @@ json-simple true + + io.projectreactor.ipc + reactor-netty + true + javax.jms javax.jms-api @@ -326,6 +331,10 @@ postgresql test + + io.projectreactor.addons + reactor-test + org.slf4j jcl-over-slf4j diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractReactiveWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractReactiveWebServerFactory.java index 002a5431d4..c7dc667625 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractReactiveWebServerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractReactiveWebServerFactory.java @@ -16,6 +16,12 @@ package org.springframework.boot.context.embedded; +import java.io.File; +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + /** * Abstract base class for {@link ReactiveWebServerFactory} implementations. * @@ -26,6 +32,8 @@ public abstract class AbstractReactiveWebServerFactory extends AbstractConfigurableReactiveWebServer implements ReactiveWebServerFactory { + protected final Log logger = LogFactory.getLog(getClass()); + public AbstractReactiveWebServerFactory() { } @@ -33,4 +41,26 @@ public abstract class AbstractReactiveWebServerFactory super(port); } + + /** + * Return the absolute temp dir for given web server. + * @param prefix servlet container name + * @return The temp dir for given servlet container. + */ + protected File createTempDir(String prefix) { + try { + File tempDir = File.createTempFile(prefix + ".", "." + getPort()); + tempDir.delete(); + tempDir.mkdir(); + tempDir.deleteOnExit(); + return tempDir; + } + catch (IOException ex) { + throw new EmbeddedWebServerException( + "Unable to create tempDir. java.io.tmpdir is set to " + + System.getProperty("java.io.tmpdir"), + ex); + } + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactory.java new file mode 100644 index 0000000000..898bea0d9e --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactory.java @@ -0,0 +1,146 @@ +/* + * 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.context.embedded.jetty; + +import java.net.InetSocketAddress; +import java.util.Map; + +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.thread.ThreadPool; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.JettyHttpHandlerAdapter; + +/** + * {@link ReactiveWebServerFactory} that can be used to create + * {@link JettyWebServer}s. + * + * @author Brian Clozel + */ +public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFactory { + + /** + * The number of acceptor threads to use. + */ + private int acceptors = -1; + + /** + * The number of selector threads to use. + */ + private int selectors = -1; + + private ThreadPool threadPool; + + /** + * Create a new {@link JettyEmbeddedServletContainerFactory} instance. + */ + public JettyReactiveWebServerFactory() { + super(); + } + + /** + * Create a new {@link JettyEmbeddedServletContainerFactory} that listens for requests + * using the specified port. + * @param port the port to listen on + */ + public JettyReactiveWebServerFactory(int port) { + super(port); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(HttpHandler httpHandler) { + JettyHttpHandlerAdapter servlet = new JettyHttpHandlerAdapter(httpHandler); + Server server = createJettyServer(servlet); + return new JettyWebServer(server, getPort() >= 0); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(Map handlerMap) { + JettyHttpHandlerAdapter servlet = new JettyHttpHandlerAdapter(handlerMap); + Server server = createJettyServer(servlet); + return new JettyWebServer(server, getPort() >= 0); + } + + protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { + int port = (getPort() >= 0 ? getPort() : 0); + InetSocketAddress address = new InetSocketAddress(getAddress(), port); + Server server = new Server(getThreadPool()); + server.addConnector(createConnector(address, server)); + ServletHolder servletHolder = new ServletHolder(servlet); + ServletContextHandler contextHandler = new ServletContextHandler(server, + "", false, false); + contextHandler.addServlet(servletHolder, "/"); + this.logger.info("Server initialized with port: " + port); + return server; + } + + private AbstractConnector createConnector(InetSocketAddress address, Server server) { + ServerConnector connector = new ServerConnector(server, this.acceptors, + this.selectors); + connector.setHost(address.getHostName()); + connector.setPort(address.getPort()); + for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) { + if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) { + ((HttpConfiguration.ConnectionFactory) connectionFactory) + .getHttpConfiguration().setSendServerVersion(false); + } + } + return connector; + } + + /** + * Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}. + * @return a Jetty {@link ThreadPool} or {@code null} + */ + public ThreadPool getThreadPool() { + return this.threadPool; + } + + /** + * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to + * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. + * @param threadPool a Jetty ThreadPool to be used + */ + public void setThreadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + } + + /** + * Set the number of acceptor threads to use. + * @param acceptors the number of acceptor threads to use + */ + public void setAcceptors(int acceptors) { + this.acceptors = acceptors; + } + + /** + * Set the number of selector threads to use. + * @param selectors the number of selector threads to use + */ + public void setSelectors(int selectors) { + this.selectors = selectors; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyWebServer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyWebServer.java new file mode 100644 index 0000000000..a91ff3e186 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyWebServer.java @@ -0,0 +1,236 @@ +/* + * 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.context.embedded.jetty; + +import java.net.BindException; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.HandlerWrapper; + +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.EmbeddedWebServerException; +import org.springframework.boot.context.embedded.PortInUseException; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link EmbeddedWebServer} that can be used to control a Jetty web server. + * Usually this class should be created using the + * {@link JettyReactiveWebServerFactory} and not directly. + * + * @author Phillip Webb + * @author Dave Syer + * @author David Liu + * @author Eddú Meléndez + * @author Brian Clozel + * @see JettyReactiveWebServerFactory + */ +public class JettyWebServer implements EmbeddedWebServer { + + private static final Log logger = LogFactory + .getLog(JettyWebServer.class); + + private final Object monitor = new Object(); + + private final Server server; + + private final boolean autoStart; + + private Connector[] connectors; + + private volatile boolean started; + + /** + * Create a new {@link JettyWebServer} instance. + * @param server the underlying Jetty server + */ + public JettyWebServer(Server server) { + this(server, true); + } + + /** + * Create a new {@link JettyWebServer} instance. + * @param server the underlying Jetty server + * @param autoStart if auto-starting the container + */ + public JettyWebServer(Server server, boolean autoStart) { + this.autoStart = autoStart; + Assert.notNull(server, "Jetty Server must not be null"); + this.server = server; + initialize(); + } + + private void initialize() { + synchronized (this.monitor) { + try { + // Cache and clear the connectors to prevent requests being handled before + // the application context is ready + this.connectors = this.server.getConnectors(); + this.server.setConnectors(null); + + // Start the server so that the ServletContext is available + this.server.start(); + this.server.setStopAtShutdown(false); + } + catch (Exception ex) { + // Ensure process isn't left running + stopSilently(); + throw new EmbeddedWebServerException( + "Unable to start embedded Jetty web server", ex); + } + } + } + + private void stopSilently() { + try { + this.server.stop(); + } + catch (Exception ex) { + // Ignore + } + } + + @Override + public void start() throws EmbeddedWebServerException { + synchronized (this.monitor) { + if (this.started) { + return; + } + this.server.setConnectors(this.connectors); + if (!this.autoStart) { + return; + } + try { + this.server.start(); + for (Handler handler : this.server.getHandlers()) { + handleDeferredInitialize(handler); + } + Connector[] connectors = this.server.getConnectors(); + for (Connector connector : connectors) { + try { + connector.start(); + } + catch (BindException ex) { + if (connector instanceof NetworkConnector) { + throw new PortInUseException( + ((NetworkConnector) connector).getPort()); + } + throw ex; + } + } + this.started = true; + JettyWebServer.logger + .info("Jetty started on port(s) " + getActualPortsDescription()); + } + catch (EmbeddedWebServerException ex) { + throw ex; + } + catch (Exception ex) { + throw new EmbeddedWebServerException( + "Unable to start embedded Jetty servlet container", ex); + } + } + } + + private String getActualPortsDescription() { + StringBuilder ports = new StringBuilder(); + for (Connector connector : this.server.getConnectors()) { + ports.append(ports.length() == 0 ? "" : ", "); + ports.append(getLocalPort(connector) + getProtocols(connector)); + } + return ports.toString(); + } + + private Integer getLocalPort(Connector connector) { + try { + // Jetty 9 internals are different, but the method name is the same + return (Integer) ReflectionUtils.invokeMethod( + ReflectionUtils.findMethod(connector.getClass(), "getLocalPort"), + connector); + } + catch (Exception ex) { + JettyWebServer.logger + .info("could not determine port ( " + ex.getMessage() + ")"); + return 0; + } + } + + private String getProtocols(Connector connector) { + List protocols = connector.getProtocols(); + return " (" + StringUtils.collectionToDelimitedString(protocols, ", ") + ")"; + } + + private void handleDeferredInitialize(Handler... handlers) throws Exception { + for (Handler handler : handlers) { + if (handler instanceof JettyEmbeddedWebAppContext) { + ((JettyEmbeddedWebAppContext) handler).deferredInitialize(); + } + else if (handler instanceof HandlerWrapper) { + handleDeferredInitialize(((HandlerWrapper) handler).getHandler()); + } + else if (handler instanceof HandlerCollection) { + handleDeferredInitialize(((HandlerCollection) handler).getHandlers()); + } + } + } + + @Override + public void stop() { + synchronized (this.monitor) { + if (!this.started) { + return; + } + this.started = false; + try { + this.server.stop(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (Exception ex) { + throw new EmbeddedWebServerException( + "Unable to stop embedded Jetty web server", ex); + } + } + } + + @Override + public int getPort() { + Connector[] connectors = this.server.getConnectors(); + for (Connector connector : connectors) { + // Probably only one... + return getLocalPort(connector); + } + return 0; + } + + /** + * Returns access to the underlying Jetty Server. + * @return the Jetty server + */ + public Server getServer() { + return this.server; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactory.java new file mode 100644 index 0000000000..92e71caec9 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactory.java @@ -0,0 +1,69 @@ +/* + * 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.context.embedded.reactor; + +import java.util.Map; + +import reactor.ipc.netty.http.server.HttpServer; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; + +/** + * {@link ReactiveWebServerFactory} that can be used to create + * {@link ReactorNettyWebServer}s. + * + * @author Brian Clozel + */ +public class ReactorNettyReactiveWebServerFactory extends AbstractReactiveWebServerFactory { + + public ReactorNettyReactiveWebServerFactory() { + } + + public ReactorNettyReactiveWebServerFactory(int port) { + super(port); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(HttpHandler httpHandler) { + HttpServer server = createHttpServer(); + ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler); + return new ReactorNettyWebServer(server, handlerAdapter); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(Map handlerMap) { + HttpServer server = createHttpServer(); + ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(handlerMap); + return new ReactorNettyWebServer(server, handlerAdapter); + } + + private HttpServer createHttpServer() { + HttpServer server; + if (getAddress() != null) { + server = HttpServer.create(getAddress().getHostAddress(), getPort()); + } + else { + server = HttpServer.create(getPort()); + } + return server; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyWebServer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyWebServer.java new file mode 100644 index 0000000000..ef09089b37 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/reactor/ReactorNettyWebServer.java @@ -0,0 +1,101 @@ +/* + * 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.context.embedded.reactor; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import reactor.core.Loopback; +import reactor.ipc.netty.NettyContext; +import reactor.ipc.netty.http.server.HttpServer; + +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.EmbeddedWebServerException; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; + +/** + * {@link EmbeddedWebServer} that can be used to control a Reactor Netty web server. + * Usually this class should be created using the + * {@link ReactorNettyReactiveWebServerFactory} and not directly. + * + * @author Brian Clozel + */ +public class ReactorNettyWebServer implements EmbeddedWebServer, Loopback { + + private static CountDownLatch latch = new CountDownLatch(1); + + private final ReactorHttpHandlerAdapter handlerAdapter; + + private final HttpServer reactorServer; + + private AtomicReference nettyContext = new AtomicReference<>(); + + public ReactorNettyWebServer(HttpServer reactorServer, ReactorHttpHandlerAdapter handlerAdapter) { + this.reactorServer = reactorServer; + this.handlerAdapter = handlerAdapter; + } + + @Override + public Object connectedInput() { + return this.reactorServer; + } + + @Override + public Object connectedOutput() { + return this.reactorServer; + } + + @Override + public void start() throws EmbeddedWebServerException { + if (this.nettyContext.get() == null) { + this.nettyContext.set(this.reactorServer.newHandler(this.handlerAdapter).block()); + startDaemonAwaitThread(); + } + } + + private void startDaemonAwaitThread() { + Thread awaitThread = new Thread("server") { + @Override + public void run() { + try { + ReactorNettyWebServer.latch.await(); + } + catch (InterruptedException e) { } + } + }; + awaitThread.setContextClassLoader(getClass().getClassLoader()); + awaitThread.setDaemon(false); + awaitThread.start(); + } + + @Override + public void stop() throws EmbeddedWebServerException { + NettyContext context = this.nettyContext.getAndSet(null); + if (context != null) { + context.dispose(); + } + latch.countDown(); + } + + @Override + public int getPort() { + if (this.nettyContext.get() != null) { + return this.nettyContext.get().address().getPort(); + } + return 0; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactory.java new file mode 100644 index 0000000000..b6d17dd314 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -0,0 +1,159 @@ +/* + * 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.context.embedded.tomcat; + +import java.io.File; +import java.util.Map; + +import org.apache.catalina.Host; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.loader.WebappLoader; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.AbstractProtocol; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ReactiveWebServerFactory} that can be used to create + * {@link TomcatWebServer}s. + * + * @author Brian Clozel + */ +public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory { + + /** + * The class name of default protocol used. + */ + public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol"; + + private String protocol = DEFAULT_PROTOCOL; + + /** + * Create a new {@link TomcatEmbeddedServletContainerFactory} instance. + */ + public TomcatReactiveWebServerFactory() { + super(); + } + + /** + * Create a new {@link TomcatEmbeddedServletContainerFactory} that listens for + * requests using the specified port. + * @param port the port to listen on + */ + public TomcatReactiveWebServerFactory(int port) { + super(port); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(HttpHandler httpHandler) { + Tomcat tomcatServer = createTomcatServer(); + TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); + prepareContext(tomcatServer.getHost(), servlet); + return new TomcatWebServer(tomcatServer, getPort() >= 0); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(Map handlerMap) { + Tomcat tomcatServer = createTomcatServer(); + TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(handlerMap); + prepareContext(tomcatServer.getHost(), servlet); + return new TomcatWebServer(tomcatServer, getPort() >= 0); + } + + private Tomcat createTomcatServer() { + Tomcat tomcat = new Tomcat(); + File baseDir = createTempDir("tomcat"); + tomcat.setBaseDir(baseDir.getAbsolutePath()); + Connector connector = new Connector(this.protocol); + tomcat.getService().addConnector(connector); + customizeConnector(connector); + tomcat.setConnector(connector); + tomcat.getHost().setAutoDeploy(false); + return tomcat; + } + + + protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) { + File docBase = createTempDir("tomcat-docbase"); + TomcatEmbeddedContext context = new TomcatEmbeddedContext(); + context.setPath(""); + context.setDocBase(docBase.getAbsolutePath()); + context.addLifecycleListener(new Tomcat.FixContextListener()); + context.setParentClassLoader(ClassUtils.getDefaultClassLoader()); + WebappLoader loader = new WebappLoader(context.getParentClassLoader()); + loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName()); + loader.setDelegate(true); + context.setLoader(loader); + Tomcat.addServlet(context, "httpHandlerServlet", servlet); + context.addServletMappingDecoded("/", "httpHandlerServlet"); + host.addChild(context); + } + + + // Needs to be protected so it can be used by subclasses + protected void customizeConnector(Connector connector) { + int port = (getPort() >= 0 ? getPort() : 0); + connector.setPort(port); + if (StringUtils.hasText(this.getServerHeader())) { + connector.setAttribute("server", this.getServerHeader()); + } + if (connector.getProtocolHandler() instanceof AbstractProtocol) { + customizeProtocol((AbstractProtocol) connector.getProtocolHandler()); + } + + // If ApplicationContext is slow to start we want Tomcat not to bind to the socket + // prematurely... + connector.setProperty("bindOnInit", "false"); + } + + private void customizeProtocol(AbstractProtocol protocol) { + if (getAddress() != null) { + protocol.setAddress(getAddress()); + } + } + + /** + * Factory method called to create the {@link TomcatEmbeddedServletContainer}. + * Subclasses can override this method to return a different + * {@link TomcatEmbeddedServletContainer} or apply additional processing to the Tomcat + * server. + * @param tomcat the Tomcat server. + * @return a new {@link TomcatEmbeddedServletContainer} instance + */ + protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer( + Tomcat tomcat) { + return new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0); + } + + /** + * The Tomcat protocol to use when create the {@link Connector}. + * @param protocol the protocol + * @see Connector#Connector(String) + */ + public void setProtocol(String protocol) { + Assert.hasLength(protocol, "Protocol must not be empty"); + this.protocol = protocol; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatWebServer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatWebServer.java new file mode 100644 index 0000000000..33d80d2960 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatWebServer.java @@ -0,0 +1,314 @@ +/* + * 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.context.embedded.tomcat; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.naming.NamingException; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.naming.ContextBindings; + +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.EmbeddedWebServerException; +import org.springframework.util.Assert; + +/** + * {@link EmbeddedWebServer} that can be used to control a Tomcat web server. + * Usually this class should be created using the + * {@link TomcatReactiveWebServerFactory} and not directly. + * + * @author Brian Clozel + */ +public class TomcatWebServer implements EmbeddedWebServer { + + + private static final Log logger = LogFactory + .getLog(TomcatEmbeddedServletContainer.class); + + private static final AtomicInteger containerCounter = new AtomicInteger(-1); + + private final Object monitor = new Object(); + + private final Map serviceConnectors = new HashMap(); + + private final Tomcat tomcat; + + private final boolean autoStart; + + private volatile boolean started; + + /** + * Create a new {@link TomcatWebServer} instance. + * @param tomcat the underlying Tomcat server + */ + public TomcatWebServer(Tomcat tomcat) { + this(tomcat, true); + } + + /** + * Create a new {@link TomcatEmbeddedServletContainer} instance. + * @param tomcat the underlying Tomcat server + * @param autoStart if the server should be started + */ + public TomcatWebServer(Tomcat tomcat, boolean autoStart) { + Assert.notNull(tomcat, "Tomcat Server must not be null"); + this.tomcat = tomcat; + this.autoStart = autoStart; + initialize(); + } + + private void initialize() throws EmbeddedWebServerException { + TomcatWebServer.logger + .info("Tomcat initialized with port(s): " + getPortsDescription(false)); + synchronized (this.monitor) { + try { + addInstanceIdToEngineName(); + + // Remove service connectors so that protocol binding doesn't happen yet + removeServiceConnectors(); + + // Start the server to trigger initialization listeners + this.tomcat.start(); + + Context context = findContext(); + try { + ContextBindings.bindClassLoader(context, context.getNamingToken(), + getClass().getClassLoader()); + } + catch (NamingException ex) { + // Naming is not enabled. Continue + } + + // Unlike Jetty, all Tomcat threads are daemon threads. We create a + // blocking non-daemon to stop immediate shutdown + startDaemonAwaitThread(); + } + catch (Exception ex) { + throw new EmbeddedWebServerException( + "Unable to start embedded Tomcat", ex); + } + } + } + + private Context findContext() { + for (Container child : this.tomcat.getHost().findChildren()) { + if (child instanceof Context) { + return (Context) child; + } + } + throw new IllegalStateException("The host does not contain a Context"); + } + + private void addInstanceIdToEngineName() { + int instanceId = containerCounter.incrementAndGet(); + if (instanceId > 0) { + Engine engine = this.tomcat.getEngine(); + engine.setName(engine.getName() + "-" + instanceId); + } + } + + private void removeServiceConnectors() { + for (Service service : this.tomcat.getServer().findServices()) { + Connector[] connectors = service.findConnectors().clone(); + this.serviceConnectors.put(service, connectors); + for (Connector connector : connectors) { + service.removeConnector(connector); + } + } + } + + private void startDaemonAwaitThread() { + Thread awaitThread = new Thread("container-" + (containerCounter.get())) { + + @Override + public void run() { + TomcatWebServer.this.tomcat.getServer().await(); + } + + }; + awaitThread.setContextClassLoader(getClass().getClassLoader()); + awaitThread.setDaemon(false); + awaitThread.start(); + } + + @Override + public void start() throws EmbeddedWebServerException { + synchronized (this.monitor) { + if (this.started) { + return; + } + try { + addPreviouslyRemovedConnectors(); + Connector connector = this.tomcat.getConnector(); + if (connector != null && this.autoStart) { + startConnector(connector); + } + checkThatConnectorsHaveStarted(); + this.started = true; + TomcatWebServer.logger + .info("Tomcat started on port(s): " + getPortsDescription(true)); + } + catch (ConnectorStartFailedException ex) { + stopSilently(); + throw ex; + } + catch (Exception ex) { + throw new EmbeddedWebServerException( + "Unable to start embedded Tomcat server", ex); + } + finally { + Context context = findContext(); + ContextBindings.unbindClassLoader(context, context.getNamingToken(), + getClass().getClassLoader()); + } + } + } + + private void checkThatConnectorsHaveStarted() { + for (Connector connector : this.tomcat.getService().findConnectors()) { + if (LifecycleState.FAILED.equals(connector.getState())) { + throw new ConnectorStartFailedException(connector.getPort()); + } + } + } + + private void stopSilently() { + try { + stopTomcat(); + } + catch (LifecycleException ex) { + // Ignore + } + } + + private void stopTomcat() throws LifecycleException { + if (Thread.currentThread() + .getContextClassLoader() instanceof TomcatEmbeddedWebappClassLoader) { + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + } + this.tomcat.stop(); + } + + private void addPreviouslyRemovedConnectors() { + Service[] services = this.tomcat.getServer().findServices(); + for (Service service : services) { + Connector[] connectors = this.serviceConnectors.get(service); + if (connectors != null) { + for (Connector connector : connectors) { + service.addConnector(connector); + if (!this.autoStart) { + stopProtocolHandler(connector); + } + } + this.serviceConnectors.remove(service); + } + } + } + + private void stopProtocolHandler(Connector connector) { + try { + connector.getProtocolHandler().stop(); + } + catch (Exception ex) { + TomcatWebServer.logger.error("Cannot pause connector: ", ex); + } + } + + private void startConnector(Connector connector) { + try { + for (Container child : this.tomcat.getHost().findChildren()) { + if (child instanceof TomcatEmbeddedContext) { + ((TomcatEmbeddedContext) child).deferredLoadOnStartup(); + } + } + } + catch (Exception ex) { + TomcatWebServer.logger.error("Cannot start connector: ", ex); + throw new EmbeddedWebServerException( + "Unable to start embedded Tomcat connectors", ex); + } + } + + Map getServiceConnectors() { + return this.serviceConnectors; + } + + @Override + public void stop() throws EmbeddedWebServerException { + synchronized (this.monitor) { + if (!this.started) { + return; + } + try { + this.started = false; + try { + stopTomcat(); + this.tomcat.destroy(); + } + catch (LifecycleException ex) { + // swallow and continue + } + } + catch (Exception ex) { + throw new EmbeddedWebServerException( + "Unable to stop embedded Tomcat", ex); + } + finally { + containerCounter.decrementAndGet(); + } + } + } + + private String getPortsDescription(boolean localPort) { + StringBuilder ports = new StringBuilder(); + for (Connector connector : this.tomcat.getService().findConnectors()) { + ports.append(ports.length() == 0 ? "" : " "); + int port = (localPort ? connector.getLocalPort() : connector.getPort()); + ports.append(port + " (" + connector.getScheme() + ")"); + } + return ports.toString(); + } + + @Override + public int getPort() { + Connector connector = this.tomcat.getConnector(); + if (connector != null) { + return connector.getLocalPort(); + } + return 0; + } + + /** + * Returns access to the underlying Tomcat server. + * @return the Tomcat server + */ + public Tomcat getTomcat() { + return this.tomcat; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactory.java new file mode 100644 index 0000000000..687b8fb497 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactory.java @@ -0,0 +1,100 @@ +/* + * 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.context.embedded.undertow; + +import java.util.Map; + +import io.undertow.Undertow; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; + +/** + * {@link ReactiveWebServerFactory} that can be used to create + * {@link UndertowWebServer}s. + * + * @author Brian Clozel + */ +public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory { + + private Integer bufferSize; + + private Integer ioThreads; + + private Integer workerThreads; + + private Boolean directBuffers; + + /** + * Create a new {@link UndertowReactiveWebServerFactory} instance. + */ + public UndertowReactiveWebServerFactory() { + } + + /** + * Create a new {@link UndertowReactiveWebServerFactory} that listens for + * requests using the specified port. + * @param port the port to listen on + */ + public UndertowReactiveWebServerFactory(int port) { + super(port); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(HttpHandler httpHandler) { + Undertow.Builder builder = createBuilder(getPort()); + UndertowHttpHandlerAdapter handler = new UndertowHttpHandlerAdapter(httpHandler); + builder.setHandler(handler); + return new UndertowWebServer(builder, getPort() >= 0); + } + + @Override + public EmbeddedWebServer getReactiveHttpServer(Map handlerMap) { + Undertow.Builder builder = createBuilder(getPort()); + UndertowHttpHandlerAdapter handler = new UndertowHttpHandlerAdapter(handlerMap); + builder.setHandler(handler); + return new UndertowWebServer(builder, getPort() >= 0); + } + + private Undertow.Builder createBuilder(int port) { + Undertow.Builder builder = Undertow.builder(); + if (this.bufferSize != null) { + builder.setBufferSize(this.bufferSize); + } + if (this.ioThreads != null) { + builder.setIoThreads(this.ioThreads); + } + if (this.workerThreads != null) { + builder.setWorkerThreads(this.workerThreads); + } + if (this.directBuffers != null) { + builder.setDirectBuffers(this.directBuffers); + } + builder.addHttpListener(port, getListenAddress()); + return builder; + } + + private String getListenAddress() { + if (getAddress() == null) { + return "0.0.0.0"; + } + return getAddress().getHostAddress(); + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowWebServer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowWebServer.java new file mode 100644 index 0000000000..26d88cd934 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowWebServer.java @@ -0,0 +1,267 @@ +/* + * 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.context.embedded.undertow; + +import java.lang.reflect.Field; +import java.net.BindException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; + +import io.undertow.Undertow; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xnio.channels.BoundChannel; + +import org.springframework.boot.context.embedded.EmbeddedWebServer; +import org.springframework.boot.context.embedded.EmbeddedWebServerException; +import org.springframework.boot.context.embedded.PortInUseException; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link EmbeddedWebServer} that can be used to control a Jetty web server. + * Usually this class should be created using the + * {@link UndertowReactiveWebServerFactory} and not directly. + * + * @author Ivan Sopov + * @author Andy Wilkinson + * @author Eddú Meléndez + * @author Christoph Dreis + * @author Brian Clozel + */ +public class UndertowWebServer implements EmbeddedWebServer { + + private static final Log logger = LogFactory + .getLog(UndertowEmbeddedServletContainer.class); + + private final Object monitor = new Object(); + + private final Undertow.Builder builder; + + private final boolean autoStart; + + private Undertow undertow; + + private volatile boolean started = false; + + /** + * Create a new {@link UndertowWebServer} instance. + * @param builder the builder + * @param autoStart if the server should be started + */ + public UndertowWebServer(Undertow.Builder builder, boolean autoStart) { + this.builder = builder; + this.autoStart = autoStart; + } + + @Override + public void start() throws EmbeddedWebServerException { + synchronized (this.monitor) { + if (this.started) { + return; + } + try { + if (!this.autoStart) { + return; + } + if (this.undertow == null) { + this.undertow = this.builder.build(); + } + this.undertow.start(); + this.started = true; + UndertowWebServer.logger + .info("Undertow started on port(s) " + getPortsDescription()); + } + catch (Exception ex) { + if (findBindException(ex) != null) { + List failedPorts = getConfiguredPorts(); + List actualPorts = getActualPorts(); + failedPorts.removeAll(actualPorts); + if (failedPorts.size() == 1) { + throw new PortInUseException( + failedPorts.iterator().next().getNumber()); + } + } + throw new EmbeddedWebServerException( + "Unable to start embedded Undertow", ex); + } + } + } + + private BindException findBindException(Exception ex) { + Throwable candidate = ex; + while (candidate != null) { + if (candidate instanceof BindException) { + return (BindException) candidate; + } + candidate = candidate.getCause(); + } + return null; + } + + private String getPortsDescription() { + List ports = getActualPorts(); + if (!ports.isEmpty()) { + return StringUtils.collectionToDelimitedString(ports, " "); + } + return "unknown"; + } + + private List getActualPorts() { + List ports = new ArrayList(); + try { + if (!this.autoStart) { + ports.add(new UndertowWebServer.Port(-1, "unknown")); + } + else { + for (BoundChannel channel : extractChannels()) { + ports.add(getPortFromChannel(channel)); + } + } + } + catch (Exception ex) { + // Continue + } + return ports; + } + + + @SuppressWarnings("unchecked") + private List extractChannels() { + Field channelsField = ReflectionUtils.findField(Undertow.class, "channels"); + ReflectionUtils.makeAccessible(channelsField); + return (List) ReflectionUtils.getField(channelsField, + this.undertow); + } + + private UndertowWebServer.Port getPortFromChannel(BoundChannel channel) { + SocketAddress socketAddress = channel.getLocalAddress(); + if (socketAddress instanceof InetSocketAddress) { + String protocol = ReflectionUtils.findField(channel.getClass(), "ssl") != null + ? "https" : "http"; + return new UndertowWebServer.Port(((InetSocketAddress) socketAddress).getPort(), protocol); + } + return null; + } + + private List getConfiguredPorts() { + List ports = new ArrayList(); + for (Object listener : extractListeners()) { + try { + ports.add(getPortFromListener(listener)); + } + catch (Exception ex) { + // Continue + } + } + return ports; + } + + @SuppressWarnings("unchecked") + private List extractListeners() { + Field listenersField = ReflectionUtils.findField(Undertow.class, "listeners"); + ReflectionUtils.makeAccessible(listenersField); + return (List) ReflectionUtils.getField(listenersField, this.undertow); + } + + private UndertowWebServer.Port getPortFromListener(Object listener) { + Field typeField = ReflectionUtils.findField(listener.getClass(), "type"); + ReflectionUtils.makeAccessible(typeField); + String protocol = ReflectionUtils.getField(typeField, listener).toString(); + Field portField = ReflectionUtils.findField(listener.getClass(), "port"); + ReflectionUtils.makeAccessible(portField); + int port = (Integer) ReflectionUtils.getField(portField, listener); + return new UndertowWebServer.Port(port, protocol); + } + + + @Override + public void stop() throws EmbeddedWebServerException { + synchronized (this.monitor) { + if (!this.started) { + return; + } + this.started = false; + try { + this.undertow.stop(); + } + catch (Exception ex) { + throw new EmbeddedWebServerException("Unable to stop undertow", + ex); + } + } + } + + @Override + public int getPort() { + List ports = getActualPorts(); + if (ports.isEmpty()) { + return 0; + } + return ports.get(0).getNumber(); + } + + /** + * An active Undertow port. + */ + private final static class Port { + + private final int number; + + private final String protocol; + + private Port(int number, String protocol) { + this.number = number; + this.protocol = protocol; + } + + public int getNumber() { + return this.number; + } + + @Override + public String toString() { + return this.number + " (" + this.protocol + ")"; + } + + @Override + public int hashCode() { + return this.number; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + UndertowWebServer.Port other = (UndertowWebServer.Port) obj; + if (this.number != other.number) { + return false; + } + return true; + } + + } +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactoryTests.java new file mode 100644 index 0000000000..eb0938ab6e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -0,0 +1,34 @@ +/* + * 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.context.embedded.jetty; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactoryTests; + +/** + * Tests for {@link JettyReactiveWebServerFactory} and + * {@link JettyWebServer}. + * + * @author Brian Clozel + */ +public class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { + + @Override + protected AbstractReactiveWebServerFactory getFactory() { + return new JettyReactiveWebServerFactory(0); + } +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactoryTests.java new file mode 100644 index 0000000000..7921ce434a --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/reactor/ReactorNettyReactiveWebServerFactoryTests.java @@ -0,0 +1,35 @@ +/* + * 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.context.embedded.reactor; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactoryTests; + +/** + * Tests for {@link ReactorNettyReactiveWebServerFactory} and + * {@link ReactorNettyWebServer}. + * + * @author Brian Clozel + */ +public class ReactorNettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { + + @Override + protected AbstractReactiveWebServerFactory getFactory() { + return new ReactorNettyReactiveWebServerFactory(0); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java new file mode 100644 index 0000000000..9f529cd312 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java @@ -0,0 +1,34 @@ +/* + * 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.context.embedded.tomcat; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactoryTests; + +/** + * Tests for {@link TomcatReactiveWebServerFactory} and + * {@link TomcatWebServer}. + * + * @author Brian Clozel + */ +public class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { + + @Override + protected AbstractReactiveWebServerFactory getFactory() { + return new TomcatReactiveWebServerFactory(0); + } +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactoryTests.java new file mode 100644 index 0000000000..38bc93f813 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowReactiveWebServerFactoryTests.java @@ -0,0 +1,34 @@ +/* + * 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.context.embedded.undertow; + +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactory; +import org.springframework.boot.context.embedded.AbstractReactiveWebServerFactoryTests; + +/** + * Tests for {@link UndertowReactiveWebServerFactory} and + * {@link UndertowWebServer}. + * + * @author Brian Clozel + */ +public class UndertowReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { + + @Override + protected AbstractReactiveWebServerFactory getFactory() { + return new UndertowReactiveWebServerFactory(0); + } +}