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
This commit is contained in:
Brian Clozel 2017-02-20 15:03:26 +01:00
parent 21878f8528
commit 0b162e894b
5 changed files with 194 additions and 23 deletions

View File

@ -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 {

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<EmbeddedServletContainerInitializedEvent>() {
new ApplicationListener<EmbeddedWebServerInitializedEvent>() {
@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) {

View File

@ -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 {