From 0b162e894b8f05f914debf74c828b60806793ddf Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 20 Feb 2017 15:03:26 +0100 Subject: [PATCH] Manage EmbeddedWebServer in ReactiveWebApplicationContext This commit adds an `EmbeddedWebServer` instance to the `ReactiveWebApplicationContext` and ties it to the application lifecycle. To launch a reactive web application, two elements are required from the context: * a `ReactiveWebServerFactory` to create a server instance * a `HttpHandler` instance to handle HTTP requests Closes gh-8337 --- .../ConditionalOnNotWebApplicationTests.java | 22 ++- .../ConditionalOnWebApplicationTests.java | 13 ++ .../ReactiveWebApplicationContext.java | 133 +++++++++++++++++- ...PortInfoApplicationContextInitializer.java | 16 +-- .../boot/SpringApplicationTests.java | 33 ++++- 5 files changed, 194 insertions(+), 23 deletions(-) diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWebApplicationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWebApplicationTests.java index 7489526162b..7c290283b24 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWebApplicationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWebApplicationTests.java @@ -18,12 +18,16 @@ package org.springframework.boot.autoconfigure.condition; import org.junit.After; import org.junit.Test; +import reactor.core.publisher.Mono; +import org.springframework.boot.autoconfigure.webflux.MockReactiveWebServerFactory; import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -33,7 +37,7 @@ import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link ConditionalOnNotWebApplication}. * - * @author Dave Syer$ + * @author Dave Syer * @author Stephane Nicoll */ public class ConditionalOnNotWebApplicationTests { @@ -61,7 +65,7 @@ public class ConditionalOnNotWebApplicationTests { @Test public void testNotWebApplicationWithReactiveContext() { ReactiveWebApplicationContext ctx = new ReactiveWebApplicationContext(); - ctx.register(NotWebApplicationConfiguration.class); + ctx.register(ReactiveApplicationConfig.class, NotWebApplicationConfiguration.class); ctx.refresh(); this.context = ctx; @@ -79,6 +83,20 @@ public class ConditionalOnNotWebApplicationTests { entry("none", "none")); } + @Configuration + protected static class ReactiveApplicationConfig { + + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory() { + return new MockReactiveWebServerFactory(); + } + + @Bean + public HttpHandler httpHandler() { + return (request, response) -> Mono.empty(); + } + } + @Configuration @ConditionalOnNotWebApplication protected static class NotWebApplicationConfiguration { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWebApplicationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWebApplicationTests.java index e6b52521333..ab0aa22070c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWebApplicationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWebApplicationTests.java @@ -18,13 +18,17 @@ package org.springframework.boot.autoconfigure.condition; import org.junit.After; import org.junit.Test; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.webflux.MockReactiveWebServerFactory; import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; +import org.springframework.boot.context.embedded.ReactiveWebServerFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -118,6 +122,15 @@ public class ConditionalOnWebApplicationTests { return "reactive"; } + @Bean + public ReactiveWebServerFactory reactiveWebServerFactory() { + return new MockReactiveWebServerFactory(); + } + + @Bean + public HttpHandler httpHandler() { + return (request, response) -> Mono.empty(); + } } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ReactiveWebApplicationContext.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ReactiveWebApplicationContext.java index 35e39893d42..ff28f947d3d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ReactiveWebApplicationContext.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ReactiveWebApplicationContext.java @@ -16,13 +16,142 @@ package org.springframework.boot.context.embedded; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContextException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.StringUtils; /** - * A {@link AnnotationConfigApplicationContext} that can be used to bootstrap itself from a contained - * embedded web server factory bean. + * A {@link AnnotationConfigApplicationContext} that can be used to bootstrap + * itself from a contained embedded web server factory bean. + * * @author Brian Clozel * @since 2.0.0 */ public class ReactiveWebApplicationContext extends AnnotationConfigApplicationContext { + + private volatile EmbeddedWebServer embeddedWebServer; + + public ReactiveWebApplicationContext() { + super(); + } + + public ReactiveWebApplicationContext(Class... annotatedClasses) { + super(annotatedClasses); + } + + @Override + public final void refresh() throws BeansException, IllegalStateException { + try { + super.refresh(); + } + catch (RuntimeException ex) { + stopAndReleaseReactiveWebServer(); + throw ex; + } + } + + @Override + protected void onRefresh() { + super.onRefresh(); + try { + createEmbeddedServletContainer(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Unable to start reactive web server", ex); + } + } + + @Override + protected void finishRefresh() { + super.finishRefresh(); + EmbeddedWebServer localServer = startReactiveWebServer(); + if (localServer != null) { + publishEvent( + new EmbeddedReactiveWebServerInitializedEvent(localServer, this)); + } + } + + @Override + protected void onClose() { + super.onClose(); + stopAndReleaseReactiveWebServer(); + } + + private void createEmbeddedServletContainer() { + EmbeddedWebServer localServer = this.embeddedWebServer; + if (localServer == null) { + this.embeddedWebServer = getReactiveWebServerFactory() + .getReactiveHttpServer(getHttpHandler()); + } + initPropertySources(); + } + + /** + * Return the {@link ReactiveWebServerFactory} that should be used to create + * the reactive web server. By default this method searches for a suitable bean + * in the context itself. + * @return a {@link ReactiveWebServerFactory} (never {@code null}) + */ + protected ReactiveWebServerFactory getReactiveWebServerFactory() { + // Use bean names so that we don't consider the hierarchy + String[] beanNames = getBeanFactory() + .getBeanNamesForType(ReactiveWebServerFactory.class); + if (beanNames.length == 0) { + throw new ApplicationContextException( + "Unable to start ReactiveWebApplicationContext due to missing " + + "ReactiveWebServerFactory bean."); + } + if (beanNames.length > 1) { + throw new ApplicationContextException( + "Unable to start ReactiveWebApplicationContext due to multiple " + + "ReactiveWebServerFactory beans : " + + StringUtils.arrayToCommaDelimitedString(beanNames)); + } + return getBeanFactory().getBean(beanNames[0], ReactiveWebServerFactory.class); + } + + /** + * Return the {@link HttpHandler} that should be used to process + * the reactive web server. By default this method searches for a suitable bean + * in the context itself. + * @return a {@link HttpHandler} (never {@code null} + */ + protected HttpHandler getHttpHandler() { + // Use bean names so that we don't consider the hierarchy + String[] beanNames = getBeanFactory() + .getBeanNamesForType(HttpHandler.class); + if (beanNames.length == 0) { + throw new ApplicationContextException( + "Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean."); + } + if (beanNames.length > 1) { + throw new ApplicationContextException( + "Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans : " + + StringUtils.arrayToCommaDelimitedString(beanNames)); + } + return getBeanFactory().getBean(beanNames[0], HttpHandler.class); + } + + private EmbeddedWebServer startReactiveWebServer() { + EmbeddedWebServer localServer = this.embeddedWebServer; + if (localServer != null) { + localServer.start(); + } + return localServer; + } + + private void stopAndReleaseReactiveWebServer() { + EmbeddedWebServer localServer = this.embeddedWebServer; + if (localServer != null) { + try { + localServer.stop(); + this.embeddedWebServer = null; + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ServerPortInfoApplicationContextInitializer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ServerPortInfoApplicationContextInitializer.java index 5a700b3ca9c..7deca9e0748 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ServerPortInfoApplicationContextInitializer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ServerPortInfoApplicationContextInitializer.java @@ -29,7 +29,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; -import org.springframework.util.StringUtils; /** * {@link ApplicationContextInitializer} that sets {@link Environment} properties for the @@ -54,11 +53,11 @@ public class ServerPortInfoApplicationContextInitializer @Override public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.addApplicationListener( - new ApplicationListener() { + new ApplicationListener() { @Override public void onApplicationEvent( - EmbeddedServletContainerInitializedEvent event) { + EmbeddedWebServerInitializedEvent event) { ServerPortInfoApplicationContextInitializer.this .onApplicationEvent(event); } @@ -66,19 +65,12 @@ public class ServerPortInfoApplicationContextInitializer }); } - protected void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) { - String propertyName = getPropertyName(event.getApplicationContext()); + protected void onApplicationEvent(EmbeddedWebServerInitializedEvent event) { + String propertyName = "local." + event.getServerId() + ".port"; setPortProperty(event.getApplicationContext(), propertyName, event.getEmbeddedWebServer().getPort()); } - protected String getPropertyName(EmbeddedWebApplicationContext context) { - String name = context.getNamespace(); - if (StringUtils.isEmpty(name)) { - name = "server"; - } - return "local." + name + ".port"; - } private void setPortProperty(ApplicationContext context, String propertyName, int port) { diff --git a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index b1df740d22c..f0db5d07692 100644 --- a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -36,6 +36,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; import org.springframework.beans.CachedIntrospectionResults; @@ -44,6 +45,7 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; +import org.springframework.boot.context.embedded.reactor.ReactorNettyReactiveWebServerFactory; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; @@ -77,6 +79,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -400,7 +403,7 @@ public class SpringApplicationTests { @Test public void defaultApplicationContextForReactiveWeb() throws Exception { - SpringApplication application = new SpringApplication(ExampleWebConfig.class); + SpringApplication application = new SpringApplication(ExampleReactiveWebConfig.class); application.setWebApplicationType(WebApplicationType.REACTIVE); this.context = application.run(); assertThat(this.context).isInstanceOf(ReactiveWebApplicationContext.class); @@ -571,7 +574,7 @@ public class SpringApplicationTests { @Test public void loadSources() throws Exception { - Object[] sources = { ExampleConfig.class, "a", TestCommandLineRunner.class }; + Object[] sources = {ExampleConfig.class, "a", TestCommandLineRunner.class}; TestSpringApplication application = new TestSpringApplication(sources); application.setWebApplicationType(WebApplicationType.NONE); application.setUseMockLoader(true); @@ -583,7 +586,7 @@ public class SpringApplicationTests { @Test public void wildcardSources() { Object[] sources = { - "classpath:org/springframework/boot/sample-${sample.app.test.prop}.xml" }; + "classpath:org/springframework/boot/sample-${sample.app.test.prop}.xml"}; TestSpringApplication application = new TestSpringApplication(sources); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run(); @@ -598,7 +601,7 @@ public class SpringApplicationTests { @Test public void runComponents() throws Exception { this.context = SpringApplication.run( - new Object[] { ExampleWebConfig.class, Object.class }, new String[0]); + new Object[] {ExampleWebConfig.class, Object.class}, new String[0]); assertThat(this.context).isNotNull(); } @@ -713,7 +716,7 @@ public class SpringApplicationTests { public void defaultCommandLineArgs() throws Exception { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setDefaultProperties(StringUtils.splitArrayElementsIntoProperties( - new String[] { "baz=", "bar=spam" }, "=")); + new String[] {"baz=", "bar=spam"}, "=")); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run("--bar=foo", "bucket", "crap"); assertThat(this.context).isInstanceOf(AnnotationConfigApplicationContext.class); @@ -864,7 +867,7 @@ public class SpringApplicationTests { assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar"); assertThat(this.context.getEnvironment().getPropertySources().iterator().next() .getName()).isEqualTo( - TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); } @Test @@ -877,7 +880,7 @@ public class SpringApplicationTests { FailingConfig.class); application.setWebApplicationType(WebApplicationType.NONE); application.run(); - }; + } }; thread.start(); thread.join(6000); @@ -1028,6 +1031,22 @@ public class SpringApplicationTests { } + @Configuration + static class ExampleReactiveWebConfig { + + @Bean + public ReactorNettyReactiveWebServerFactory webServerFactory() { + return new ReactorNettyReactiveWebServerFactory(0); + } + + @Bean + public HttpHandler httpHandler() { + return (serverHttpRequest, serverHttpResponse) -> Mono.empty(); + } + + } + + @Configuration static class FailingConfig {