Commit 0b162e89 authored by Brian Clozel's avatar Brian Clozel

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
parent 21878f85
...@@ -18,12 +18,16 @@ package org.springframework.boot.autoconfigure.condition; ...@@ -18,12 +18,16 @@ package org.springframework.boot.autoconfigure.condition;
import org.junit.After; import org.junit.After;
import org.junit.Test; 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.ReactiveWebApplicationContext;
import org.springframework.boot.context.embedded.ReactiveWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
...@@ -33,7 +37,7 @@ import static org.assertj.core.api.Assertions.entry; ...@@ -33,7 +37,7 @@ import static org.assertj.core.api.Assertions.entry;
/** /**
* Tests for {@link ConditionalOnNotWebApplication}. * Tests for {@link ConditionalOnNotWebApplication}.
* *
* @author Dave Syer$ * @author Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
public class ConditionalOnNotWebApplicationTests { public class ConditionalOnNotWebApplicationTests {
...@@ -61,7 +65,7 @@ public class ConditionalOnNotWebApplicationTests { ...@@ -61,7 +65,7 @@ public class ConditionalOnNotWebApplicationTests {
@Test @Test
public void testNotWebApplicationWithReactiveContext() { public void testNotWebApplicationWithReactiveContext() {
ReactiveWebApplicationContext ctx = new ReactiveWebApplicationContext(); ReactiveWebApplicationContext ctx = new ReactiveWebApplicationContext();
ctx.register(NotWebApplicationConfiguration.class); ctx.register(ReactiveApplicationConfig.class, NotWebApplicationConfiguration.class);
ctx.refresh(); ctx.refresh();
this.context = ctx; this.context = ctx;
...@@ -79,6 +83,20 @@ public class ConditionalOnNotWebApplicationTests { ...@@ -79,6 +83,20 @@ public class ConditionalOnNotWebApplicationTests {
entry("none", "none")); 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 @Configuration
@ConditionalOnNotWebApplication @ConditionalOnNotWebApplication
protected static class NotWebApplicationConfiguration { protected static class NotWebApplicationConfiguration {
......
...@@ -18,13 +18,17 @@ package org.springframework.boot.autoconfigure.condition; ...@@ -18,13 +18,17 @@ package org.springframework.boot.autoconfigure.condition;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; 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.ReactiveWebApplicationContext;
import org.springframework.boot.context.embedded.ReactiveWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
...@@ -118,6 +122,15 @@ public class ConditionalOnWebApplicationTests { ...@@ -118,6 +122,15 @@ public class ConditionalOnWebApplicationTests {
return "reactive"; return "reactive";
} }
@Bean
public ReactiveWebServerFactory reactiveWebServerFactory() {
return new MockReactiveWebServerFactory();
}
@Bean
public HttpHandler httpHandler() {
return (request, response) -> Mono.empty();
}
} }
} }
...@@ -16,13 +16,142 @@ ...@@ -16,13 +16,142 @@
package org.springframework.boot.context.embedded; 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.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 * A {@link AnnotationConfigApplicationContext} that can be used to bootstrap
* embedded web server factory bean. * itself from a contained embedded web server factory bean.
*
* @author Brian Clozel * @author Brian Clozel
* @since 2.0.0 * @since 2.0.0
*/ */
public class ReactiveWebApplicationContext extends AnnotationConfigApplicationContext { 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);
}
}
}
} }
...@@ -29,7 +29,6 @@ import org.springframework.core.env.Environment; ...@@ -29,7 +29,6 @@ import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
/** /**
* {@link ApplicationContextInitializer} that sets {@link Environment} properties for the * {@link ApplicationContextInitializer} that sets {@link Environment} properties for the
...@@ -54,11 +53,11 @@ public class ServerPortInfoApplicationContextInitializer ...@@ -54,11 +53,11 @@ public class ServerPortInfoApplicationContextInitializer
@Override @Override
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener( applicationContext.addApplicationListener(
new ApplicationListener<EmbeddedServletContainerInitializedEvent>() { new ApplicationListener<EmbeddedWebServerInitializedEvent>() {
@Override @Override
public void onApplicationEvent( public void onApplicationEvent(
EmbeddedServletContainerInitializedEvent event) { EmbeddedWebServerInitializedEvent event) {
ServerPortInfoApplicationContextInitializer.this ServerPortInfoApplicationContextInitializer.this
.onApplicationEvent(event); .onApplicationEvent(event);
} }
...@@ -66,19 +65,12 @@ public class ServerPortInfoApplicationContextInitializer ...@@ -66,19 +65,12 @@ public class ServerPortInfoApplicationContextInitializer
}); });
} }
protected void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) { protected void onApplicationEvent(EmbeddedWebServerInitializedEvent event) {
String propertyName = getPropertyName(event.getApplicationContext()); String propertyName = "local." + event.getServerId() + ".port";
setPortProperty(event.getApplicationContext(), propertyName, setPortProperty(event.getApplicationContext(), propertyName,
event.getEmbeddedWebServer().getPort()); 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, private void setPortProperty(ApplicationContext context, String propertyName,
int port) { int port) {
......
...@@ -36,6 +36,7 @@ import org.junit.Rule; ...@@ -36,6 +36,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.CachedIntrospectionResults; import org.springframework.beans.CachedIntrospectionResults;
...@@ -44,6 +45,7 @@ import org.springframework.beans.factory.support.BeanNameGenerator; ...@@ -44,6 +45,7 @@ import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
import org.springframework.boot.context.embedded.ReactiveWebApplicationContext; 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.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent;
...@@ -77,6 +79,7 @@ import org.springframework.core.io.ClassPathResource; ...@@ -77,6 +79,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -400,7 +403,7 @@ public class SpringApplicationTests { ...@@ -400,7 +403,7 @@ public class SpringApplicationTests {
@Test @Test
public void defaultApplicationContextForReactiveWeb() throws Exception { public void defaultApplicationContextForReactiveWeb() throws Exception {
SpringApplication application = new SpringApplication(ExampleWebConfig.class); SpringApplication application = new SpringApplication(ExampleReactiveWebConfig.class);
application.setWebApplicationType(WebApplicationType.REACTIVE); application.setWebApplicationType(WebApplicationType.REACTIVE);
this.context = application.run(); this.context = application.run();
assertThat(this.context).isInstanceOf(ReactiveWebApplicationContext.class); assertThat(this.context).isInstanceOf(ReactiveWebApplicationContext.class);
...@@ -571,7 +574,7 @@ public class SpringApplicationTests { ...@@ -571,7 +574,7 @@ public class SpringApplicationTests {
@Test @Test
public void loadSources() throws Exception { public void loadSources() throws Exception {
Object[] sources = { ExampleConfig.class, "a", TestCommandLineRunner.class }; Object[] sources = {ExampleConfig.class, "a", TestCommandLineRunner.class};
TestSpringApplication application = new TestSpringApplication(sources); TestSpringApplication application = new TestSpringApplication(sources);
application.setWebApplicationType(WebApplicationType.NONE); application.setWebApplicationType(WebApplicationType.NONE);
application.setUseMockLoader(true); application.setUseMockLoader(true);
...@@ -583,7 +586,7 @@ public class SpringApplicationTests { ...@@ -583,7 +586,7 @@ public class SpringApplicationTests {
@Test @Test
public void wildcardSources() { public void wildcardSources() {
Object[] sources = { 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); TestSpringApplication application = new TestSpringApplication(sources);
application.setWebApplicationType(WebApplicationType.NONE); application.setWebApplicationType(WebApplicationType.NONE);
this.context = application.run(); this.context = application.run();
...@@ -598,7 +601,7 @@ public class SpringApplicationTests { ...@@ -598,7 +601,7 @@ public class SpringApplicationTests {
@Test @Test
public void runComponents() throws Exception { public void runComponents() throws Exception {
this.context = SpringApplication.run( 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(); assertThat(this.context).isNotNull();
} }
...@@ -713,7 +716,7 @@ public class SpringApplicationTests { ...@@ -713,7 +716,7 @@ public class SpringApplicationTests {
public void defaultCommandLineArgs() throws Exception { public void defaultCommandLineArgs() throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class); SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setDefaultProperties(StringUtils.splitArrayElementsIntoProperties( application.setDefaultProperties(StringUtils.splitArrayElementsIntoProperties(
new String[] { "baz=", "bar=spam" }, "=")); new String[] {"baz=", "bar=spam"}, "="));
application.setWebApplicationType(WebApplicationType.NONE); application.setWebApplicationType(WebApplicationType.NONE);
this.context = application.run("--bar=foo", "bucket", "crap"); this.context = application.run("--bar=foo", "bucket", "crap");
assertThat(this.context).isInstanceOf(AnnotationConfigApplicationContext.class); assertThat(this.context).isInstanceOf(AnnotationConfigApplicationContext.class);
...@@ -864,7 +867,7 @@ public class SpringApplicationTests { ...@@ -864,7 +867,7 @@ public class SpringApplicationTests {
assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar"); assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar");
assertThat(this.context.getEnvironment().getPropertySources().iterator().next() assertThat(this.context.getEnvironment().getPropertySources().iterator().next()
.getName()).isEqualTo( .getName()).isEqualTo(
TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
} }
@Test @Test
...@@ -877,7 +880,7 @@ public class SpringApplicationTests { ...@@ -877,7 +880,7 @@ public class SpringApplicationTests {
FailingConfig.class); FailingConfig.class);
application.setWebApplicationType(WebApplicationType.NONE); application.setWebApplicationType(WebApplicationType.NONE);
application.run(); application.run();
}; }
}; };
thread.start(); thread.start();
thread.join(6000); thread.join(6000);
...@@ -1028,6 +1031,22 @@ public class SpringApplicationTests { ...@@ -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 @Configuration
static class FailingConfig { static class FailingConfig {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment