From 2588a71ac4d8b47a7b3b5a556cb50b490a136d7c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 12 Oct 2018 11:24:51 +0200 Subject: [PATCH] Auto-Configure HTTP ResourceFactories on servers This commit auto-configures HTTP resource factories on both Reactor Netty and Jetty server instances. This creates `ReactorResourceFactory` and `JettyResourceFactory` beans when necessary - those beans can be reused and applied by the client auto-configuration in order to share resources between client and server for optimal performance. The server auto-configuration has the highest precedence, so from now on, the auto-configured ResourceFactory bean on the client side will be skipped if a reactive server is configured. Closes gh-14495 --- ...ReactiveWebServerFactoryConfiguration.java | 28 +++++++-- .../ClientHttpConnectorConfiguration.java | 4 +- .../main/asciidoc/spring-boot-features.adoc | 53 +++++++++++++--- .../jetty/JettyReactiveWebServerFactory.java | 63 +++++++++++++------ .../netty/NettyReactiveWebServerFactory.java | 30 +++++++-- .../JettyReactiveWebServerFactoryTests.java | 17 +++++ 6 files changed, 156 insertions(+), 39 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java index fc2cf47591f0..9ed9c3c6dde8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java @@ -28,6 +28,8 @@ import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.JettyResourceFactory; +import org.springframework.http.client.reactive.ReactorResourceFactory; /** * Configuration classes for reactive web servers @@ -45,8 +47,17 @@ abstract class ReactiveWebServerFactoryConfiguration { static class EmbeddedNetty { @Bean - public NettyReactiveWebServerFactory nettyReactiveWebServerFactory() { - return new NettyReactiveWebServerFactory(); + @ConditionalOnMissingBean + public ReactorResourceFactory reactorServerResourceFactory() { + return new ReactorResourceFactory(); + } + + @Bean + public NettyReactiveWebServerFactory nettyReactiveWebServerFactory( + ReactorResourceFactory resourceFactory) { + NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(); + serverFactory.setResourceFactory(resourceFactory); + return serverFactory; } } @@ -69,8 +80,17 @@ public TomcatReactiveWebServerFactory tomcatReactiveWebServerFactory() { static class EmbeddedJetty { @Bean - public JettyReactiveWebServerFactory jettyReactiveWebServerFactory() { - return new JettyReactiveWebServerFactory(); + @ConditionalOnMissingBean + public JettyResourceFactory jettyServerResourceFactory() { + return new JettyResourceFactory(); + } + + @Bean + public JettyReactiveWebServerFactory jettyReactiveWebServerFactory( + JettyResourceFactory resourceFactory) { + JettyReactiveWebServerFactory serverFactory = new JettyReactiveWebServerFactory(); + serverFactory.setResourceFactory(resourceFactory); + return serverFactory; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java index 4e28a1c20c42..c5d8a856faec 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java @@ -46,7 +46,7 @@ public static class ReactorNetty { @Bean @ConditionalOnMissingBean - public ReactorResourceFactory reactorResourceFactory() { + public ReactorResourceFactory reactorClientResourceFactory() { return new ReactorResourceFactory(); } @@ -66,7 +66,7 @@ public static class JettyClient { @Bean @ConditionalOnMissingBean - public JettyResourceFactory jettyResourceFactory() { + public JettyResourceFactory jettyClientResourceFactory() { return new JettyResourceFactory(); } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 9ba652d11705..13f9cd1abb11 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -3211,6 +3211,35 @@ instead. There is a {github-code}/spring-boot-samples/spring-boot-sample-web-jsp[JSP sample] so that you can see how to set things up. +[[boot-features-reactive-server]] +=== Embedded Reactive Server Support + +Spring Boot includes support for the following embedded reactive web servers: +Reactor Netty, Tomcat, Jetty, and Undertow. Most developers use the appropriate “Starter” +to obtain a fully configured instance. By default, the embedded server listens for HTTP +requests on port 8080. + +[[boot-features-reactive-server-resources]] +=== Reactive Server Resources Configuration + +When auto-configuring a Reactor Netty or Jetty server, Spring Boot will create specific +beans that will provide HTTP resources to the server instance: `ReactorResourceFactory` +or `JettyResourceFactory`. + +By default, those resources will be also shared with the Reactor Netty and Jetty clients +for optimal performances, given: + +* the same technology is used for server and client +* the client instance is built using the `WebClient.Builder` bean auto-configured by +Spring Boot + +Developers can override the resource configuration for Jetty and Reactor Netty by providing +a custom `ReactorResourceFactory` or `JettyResourceFactory` bean - this will be applied to +both clients and servers. + +You can learn more about the resource configuration on the client side in the +<>. + [[boot-features-security]] @@ -5955,19 +5984,23 @@ The following code shows a typical example: [[boot-features-webclient-runtime]] === WebClient Runtime -Spring Boot will auto-detect which `ClientHttpConnector` to drive `WebClient`, depending -on the libraries available on the application classpath. +Spring Boot will auto-detect which `ClientHttpConnector` to use to drive `WebClient`, +depending on the libraries available on the application classpath. For now, Reactor +Netty and Jetty RS client are supported. -The `spring-boot-starter-webflux` depends on `io.projectreactor.netty:reactor-netty` by -default, which brings both server and client implementations. If you choose to use Jetty +The `spring-boot-starter-webflux` starter depends on `io.projectreactor.netty:reactor-netty` +by default, which brings both server and client implementations. If you choose to use Jetty as a reactive server instead, you should add a dependency on the Jetty Reactive HTTP -client library, `org.eclipse.jetty:jetty-reactive-httpclient`, because it will -automatically share HTTP resources with the server. +client library, `org.eclipse.jetty:jetty-reactive-httpclient`. Using the same technology +for server and client has it advantages, as it will automatically share HTTP resources +between client and server. + +Developers can override the resource configuration for Jetty and Reactor Netty by providing +a custom `ReactorResourceFactory` or `JettyResourceFactory` bean - this will be applied to +both clients and servers. -Developers can override this choice by defining their own `ClientHttpConnector` bean; -in this case, and depending on your HTTP client library of choice, you should also -define a resource factory bean that manages the HTTP resources for that client. -For example, a `ReactorResourceFactory` bean for the Reactor Netty client. +If you wish to override that choice for the client, you can define your own +`ClientHttpConnector` bean and have full control over the client configuration. You can learn more about the {spring-reference}web-reactive.html#webflux-client-builder[`WebClient` configuration diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index ea62d5cafa98..9c713a1f88ee 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -28,6 +28,7 @@ import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.HandlerWrapper; @@ -38,6 +39,7 @@ import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; +import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.JettyHttpHandlerAdapter; import org.springframework.util.Assert; @@ -69,6 +71,8 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact private List jettyServerCustomizers = new ArrayList<>(); + private JettyResourceFactory resourceFactory; + private ThreadPool threadPool; /** @@ -129,11 +133,41 @@ public Collection getServerCustomizers() { return this.jettyServerCustomizers; } + /** + * Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}. + * @return a Jetty {@link ThreadPool} or {@code null} + */ + public ThreadPool getThreadPool() { + return this.threadPool; + } + + /** + * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to + * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. + * @param threadPool a Jetty ThreadPool to be used + */ + public void setThreadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + } + @Override public void setSelectors(int selectors) { this.selectors = selectors; } + /** + * Set the {@link JettyResourceFactory} to get the shared resources from. + * @param resourceFactory the server resources + * @since 2.1.0 + */ + public void setResourceFactory(JettyResourceFactory resourceFactory) { + this.resourceFactory = resourceFactory; + } + + protected JettyResourceFactory getResourceFactory() { + return this.resourceFactory; + } + protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { int port = (getPort() >= 0) ? getPort() : 0; InetSocketAddress address = new InetSocketAddress(getAddress(), port); @@ -160,8 +194,16 @@ protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { } private AbstractConnector createConnector(InetSocketAddress address, Server server) { - ServerConnector connector = new ServerConnector(server, this.acceptors, - this.selectors); + ServerConnector connector; + JettyResourceFactory resourceFactory = getResourceFactory(); + if (resourceFactory != null) { + connector = new ServerConnector(server, resourceFactory.getExecutor(), + resourceFactory.getScheduler(), resourceFactory.getByteBufferPool(), + this.acceptors, this.selectors, new HttpConnectionFactory()); + } + else { + connector = new ServerConnector(server, this.acceptors, this.selectors); + } connector.setHost(address.getHostString()); connector.setPort(address.getPort()); for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) { @@ -195,21 +237,4 @@ private void customizeSsl(Server server, InetSocketAddress address) { .customize(server); } - /** - * Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}. - * @return a Jetty {@link ThreadPool} or {@code null} - */ - public ThreadPool getThreadPool() { - return this.threadPool; - } - - /** - * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to - * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. - * @param threadPool a Jetty ThreadPool to be used - */ - public void setThreadPool(ThreadPool threadPool) { - this.threadPool = threadPool; - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index 0e020556ffb3..67bd0cffe440 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -25,10 +25,12 @@ import reactor.netty.http.HttpProtocol; import reactor.netty.http.server.HttpServer; +import reactor.netty.resources.LoopResources; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; +import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; @@ -47,6 +49,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact private boolean useForwardHeaders; + private ReactorResourceFactory resourceFactory; + public NettyReactiveWebServerFactory() { } @@ -109,9 +113,28 @@ public void setUseForwardHeaders(boolean useForwardHeaders) { this.useForwardHeaders = useForwardHeaders; } + /** + * Set the {@link ReactorResourceFactory} to get the shared resources from. + * @param resourceFactory the server resources + * @since 2.1.0 + */ + public void setResourceFactory(ReactorResourceFactory resourceFactory) { + this.resourceFactory = resourceFactory; + } + private HttpServer createHttpServer() { - HttpServer server = HttpServer.create().tcpConfiguration( - (tcpServer) -> tcpServer.addressSupplier(this::getListenAddress)); + HttpServer server = HttpServer.create(); + if (this.resourceFactory != null) { + LoopResources resources = this.resourceFactory.getLoopResources(); + Assert.notNull(resources, + "No LoopResources: is ReactorResourceFactory not initialized yet?"); + server = server.tcpConfiguration((tcpServer) -> tcpServer.runOn(resources) + .addressSupplier(this::getListenAddress)); + } + else { + server = server.tcpConfiguration( + (tcpServer) -> tcpServer.addressSupplier(this::getListenAddress)); + } if (getSsl() != null && getSsl().isEnabled()) { SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(getSsl(), getHttp2(), getSslStoreProvider()); @@ -122,8 +145,7 @@ private HttpServer createHttpServer() { getCompression()); server = compressionCustomizer.apply(server); } - server = server.protocol(listProtocols()); - server = server.forwarded(this.useForwardHeaders); + server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders); return applyCustomizers(server); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java index 93a2618ac14e..296f81ee4b02 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -26,6 +26,7 @@ import org.mockito.InOrder; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; +import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.server.reactive.HttpHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -99,4 +100,20 @@ public void useForwardedHeaders() { assertForwardHeaderIsUsed(factory); } + @Test + public void useServerResources() throws Exception { + JettyResourceFactory resourceFactory = new JettyResourceFactory(); + resourceFactory.afterPropertiesSet(); + JettyReactiveWebServerFactory factory = getFactory(); + factory.setResourceFactory(resourceFactory); + JettyWebServer webServer = (JettyWebServer) factory + .getWebServer(new EchoHandler()); + webServer.start(); + Connector connector = webServer.getServer().getConnectors()[0]; + assertThat(connector.getByteBufferPool()) + .isEqualTo(resourceFactory.getByteBufferPool()); + assertThat(connector.getExecutor()).isEqualTo(resourceFactory.getExecutor()); + assertThat(connector.getScheduler()).isEqualTo(resourceFactory.getScheduler()); + } + }