diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index fc6dae6e13..3551095503 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -163,7 +163,7 @@ content into your application. Rather, pick only the properties that you need. server.compression.enabled=false # Whether response compression is enabled. server.compression.excluded-user-agents= # List of user-agents to exclude from compression. server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript # Comma-separated list of MIME types that should be compressed. - server.compression.min-response-size=2048 # Minimum response size that is required for compression to be performed. + server.compression.min-response-size=2048 # Minimum "Content-Length" value that is required for compression to be performed. server.connection-timeout= # Time that connectors wait for another HTTP request before closing the connection. When not set, the connector's container-specific default is used. Use a value of -1 to indicate no (that is, an infinite) timeout. server.error.include-exception=false # Include the "exception" attribute. server.error.include-stacktrace=never # When to include a "stacktrace" attribute. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/CompressionCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/CompressionCustomizer.java new file mode 100644 index 0000000000..7130e8b3dc --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/CompressionCustomizer.java @@ -0,0 +1,115 @@ +/* + * 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. + * 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.web.embedded.netty; + +import java.util.function.BiPredicate; + +import io.netty.handler.codec.http.HttpHeaderNames; +import reactor.ipc.netty.http.server.HttpServerOptions; +import reactor.ipc.netty.http.server.HttpServerRequest; +import reactor.ipc.netty.http.server.HttpServerResponse; + +import org.springframework.boot.web.server.Compression; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Configure the HTTP compression on an Reactor Netty request/response handler. + * + * @author Stephane Maldini + */ +final class CompressionCustomizer implements NettyServerCustomizer { + + private final Compression compression; + + CompressionCustomizer(Compression compression) { + this.compression = compression; + } + + @Override + public void customize(HttpServerOptions.Builder builder) { + if (this.compression.getMinResponseSize() >= 0) { + builder.compression(this.compression.getMinResponseSize()); + } + BiPredicate compressPredicate = null; + if (this.compression.getMimeTypes() != null && + this.compression.getMimeTypes().length > 0) { + compressPredicate = new CompressibleMimeTypePredicate(this.compression.getMimeTypes()); + } + if (this.compression.getExcludedUserAgents() != null && + this.compression.getExcludedUserAgents().length > 0) { + BiPredicate agentCompressPredicate = + new CompressibleAgentPredicate(this.compression.getExcludedUserAgents()); + compressPredicate = compressPredicate == null ? + agentCompressPredicate : + compressPredicate.and(agentCompressPredicate); + } + if (compressPredicate != null) { + builder.compression(compressPredicate); + } + } + + private static class CompressibleAgentPredicate + implements BiPredicate { + + private final String[] excludedAgents; + + CompressibleAgentPredicate(String[] excludedAgents) { + this.excludedAgents = new String[excludedAgents.length]; + System.arraycopy(excludedAgents, 0, this.excludedAgents, 0, excludedAgents.length); + } + + @Override + public boolean test(HttpServerRequest request, HttpServerResponse response) { + for (String excludedAgent : this.excludedAgents) { + if (request.requestHeaders() + .contains(HttpHeaderNames.USER_AGENT, excludedAgent, true)) { + return false; + } + } + return true; + } + } + + private static class CompressibleMimeTypePredicate + implements BiPredicate { + + private final MimeType[] mimeTypes; + + CompressibleMimeTypePredicate(String[] mimeTypes) { + this.mimeTypes = new MimeType[mimeTypes.length]; + for (int i = 0; i < mimeTypes.length; i++) { + this.mimeTypes[i] = MimeTypeUtils.parseMimeType(mimeTypes[i]); + } + } + + @Override + public boolean test(HttpServerRequest request, HttpServerResponse response) { + String contentType = response.responseHeaders() + .get(HttpHeaderNames.CONTENT_TYPE); + if (contentType != null) { + for (MimeType mimeType : this.mimeTypes) { + if (mimeType.isCompatibleWith(MimeTypeUtils.parseMimeType(contentType))) { + return true; + } + } + } + return false; + } + + } +} 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 4d805bb118..970dab8e06 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 @@ -106,8 +106,10 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact getSsl(), getSslStoreProvider()); sslServerCustomizer.customize(options); } - if (getCompression() != null) { - options.compression(getCompression().getEnabled()); + if (getCompression() != null && getCompression().getEnabled()) { + CompressionCustomizer compressionCustomizer = new CompressionCustomizer( + getCompression()); + compressionCustomizer.customize(options); } applyCustomizers(options); }).build(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Compression.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Compression.java index fe4991781b..36d48d1f75 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Compression.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Compression.java @@ -43,7 +43,7 @@ public class Compression { private String[] excludedUserAgents = null; /** - * Minimum response size that is required for compression to be performed. + * Minimum "Content-Length" value that is required for compression to be performed. */ private int minResponseSize = 2048; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 6a51c775e5..385fd05582 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -46,7 +46,6 @@ import reactor.ipc.netty.http.client.HttpClientOptions; import reactor.test.StepVerifier; import org.springframework.boot.testsupport.rule.OutputCapture; -import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerFactory; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Ssl; @@ -258,7 +257,7 @@ public abstract class AbstractReactiveWebServerFactoryTests { } @Test - public void compressionOfResponseToGetRequest() throws Exception { + public void compressionOfResponseToGetRequest() { WebClient client = prepareCompressionTest(); ResponseEntity response = client.get().exchange() .flatMap((res) -> res.toEntity(Void.class)).block(); @@ -266,7 +265,7 @@ public abstract class AbstractReactiveWebServerFactoryTests { } @Test - public void compressionOfResponseToPostRequest() throws Exception { + public void compressionOfResponseToPostRequest() { WebClient client = prepareCompressionTest(); ResponseEntity response = client.post().exchange() .flatMap((res) -> res.toEntity(Void.class)).block(); @@ -274,11 +273,10 @@ public abstract class AbstractReactiveWebServerFactoryTests { } @Test - public void noCompressionForMimeType() throws Exception { - Assumptions.assumeThat(getFactory()) - .isNotInstanceOf(NettyReactiveWebServerFactory.class); + public void noCompressionForSmallResponse() { Compression compression = new Compression(); - compression.setMimeTypes(new String[] { "application/json" }); + compression.setEnabled(true); + compression.setMinResponseSize(3001); WebClient client = prepareCompressionTest(compression); ResponseEntity response = client.get().exchange() .flatMap((res) -> res.toEntity(Void.class)).block(); @@ -286,12 +284,20 @@ public abstract class AbstractReactiveWebServerFactoryTests { } @Test - public void noCompressionForUserAgent() throws Exception { - Assumptions.assumeThat(getFactory()) - .isNotInstanceOf(NettyReactiveWebServerFactory.class); + public void noCompressionForMimeType() { + Compression compression = new Compression(); + compression.setMimeTypes(new String[] {"application/json"}); + WebClient client = prepareCompressionTest(compression); + ResponseEntity response = client.get().exchange() + .flatMap((res) -> res.toEntity(Void.class)).block(); + assertResponseIsNotCompressed(response); + } + + @Test + public void noCompressionForUserAgent() { Compression compression = new Compression(); compression.setEnabled(true); - compression.setExcludedUserAgents(new String[] { "testUserAgent" }); + compression.setExcludedUserAgents(new String[] {"testUserAgent"}); WebClient client = prepareCompressionTest(compression); ResponseEntity response = client.get().header("User-Agent", "testUserAgent") .exchange().flatMap((res) -> res.toEntity(Void.class)).block(); @@ -342,7 +348,7 @@ public abstract class AbstractReactiveWebServerFactoryTests { extends ChannelInboundHandlerAdapter { @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpResponse) { HttpResponse response = (HttpResponse) msg; boolean compressed = response.headers() @@ -375,6 +381,7 @@ public abstract class AbstractReactiveWebServerFactoryTests { public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { response.setStatusCode(HttpStatus.OK); response.getHeaders().setContentType(this.mediaType); + response.getHeaders().setContentLength(this.bytes.readableByteCount()); return response.writeWith(Mono.just(this.bytes)); }