Commit f331ac13 authored by Brian Clozel's avatar Brian Clozel

Add reactive web server infrastructure

This commit adds the infrastructure for creating and customizing
reactive embedded web servers. Common configuration has been refactored
into the new `ConfigurableEmbeddedWebServer` interface.

See gh-8302
parent a38d2456
......@@ -112,6 +112,7 @@ org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguratio
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.webflux.ReactiveWebServerAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
......
/*
* 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;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.util.Assert;
/**
* Abstract base class for {@link ConfigurableReactiveWebServer} implementations.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class AbstractConfigurableReactiveWebServer implements ConfigurableReactiveWebServer {
private int port = 8080;
private Set<ErrorPage> errorPages = new LinkedHashSet<ErrorPage>();
private InetAddress address;
private Ssl ssl;
private SslStoreProvider sslStoreProvider;
private Compression compression;
private String serverHeader;
/**
* Create a new {@link AbstractConfigurableReactiveWebServer} instance.
*/
public AbstractConfigurableReactiveWebServer() {
}
/**
* Create a new {@link AbstractConfigurableReactiveWebServer} instance with the
* specified port.
* @param port the port number for the reactive web server
*/
public AbstractConfigurableReactiveWebServer(int port) {
this.port = port;
}
@Override
public void setAddress(InetAddress address) {
this.address = address;
}
/**
* Return the address that the reactive web server binds to.
* @return the address
*/
public InetAddress getAddress() {
return this.address;
}
@Override
public void setPort(int port) {
this.port = port;
}
/**
* The port that the reactive web server listens on.
* @return the port
*/
public int getPort() {
return this.port;
}
@Override
public void setErrorPages(Set<? extends ErrorPage> errorPages) {
Assert.notNull(errorPages, "ErrorPages must not be null");
this.errorPages = new LinkedHashSet<ErrorPage>(errorPages);
}
@Override
public void addErrorPages(ErrorPage... errorPages) {
Assert.notNull(errorPages, "ErrorPages must not be null");
this.errorPages.addAll(Arrays.asList(errorPages));
}
/**
* Return a mutable set of {@link ErrorPage ErrorPages} that will be used when
* handling exceptions.
* @return the error pages
*/
public Set<ErrorPage> getErrorPages() {
return this.errorPages;
}
@Override
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
public Ssl getSsl() {
return this.ssl;
}
@Override
public void setSslStoreProvider(SslStoreProvider sslStoreProvider) {
this.sslStoreProvider = sslStoreProvider;
}
public SslStoreProvider getSslStoreProvider() {
return this.sslStoreProvider;
}
public Compression getCompression() {
return this.compression;
}
@Override
public void setCompression(Compression compression) {
this.compression = compression;
}
public String getServerHeader() {
return this.serverHeader;
}
@Override
public void setServerHeader(String serverHeader) {
this.serverHeader = serverHeader;
}
}
/*
* 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;
/**
* Abstract base class for {@link ReactiveWebServerFactory} implementations.
*
* @author Brian Clozel
* @since 2.0.0
*/
public abstract class AbstractReactiveWebServerFactory
extends AbstractConfigurableReactiveWebServer
implements ReactiveWebServerFactory {
public AbstractReactiveWebServerFactory() {
}
public AbstractReactiveWebServerFactory(int port) {
super(port);
}
}
......@@ -17,16 +17,12 @@
package org.springframework.boot.context.embedded;
import java.io.File;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.boot.web.servlet.ErrorPageRegistry;
import org.springframework.boot.web.servlet.ServletContextInitializer;
/**
......@@ -41,7 +37,7 @@ import org.springframework.boot.web.servlet.ServletContextInitializer;
* @see EmbeddedServletContainerFactory
* @see EmbeddedServletContainerCustomizer
*/
public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry {
public interface ConfigurableEmbeddedServletContainer extends ConfigurableEmbeddedWebServer {
/**
* Sets the context path for the embedded servlet container. The context should start
......@@ -59,14 +55,6 @@ public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry
*/
void setDisplayName(String displayName);
/**
* Sets the port that the embedded servlet container should listen on. If not
* specified port '8080' will be used. Use port -1 to disable auto-start (i.e start
* the web application context but not have it listen to any port).
* @param port the port to set
*/
void setPort(int port);
/**
* The session timeout in seconds (default 30 minutes). If 0 or negative then sessions
* never expire.
......@@ -94,12 +82,6 @@ public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry
*/
void setSessionStoreDir(File sessionStoreDir);
/**
* Sets the specific network address that the server should bind to.
* @param address the address to set (defaults to {@code null})
*/
void setAddress(InetAddress address);
/**
* Set if the DefaultServlet should be registered. Defaults to {@code true} so that
* files from the {@link #setDocumentRoot(File) document root} will be served.
......@@ -107,12 +89,6 @@ public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry
*/
void setRegisterDefaultServlet(boolean registerDefaultServlet);
/**
* Sets the error pages that will be used when handling exceptions.
* @param errorPages the error pages
*/
void setErrorPages(Set<? extends ErrorPage> errorPages);
/**
* Sets the mime-type mappings.
* @param mimeMappings the mime type mappings (defaults to
......@@ -146,38 +122,12 @@ public interface ConfigurableEmbeddedServletContainer extends ErrorPageRegistry
*/
void addInitializers(ServletContextInitializer... initializers);
/**
* Sets the SSL configuration that will be applied to the container's default
* connector.
* @param ssl the SSL configuration
*/
void setSsl(Ssl ssl);
/**
* Sets a provider that will be used to obtain SSL stores.
* @param sslStoreProvider the SSL store provider
*/
void setSslStoreProvider(SslStoreProvider sslStoreProvider);
/**
* Sets the configuration that will be applied to the container's JSP servlet.
* @param jsp the JSP servlet configuration
*/
void setJsp(Jsp jsp);
/**
* Sets the compression configuration that will be applied to the container's default
* connector.
* @param compression the compression configuration
*/
void setCompression(Compression compression);
/**
* Sets the server header value.
* @param serverHeader the server header value
*/
void setServerHeader(String serverHeader);
/**
* Sets the Locale to Charset mappings.
* @param localeCharsetMappings the Locale to Charset mappings
......
/*
* 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;
import java.net.InetAddress;
import java.util.Set;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.boot.web.servlet.ErrorPageRegistry;
/**
* Interface that regroups common customizations to
* embedded server factories such as {@link EmbeddedServletContainerFactory}
* and {@link ReactiveWebServerFactory}.
* @author Brian Clozel
* @since 2.0.0
*/
public interface ConfigurableEmbeddedWebServer extends ErrorPageRegistry {
/**
* Sets the port that the embedded servlet container should listen on. If not
* specified port '8080' will be used. Use port -1 to disable auto-start (i.e start
* the web application context but not have it listen to any port).
* @param port the port to set
*/
void setPort(int port);
/**
* Sets the specific network address that the server should bind to.
* @param address the address to set (defaults to {@code null})
*/
void setAddress(InetAddress address);
/**
* Sets the error pages that will be used when handling exceptions.
* @param errorPages the error pages
*/
void setErrorPages(Set<? extends ErrorPage> errorPages);
/**
* Sets the SSL configuration that will be applied to the container's default
* connector.
* @param ssl the SSL configuration
*/
void setSsl(Ssl ssl);
/**
* Sets a provider that will be used to obtain SSL stores.
* @param sslStoreProvider the SSL store provider
*/
void setSslStoreProvider(SslStoreProvider sslStoreProvider);
/**
* Sets the compression configuration that will be applied to the container's default
* connector.
* @param compression the compression configuration
*/
void setCompression(Compression compression);
/**
* Sets the server header value.
* @param serverHeader the server header value
*/
void setServerHeader(String serverHeader);
}
/*
* 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;
/**
* Interface that represents customizations to a {@link ReactiveWebServerFactory}.
*
* @author Brian Clozel
* @since 2.0.0
*/
public interface ConfigurableReactiveWebServer extends ConfigurableEmbeddedWebServer {
}
/*
* 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;
/**
* Strategy interface for customizing auto-configured embedded reactive servers. Any
* beans of this type will get a callback with the server factory before the server
* itself is started, so you can set the port, address, error pages etc.
* @author Brian Clozel
* @since 2.0.0
*/
@FunctionalInterface
public interface ReactiveWebServerCustomizer {
/**
* Customize the specified {@link ConfigurableReactiveWebServer}.
* @param server the server to customize
*/
void customize(ConfigurableReactiveWebServer server);
}
/*
* 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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
/**
* {@link BeanPostProcessor} that applies all {@link ReactiveWebServerCustomizer}s
* from the bean factory to {@link ConfigurableReactiveWebServer} beans.
*
*
* @author Brian Clozel
*/
public class ReactiveWebServerCustomizerBeanPostProcessor
implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
private List<ReactiveWebServerCustomizer> customizers;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof ConfigurableReactiveWebServer) {
postProcessBeforeInitialization((ConfigurableReactiveWebServer) bean);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
private void postProcessBeforeInitialization(
ConfigurableReactiveWebServer bean) {
for (ReactiveWebServerCustomizer customizer : getCustomizers()) {
customizer.customize(bean);
}
}
private Collection<ReactiveWebServerCustomizer> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<ReactiveWebServerCustomizer>(
this.applicationContext
.getBeansOfType(ReactiveWebServerCustomizer.class,
false, false)
.values());
Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
}
/*
* 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;
import java.util.Map;
import org.springframework.context.ApplicationContext;
import org.springframework.http.server.reactive.HttpHandler;
/**
* Factory interface that can be used to create reactive {@link EmbeddedWebServer}s.
*
* @author Brian Clozel
* @since 2.0.0
* @see EmbeddedWebServer
*/
public interface ReactiveWebServerFactory {
/**
* Gets a new fully configured but paused {@link EmbeddedWebServer} instance.
* Clients should not be able to connect to the returned server until
* {@link EmbeddedWebServer#start()} is called (which happens when the
* {@link ApplicationContext} has been fully refreshed).
* @param httpHandler the HTTP handler in charge of processing requests
* @return a fully configured and started {@link EmbeddedWebServer}
* @see EmbeddedWebServer#stop()
*/
EmbeddedWebServer getReactiveHttpServer(HttpHandler httpHandler);
/**
* Register a map of {@link HttpHandler}s, each to a specific context path.
*
* @param handlerMap a map of context paths and the associated {@code HttpHandler}
* @return a fully configured and started {@link EmbeddedWebServer}
* @see EmbeddedWebServer#stop()
*/
EmbeddedWebServer getReactiveHttpServer(Map<String, HttpHandler> handlerMap);
}
/*
* 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;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.testutil.InternalOutputCapture;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.SocketUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base for testing classes that extends {@link AbstractReactiveWebServerFactory}.
*
* @author Brian Clozel
*/
public abstract class AbstractReactiveWebServerFactoryTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public InternalOutputCapture output = new InternalOutputCapture();
protected EmbeddedWebServer webServer;
@After
public void tearDown() {
if (this.webServer != null) {
try {
this.webServer.stop();
}
catch (Exception ex) {
// Ignore
}
}
}
protected abstract AbstractReactiveWebServerFactory getFactory();
@Test
public void startStopServer() {
this.webServer = getFactory().getReactiveHttpServer(new EchoHandler());
this.webServer.start();
Mono<String> result = getWebClient()
.post().uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.exchange(BodyInserters.fromObject("Hello World"))
.then(response -> response.bodyToMono(String.class));
assertThat(result.block()).isEqualTo("Hello World");
this.webServer.stop();
Mono<ClientResponse> response = getWebClient()
.post().uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.exchange(BodyInserters.fromObject("Hello World"));
StepVerifier.create(response)
.expectError()
.verify();
}
@Test
public void specificPort() throws Exception {
AbstractReactiveWebServerFactory factory = getFactory();
int specificPort = SocketUtils.findAvailableTcpPort(41000);
factory.setPort(specificPort);
this.webServer = factory.getReactiveHttpServer(new EchoHandler());
this.webServer.start();
Mono<String> result = WebClient.create("http://localhost:" + specificPort)
.post().uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.exchange(BodyInserters.fromObject("Hello World"))
.then(response -> response.bodyToMono(String.class));
assertThat(result.block()).isEqualTo("Hello World");
assertThat(this.webServer.getPort()).isEqualTo(specificPort);
}
protected WebClient getWebClient() {
return WebClient.create("http://localhost:" + this.webServer.getPort());
}
protected static class EchoHandler implements HttpHandler {
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.OK);
return response.writeWith(request.getBody());
}
}
}
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