Skip to content

Commit 52b40ee

Browse files
committed
Merge pull request #12269 from smaldini:addNettyCompressionOptions
* pr/12269: Polish Add Netty Compression support
2 parents 0abe62e + 14d36c9 commit 52b40ee

File tree

5 files changed

+140
-16
lines changed

5 files changed

+140
-16
lines changed

spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ content into your application. Rather, pick only the properties that you need.
163163
server.compression.enabled=false # Whether response compression is enabled.
164164
server.compression.excluded-user-agents= # List of user-agents to exclude from compression.
165165
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.
166-
server.compression.min-response-size=2048 # Minimum response size that is required for compression to be performed.
166+
server.compression.min-response-size=2048 # Minimum "Content-Length" value that is required for compression to be performed.
167167
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.
168168
server.error.include-exception=false # Include the "exception" attribute.
169169
server.error.include-stacktrace=never # When to include a "stacktrace" attribute.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.embedded.netty;
18+
19+
import java.util.function.BiPredicate;
20+
21+
import io.netty.handler.codec.http.HttpHeaderNames;
22+
import reactor.ipc.netty.http.server.HttpServerOptions;
23+
import reactor.ipc.netty.http.server.HttpServerRequest;
24+
import reactor.ipc.netty.http.server.HttpServerResponse;
25+
26+
import org.springframework.boot.web.server.Compression;
27+
import org.springframework.util.MimeType;
28+
import org.springframework.util.MimeTypeUtils;
29+
30+
/**
31+
* Configure the HTTP compression on an Reactor Netty request/response handler.
32+
*
33+
* @author Stephane Maldini
34+
*/
35+
final class CompressionCustomizer implements NettyServerCustomizer {
36+
37+
private final Compression compression;
38+
39+
CompressionCustomizer(Compression compression) {
40+
this.compression = compression;
41+
}
42+
43+
@Override
44+
public void customize(HttpServerOptions.Builder builder) {
45+
if (this.compression.getMinResponseSize() >= 0) {
46+
builder.compression(this.compression.getMinResponseSize());
47+
}
48+
BiPredicate<HttpServerRequest, HttpServerResponse> compressPredicate = null;
49+
if (this.compression.getMimeTypes() != null &&
50+
this.compression.getMimeTypes().length > 0) {
51+
compressPredicate = new CompressibleMimeTypePredicate(this.compression.getMimeTypes());
52+
}
53+
if (this.compression.getExcludedUserAgents() != null &&
54+
this.compression.getExcludedUserAgents().length > 0) {
55+
BiPredicate<HttpServerRequest, HttpServerResponse> agentCompressPredicate =
56+
new CompressibleAgentPredicate(this.compression.getExcludedUserAgents());
57+
compressPredicate = compressPredicate == null ?
58+
agentCompressPredicate :
59+
compressPredicate.and(agentCompressPredicate);
60+
}
61+
if (compressPredicate != null) {
62+
builder.compression(compressPredicate);
63+
}
64+
}
65+
66+
private static class CompressibleAgentPredicate
67+
implements BiPredicate<HttpServerRequest, HttpServerResponse> {
68+
69+
private final String[] excludedAgents;
70+
71+
CompressibleAgentPredicate(String[] excludedAgents) {
72+
this.excludedAgents = new String[excludedAgents.length];
73+
System.arraycopy(excludedAgents, 0, this.excludedAgents, 0, excludedAgents.length);
74+
}
75+
76+
@Override
77+
public boolean test(HttpServerRequest request, HttpServerResponse response) {
78+
for (String excludedAgent : this.excludedAgents) {
79+
if (request.requestHeaders()
80+
.contains(HttpHeaderNames.USER_AGENT, excludedAgent, true)) {
81+
return false;
82+
}
83+
}
84+
return true;
85+
}
86+
}
87+
88+
private static class CompressibleMimeTypePredicate
89+
implements BiPredicate<HttpServerRequest, HttpServerResponse> {
90+
91+
private final MimeType[] mimeTypes;
92+
93+
CompressibleMimeTypePredicate(String[] mimeTypes) {
94+
this.mimeTypes = new MimeType[mimeTypes.length];
95+
for (int i = 0; i < mimeTypes.length; i++) {
96+
this.mimeTypes[i] = MimeTypeUtils.parseMimeType(mimeTypes[i]);
97+
}
98+
}
99+
100+
@Override
101+
public boolean test(HttpServerRequest request, HttpServerResponse response) {
102+
String contentType = response.responseHeaders()
103+
.get(HttpHeaderNames.CONTENT_TYPE);
104+
if (contentType != null) {
105+
for (MimeType mimeType : this.mimeTypes) {
106+
if (mimeType.isCompatibleWith(MimeTypeUtils.parseMimeType(contentType))) {
107+
return true;
108+
}
109+
}
110+
}
111+
return false;
112+
}
113+
114+
}
115+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ private HttpServer createHttpServer() {
106106
getSsl(), getSslStoreProvider());
107107
sslServerCustomizer.customize(options);
108108
}
109-
if (getCompression() != null) {
110-
options.compression(getCompression().getEnabled());
109+
if (getCompression() != null && getCompression().getEnabled()) {
110+
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
111+
getCompression());
112+
compressionCustomizer.customize(options);
111113
}
112114
applyCustomizers(options);
113115
}).build();

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Compression.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class Compression {
4343
private String[] excludedUserAgents = null;
4444

4545
/**
46-
* Minimum response size that is required for compression to be performed.
46+
* Minimum "Content-Length" value that is required for compression to be performed.
4747
*/
4848
private int minResponseSize = 2048;
4949

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import reactor.test.StepVerifier;
4747

4848
import org.springframework.boot.testsupport.rule.OutputCapture;
49-
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
5049
import org.springframework.boot.web.embedded.undertow.UndertowReactiveWebServerFactory;
5150
import org.springframework.boot.web.server.Compression;
5251
import org.springframework.boot.web.server.Ssl;
@@ -258,40 +257,47 @@ protected WebClient.Builder getWebClient(
258257
}
259258

260259
@Test
261-
public void compressionOfResponseToGetRequest() throws Exception {
260+
public void compressionOfResponseToGetRequest() {
262261
WebClient client = prepareCompressionTest();
263262
ResponseEntity<Void> response = client.get().exchange()
264263
.flatMap((res) -> res.toEntity(Void.class)).block();
265264
assertResponseIsCompressed(response);
266265
}
267266

268267
@Test
269-
public void compressionOfResponseToPostRequest() throws Exception {
268+
public void compressionOfResponseToPostRequest() {
270269
WebClient client = prepareCompressionTest();
271270
ResponseEntity<Void> response = client.post().exchange()
272271
.flatMap((res) -> res.toEntity(Void.class)).block();
273272
assertResponseIsCompressed(response);
274273
}
275274

276275
@Test
277-
public void noCompressionForMimeType() throws Exception {
278-
Assumptions.assumeThat(getFactory())
279-
.isNotInstanceOf(NettyReactiveWebServerFactory.class);
276+
public void noCompressionForSmallResponse() {
280277
Compression compression = new Compression();
281-
compression.setMimeTypes(new String[] { "application/json" });
278+
compression.setEnabled(true);
279+
compression.setMinResponseSize(3001);
282280
WebClient client = prepareCompressionTest(compression);
283281
ResponseEntity<Void> response = client.get().exchange()
284282
.flatMap((res) -> res.toEntity(Void.class)).block();
285283
assertResponseIsNotCompressed(response);
286284
}
287285

288286
@Test
289-
public void noCompressionForUserAgent() throws Exception {
290-
Assumptions.assumeThat(getFactory())
291-
.isNotInstanceOf(NettyReactiveWebServerFactory.class);
287+
public void noCompressionForMimeType() {
288+
Compression compression = new Compression();
289+
compression.setMimeTypes(new String[] {"application/json"});
290+
WebClient client = prepareCompressionTest(compression);
291+
ResponseEntity<Void> response = client.get().exchange()
292+
.flatMap((res) -> res.toEntity(Void.class)).block();
293+
assertResponseIsNotCompressed(response);
294+
}
295+
296+
@Test
297+
public void noCompressionForUserAgent() {
292298
Compression compression = new Compression();
293299
compression.setEnabled(true);
294-
compression.setExcludedUserAgents(new String[] { "testUserAgent" });
300+
compression.setExcludedUserAgents(new String[] {"testUserAgent"});
295301
WebClient client = prepareCompressionTest(compression);
296302
ResponseEntity<Void> response = client.get().header("User-Agent", "testUserAgent")
297303
.exchange().flatMap((res) -> res.toEntity(Void.class)).block();
@@ -342,7 +348,7 @@ protected static class CompressionDetectionHandler
342348
extends ChannelInboundHandlerAdapter {
343349

344350
@Override
345-
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
351+
public void channelRead(ChannelHandlerContext ctx, Object msg) {
346352
if (msg instanceof HttpResponse) {
347353
HttpResponse response = (HttpResponse) msg;
348354
boolean compressed = response.headers()
@@ -375,6 +381,7 @@ public CharsHandler(int contentSize, MediaType mediaType) {
375381
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
376382
response.setStatusCode(HttpStatus.OK);
377383
response.getHeaders().setContentType(this.mediaType);
384+
response.getHeaders().setContentLength(this.bytes.readableByteCount());
378385
return response.writeWith(Mono.just(this.bytes));
379386
}
380387

0 commit comments

Comments
 (0)