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
This commit is contained in:
Brian Clozel
2018-10-12 11:24:51 +02:00
parent 11efe92ce7
commit 2588a71ac4
6 changed files with 156 additions and 39 deletions

View File

@@ -28,6 +28,8 @@ import org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerF
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 @@ abstract class ReactiveWebServerFactoryConfiguration {
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;
}
}

View File

@@ -46,7 +46,7 @@ class ClientHttpConnectorConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactorResourceFactory reactorResourceFactory() {
public ReactorResourceFactory reactorClientResourceFactory() {
return new ReactorResourceFactory();
}
@@ -66,7 +66,7 @@ class ClientHttpConnectorConfiguration {
@Bean
@ConditionalOnMissingBean
public JettyResourceFactory jettyResourceFactory() {
public JettyResourceFactory jettyClientResourceFactory() {
return new JettyResourceFactory();
}

View File

@@ -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-webclient-runtime, WebClient Runtime section>>.
[[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 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.
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.
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

View File

@@ -28,6 +28,7 @@ import org.eclipse.jetty.server.AbstractConnector;
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.eclipse.jetty.util.thread.ThreadPool;
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<JettyServerCustomizer> jettyServerCustomizers = new ArrayList<>();
private JettyResourceFactory resourceFactory;
private ThreadPool threadPool;
/**
@@ -129,11 +133,41 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
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 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
}
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 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
.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;
}
}

View File

@@ -25,10 +25,12 @@ import java.util.List;
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 class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
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 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
getCompression());
server = compressionCustomizer.apply(server);
}
server = server.protocol(listProtocols());
server = server.forwarded(this.useForwardHeaders);
server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders);
return applyCustomizers(server);
}

View File

@@ -26,6 +26,7 @@ import org.junit.Test;
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 class JettyReactiveWebServerFactoryTests
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());
}
}