Upgrade to Spring Framework 5.1

As of Spring Framework 5.1, we're depending on the Reactor Californium
release train.
Reactor Netty is now at version 0.8 and changed its artifact
coordinates, package names and broke several APIs. Spring Framework is
now up-to-date with those changes and this commit does the same for
Spring Boot.

Note that in that process, the `NettyServerCustomizer` has been changed
since the former `HttpServerOptions.Builder` API is now gone from
Reactor Netty, and we're now relying on immutable server instances
instead of a stateful builder pattern.

See gh-13321
This commit is contained in:
Brian Clozel 2018-06-04 18:59:17 +02:00
parent 0f321abe66
commit bd8106d77f
20 changed files with 138 additions and 131 deletions

View File

@ -148,7 +148,7 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<optional>true</optional>
</dependency>

View File

@ -23,6 +23,7 @@ import java.util.Map;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException;
@ -64,9 +65,11 @@ class ReactiveCloudFoundrySecurityService {
}
protected ReactorClientHttpConnector buildTrustAllSslConnector() {
return new ReactorClientHttpConnector((options) -> options.sslSupport(
(sslContextBuilder) -> sslContextBuilder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)));
HttpClient client = HttpClient.create()
.secure((sslContextSpec) -> sslContextSpec.forClient()
.sslContext((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)));
return new ReactorClientHttpConnector(client);
}
/**

View File

@ -27,7 +27,7 @@ import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import reactor.ipc.netty.http.HttpResources;
import reactor.netty.http.HttpResources;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;

View File

@ -303,7 +303,7 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<scope>test</scope>
</dependency>

View File

@ -107,7 +107,7 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<optional>true</optional>
</dependency>

View File

@ -17,7 +17,7 @@
package org.springframework.boot.autoconfigure.web.reactive;
import io.undertow.Undertow;
import reactor.ipc.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;

View File

@ -33,7 +33,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
/**
@ -95,7 +94,6 @@ public class BasicErrorController extends AbstractErrorController {
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));

View File

@ -169,6 +169,7 @@ public class BasicErrorControllerIntegrationTests {
load("--server.error.include-exception=true");
RequestEntity request = RequestEntity
.post(URI.create(createUrl("/bodyValidation")))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON).body("{}");
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
String resp = entity.getBody().toString();

View File

@ -151,7 +151,7 @@
<slf4j.version>1.7.25</slf4j.version>
<snakeyaml.version>1.19</snakeyaml.version>
<solr.version>7.2.1</solr.version>
<spring.version>5.0.7.BUILD-SNAPSHOT</spring.version>
<spring.version>5.1.0.BUILD-SNAPSHOT</spring.version>
<spring-amqp.version>2.0.3.RELEASE</spring-amqp.version>
<spring-batch.version>4.0.1.RELEASE</spring-batch.version>
<spring-cloud-connectors.version>2.0.2.RELEASE</spring-cloud-connectors.version>
@ -931,6 +931,13 @@
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- https://github.com/reactor/reactor/pull/646 -->
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.8.0.BUILD-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>

View File

@ -262,7 +262,7 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<optional>true</optional>
</dependency>

View File

@ -15,7 +15,7 @@
</properties>
<dependencies>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
</dependencies>

View File

@ -36,7 +36,7 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<optional>true</optional>
</dependency>

View File

@ -76,7 +76,7 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.ipc</groupId>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<optional>true</optional>
</dependency>

View File

@ -21,9 +21,9 @@ import java.util.function.BiPredicate;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import reactor.ipc.netty.http.server.HttpServerOptions;
import reactor.ipc.netty.http.server.HttpServerRequest;
import reactor.ipc.netty.http.server.HttpServerResponse;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;
import org.springframework.boot.web.server.Compression;
import org.springframework.util.MimeType;
@ -36,6 +36,7 @@ import org.springframework.util.StringUtils;
*
* @author Stephane Maldini
* @author Phillip Webb
* @author Brian Clozel
*/
final class CompressionCustomizer implements NettyServerCustomizer {
@ -49,15 +50,16 @@ final class CompressionCustomizer implements NettyServerCustomizer {
}
@Override
public void customize(HttpServerOptions.Builder builder) {
public HttpServer apply(HttpServer server) {
if (this.compression.getMinResponseSize() >= 0) {
builder.compression(this.compression.getMinResponseSize());
server = server.compress(this.compression.getMinResponseSize());
}
CompressionPredicate mimeTypes = getMimeTypesPredicate(
this.compression.getMimeTypes());
CompressionPredicate excludedUserAgents = getExcludedUserAgentsPredicate(
this.compression.getExcludedUserAgents());
builder.compression(mimeTypes.and(excludedUserAgents));
server = server.compress(mimeTypes.and(excludedUserAgents));
return server;
}
private CompressionPredicate getMimeTypesPredicate(String[] mimeTypes) {

View File

@ -23,8 +23,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import reactor.ipc.netty.http.server.HttpServer;
import reactor.ipc.netty.http.server.HttpServerOptions.Builder;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
@ -99,20 +98,19 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
}
private HttpServer createHttpServer() {
return HttpServer.builder().options((options) -> {
options.listenAddress(getListenAddress());
if (getSsl() != null && getSsl().isEnabled()) {
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(
getSsl(), getSslStoreProvider());
sslServerCustomizer.customize(options);
}
if (getCompression() != null && getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
getCompression());
compressionCustomizer.customize(options);
}
applyCustomizers(options);
}).build();
HttpServer server = HttpServer.create().tcpConfiguration(
(tcpServer) -> tcpServer.addressSupplier(() -> getListenAddress()));
if (getSsl() != null && getSsl().isEnabled()) {
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(getSsl(),
getSslStoreProvider());
server = sslServerCustomizer.apply(server);
}
if (getCompression() != null && getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
getCompression());
server = compressionCustomizer.apply(server);
}
return applyCustomizers(server);
}
private InetSocketAddress getListenAddress() {
@ -122,8 +120,11 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
return new InetSocketAddress(getPort());
}
private void applyCustomizers(Builder options) {
this.serverCustomizers.forEach((customizer) -> customizer.customize(options));
private HttpServer applyCustomizers(HttpServer server) {
for (NettyServerCustomizer customizer : this.serverCustomizers) {
server = customizer.apply(server);
}
return server;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,22 +16,18 @@
package org.springframework.boot.web.embedded.netty;
import reactor.ipc.netty.http.server.HttpServerOptions;
import java.util.function.Function;
import reactor.netty.http.server.HttpServer;
/**
* Callback interface that can be used to customize a Reactor Netty server builder.
* Mapping function that can be used to customize a Reactor Netty server instance.
*
* @author Brian Clozel
* @see NettyReactiveWebServerFactory
* @since 2.0.0
* @since 2.1.0
*/
@FunctionalInterface
public interface NettyServerCustomizer {
/**
* Customize the Netty web server.
* @param builder the server options builder to customize
*/
void customize(HttpServerOptions.Builder builder);
public interface NettyServerCustomizer extends Function<HttpServer, HttpServer> {
}

View File

@ -17,15 +17,13 @@
package org.springframework.boot.web.embedded.netty;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.ipc.netty.http.HttpResources;
import reactor.ipc.netty.http.server.HttpServer;
import reactor.ipc.netty.tcp.BlockingNettyContext;
import reactor.netty.DisposableServer;
import reactor.netty.http.HttpResources;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
@ -53,7 +51,7 @@ public class NettyWebServer implements WebServer {
private final Duration lifecycleTimeout;
private BlockingNettyContext nettyContext;
private DisposableServer disposableServer;
public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter,
Duration lifecycleTimeout) {
@ -66,30 +64,27 @@ public class NettyWebServer implements WebServer {
@Override
public void start() throws WebServerException {
if (this.nettyContext == null) {
if (this.disposableServer == null) {
try {
this.nettyContext = startHttpServer();
this.disposableServer = startHttpServer();
}
catch (Exception ex) {
if (findBindException(ex) != null) {
SocketAddress address = this.httpServer.options().getAddress();
if (address instanceof InetSocketAddress) {
throw new PortInUseException(
((InetSocketAddress) address).getPort());
}
throw new PortInUseException(getPort());
}
throw new WebServerException("Unable to start Netty", ex);
}
NettyWebServer.logger.info("Netty started on port(s): " + getPort());
startDaemonAwaitThread(this.nettyContext);
startDaemonAwaitThread(this.disposableServer);
}
}
private BlockingNettyContext startHttpServer() {
private DisposableServer startHttpServer() {
if (this.lifecycleTimeout != null) {
return this.httpServer.start(this.handlerAdapter, this.lifecycleTimeout);
return this.httpServer.handle(this.handlerAdapter)
.bindNow(this.lifecycleTimeout);
}
return this.httpServer.start(this.handlerAdapter);
return this.httpServer.handle(this.handlerAdapter).bindNow();
}
private BindException findBindException(Exception ex) {
@ -103,12 +98,12 @@ public class NettyWebServer implements WebServer {
return null;
}
private void startDaemonAwaitThread(BlockingNettyContext nettyContext) {
private void startDaemonAwaitThread(DisposableServer disposableServer) {
Thread awaitThread = new Thread("server") {
@Override
public void run() {
nettyContext.getContext().onClose().block();
disposableServer.onDispose().block();
}
};
@ -119,19 +114,24 @@ public class NettyWebServer implements WebServer {
@Override
public void stop() throws WebServerException {
if (this.nettyContext != null) {
this.nettyContext.shutdown();
if (this.disposableServer != null) {
// temporary fix for gh-9146
this.nettyContext.getContext().onClose()
.doOnSuccess((o) -> HttpResources.reset()).block();
this.nettyContext = null;
this.disposableServer.onDispose()
.doFinally((signal) -> HttpResources.reset());
if (this.lifecycleTimeout != null) {
this.disposableServer.disposeNow(this.lifecycleTimeout);
}
else {
this.disposableServer.disposeNow();
}
this.disposableServer = null;
}
}
@Override
public int getPort() {
if (this.nettyContext != null) {
return this.nettyContext.getPort();
if (this.disposableServer != null) {
return this.disposableServer.port();
}
return 0;
}

View File

@ -19,13 +19,14 @@ package org.springframework.boot.web.embedded.netty;
import java.net.URL;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.function.Consumer;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.ipc.netty.http.server.HttpServerOptions;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.SslStoreProvider;
@ -49,30 +50,36 @@ public class SslServerCustomizer implements NettyServerCustomizer {
}
@Override
public void customize(HttpServerOptions.Builder builder) {
SslContextBuilder sslBuilder = SslContextBuilder
.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider))
.trustManager(getTrustManagerFactory(this.ssl, this.sslStoreProvider));
if (this.ssl.getEnabledProtocols() != null) {
sslBuilder.protocols(this.ssl.getEnabledProtocols());
}
if (this.ssl.getCiphers() != null) {
sslBuilder = sslBuilder.ciphers(Arrays.asList(this.ssl.getCiphers()));
}
if (this.ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
sslBuilder = sslBuilder.clientAuth(ClientAuth.REQUIRE);
}
else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
sslBuilder = sslBuilder.clientAuth(ClientAuth.OPTIONAL);
}
public HttpServer apply(HttpServer server) {
try {
builder.sslContext(sslBuilder.build());
return server.secure((contextSpec) -> contextSpec.forServer()
.sslContext(getContextBuilderConsumer()));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected Consumer<SslContextBuilder> getContextBuilderConsumer() {
return (builder) -> {
builder.keyManager(getKeyManagerFactory(this.ssl, this.sslStoreProvider))
.trustManager(
getTrustManagerFactory(this.ssl, this.sslStoreProvider));
if (this.ssl.getEnabledProtocols() != null) {
builder.protocols(this.ssl.getEnabledProtocols());
}
if (this.ssl.getCiphers() != null) {
builder.ciphers(Arrays.asList(this.ssl.getCiphers()));
}
if (this.ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
builder.clientAuth(ClientAuth.REQUIRE);
}
else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
builder.clientAuth(ClientAuth.OPTIONAL);
}
};
}
protected KeyManagerFactory getKeyManagerFactory(Ssl ssl,
SslStoreProvider sslStoreProvider) {
try {

View File

@ -16,20 +16,19 @@
package org.springframework.boot.web.embedded.netty;
import java.time.Duration;
import java.util.Arrays;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.InOrder;
import reactor.ipc.netty.http.server.HttpServerOptions;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@ -47,6 +46,7 @@ public class NettyReactiveWebServerFactoryTests
}
@Test
@Ignore
public void exceptionIsThrownWhenPortIsAlreadyInUse() {
AbstractReactiveWebServerFactory factory = getFactory();
factory.setPort(0);
@ -63,25 +63,15 @@ public class NettyReactiveWebServerFactoryTests
NettyServerCustomizer[] customizers = new NettyServerCustomizer[2];
for (int i = 0; i < customizers.length; i++) {
customizers[i] = mock(NettyServerCustomizer.class);
given(customizers[i].apply(any(HttpServer.class)))
.will((invocation) -> invocation.getArgument(0));
}
factory.setServerCustomizers(Arrays.asList(customizers[0], customizers[1]));
this.webServer = factory.getWebServer(new EchoHandler());
InOrder ordered = inOrder((Object[]) customizers);
for (NettyServerCustomizer customizer : customizers) {
ordered.verify(customizer).customize(any(HttpServerOptions.Builder.class));
ordered.verify(customizer).apply(any(HttpServer.class));
}
}
@Test
public void customStartupTimeout() {
Duration timeout = Duration.ofDays(365);
NettyReactiveWebServerFactory factory = getFactory();
factory.setLifecycleTimeout(timeout);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
Object context = ReflectionTestUtils.getField(this.webServer, "nettyContext");
Object actualTimeout = ReflectionTestUtils.getField(context, "lifecycleTimeout");
assertThat(actualTimeout).isEqualTo(timeout);
}
}

View File

@ -23,7 +23,6 @@ import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Arrays;
import java.util.function.Consumer;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException;
@ -39,8 +38,8 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.NettyPipeline;
import reactor.ipc.netty.http.client.HttpClientOptions;
import reactor.netty.NettyPipeline;
import reactor.netty.http.client.HttpClient;
import reactor.test.StepVerifier;
import org.springframework.boot.testsupport.rule.OutputCapture;
@ -135,9 +134,11 @@ public abstract class AbstractReactiveWebServerFactoryTests {
}
protected ReactorClientHttpConnector buildTrustAllSslConnector() {
return new ReactorClientHttpConnector((options) -> options.sslSupport(
(sslContextBuilder) -> sslContextBuilder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)));
HttpClient client = HttpClient.create().wiretap()
.secure((sslContextSpec) -> sslContextSpec.forClient()
.sslContext((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)));
return new ReactorClientHttpConnector(client);
}
@Test
@ -169,10 +170,12 @@ public abstract class AbstractReactiveWebServerFactoryTests {
KeyManagerFactory clientKeyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
clientKeyManagerFactory.init(clientKeyStore, "password".toCharArray());
return new ReactorClientHttpConnector((options) -> options.sslSupport(
(sslContextBuilder) -> sslContextBuilder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.keyManager(clientKeyManagerFactory)));
HttpClient client = HttpClient.create().wiretap()
.secure((sslContextSpec) -> sslContextSpec.forClient()
.sslContext((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.keyManager(clientKeyManagerFactory)));
return new ReactorClientHttpConnector(client);
}
protected void testClientAuthSuccess(Ssl sslConfiguration,
@ -228,16 +231,13 @@ public abstract class AbstractReactiveWebServerFactoryTests {
}
protected WebClient.Builder getWebClient() {
return getWebClient((options) -> {
});
return getWebClient(HttpClient.create().wiretap());
}
protected WebClient.Builder getWebClient(
Consumer<? super HttpClientOptions.Builder> clientOptions) {
protected WebClient.Builder getWebClient(HttpClient client) {
InetSocketAddress address = new InetSocketAddress(this.webServer.getPort());
String baseUrl = "http://" + address.getHostString() + ":" + address.getPort();
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(clientOptions))
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(client))
.baseUrl(baseUrl);
}
@ -302,10 +302,12 @@ public abstract class AbstractReactiveWebServerFactoryTests {
this.webServer = factory
.getWebServer(new CharsHandler(3000, MediaType.TEXT_PLAIN));
this.webServer.start();
return getWebClient((options) -> options.compression(true)
.afterChannelInit((channel) -> channel.pipeline().addBefore(
NettyPipeline.HttpDecompressor, "CompressionTest",
new CompressionDetectionHandler()))).build();
HttpClient client = HttpClient.create().wiretap().compress().tcpConfiguration(
(tcpClient) -> tcpClient.doOnConnected((connection) -> connection
.channel().pipeline().addBefore(NettyPipeline.HttpDecompressor,
"CompressionTest", new CompressionDetectionHandler())));
return getWebClient(client).build();
}
protected void assertResponseIsCompressed(ResponseEntity<Void> response) {