Browse Source
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-8302pull/8410/head
10 changed files with 586 additions and 51 deletions
@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
/* |
||||
* 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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* 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); |
||||
|
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* 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 { |
||||
|
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* 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); |
||||
} |
||||
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* 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); |
||||
} |
||||
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* 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()); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue