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:
parent
21878f8528
commit
0b162e894b
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
Loading…
Reference in New Issue