From 5f25080091f919d4f07259df64e851e6c21ae10a Mon Sep 17 00:00:00 2001 From: Ivan Sopov Date: Mon, 29 Jun 2015 23:39:47 +0300 Subject: [PATCH] Add HTTP compression excludeUserAgents property Closes gh-3363 --- .../appendix-application-properties.adoc | 1 + .../boot/context/embedded/Compression.java | 14 ++++++- .../JettyEmbeddedServletContainerFactory.java | 15 +++++++ ...TomcatEmbeddedServletContainerFactory.java | 5 +++ .../UndertowEmbeddedServletContainer.java | 39 ++++++++++++------- ...tEmbeddedServletContainerFactoryTests.java | 29 +++++++++----- ...yEmbeddedServletContainerFactoryTests.java | 7 +++- 7 files changed, 85 insertions(+), 25 deletions(-) diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 24234afe67..91fa4c300c 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -68,6 +68,7 @@ content into your application; rather pick only the properties that you need. server.port=8080 server.address= # bind to a specific NIC server.compression.enabled=false # if response compression is enabled + server.compression.exclude-user-agents= # list of user-agents to exclude from compression server.compression.mime-types=text/html,text/xml,text/plain,text/css # 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.context-parameters.*= # Servlet context init parameters, e.g. server.context-parameters.a=alpha diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/Compression.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Compression.java index 10f840ac9e..592c09fe16 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/Compression.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Compression.java @@ -37,7 +37,12 @@ public class Compression { "text/css" }; /** - * Minimum response size that is required for compression to be performed + * Comma-separated list of user agents for which responses should not be compressed. + */ + private String[] excludedUserAgents = null; + + /** + * Minimum response size that is required for compression to be performed. */ private int minResponseSize = 2048; @@ -65,4 +70,11 @@ public class Compression { this.minResponseSize = minSize; } + public String[] getExcludedUserAgents() { + return this.excludedUserAgents; + } + + public void setExcludedUserAgents(String[] excludedUserAgents) { + this.excludedUserAgents = excludedUserAgents; + } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index 8a789e365b..c812ab52fd 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -588,6 +588,12 @@ public class JettyEmbeddedServletContainerFactory extends .invoke(handler, new HashSet(Arrays.asList(compression .getMimeTypes()))); + if (compression.getExcludedUserAgents() != null) { + ReflectionUtils.findMethod(handlerClass, "setExcluded", Set.class) + .invoke(handler, + new HashSet(Arrays.asList(compression + .getExcludedUserAgents()))); + } return handler; } catch (Exception ex) { @@ -605,6 +611,10 @@ public class JettyEmbeddedServletContainerFactory extends gzipHandler.setMinGzipSize(compression.getMinResponseSize()); gzipHandler.setMimeTypes(new HashSet(Arrays.asList(compression .getMimeTypes()))); + if (compression.getExcludedUserAgents() != null) { + gzipHandler.setExcluded(new HashSet(Arrays.asList(compression + .getExcludedUserAgents()))); + } return gzipHandler; } @@ -623,6 +633,11 @@ public class JettyEmbeddedServletContainerFactory extends ReflectionUtils.findMethod(handlerClass, "setIncludedMimeTypes", String[].class).invoke(handler, new Object[] { compression.getMimeTypes() }); + if (compression.getExcludedUserAgents() != null) { + ReflectionUtils.findMethod(handlerClass, "setExcludedAgentPatterns", + String[].class).invoke(handler, + new Object[] { compression.getExcludedUserAgents() }); + } return handler; } catch (Exception ex) { diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index 4c409fc0a9..06f22ff4cf 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -280,6 +280,11 @@ public class TomcatEmbeddedServletContainerFactory extends protocol.setCompressionMinSize(compression.getMinResponseSize()); protocol.setCompressableMimeTypes(StringUtils .arrayToCommaDelimitedString(compression.getMimeTypes())); + if (getCompression().getExcludedUserAgents() != null) { + protocol.setNoCompressionUserAgents(StringUtils + .arrayToCommaDelimitedString(getCompression() + .getExcludedUserAgents())); + } } } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java index 682db2ab07..128c6ac9e8 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.undertow; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.Undertow.Builder; +import io.undertow.attribute.RequestHeaderAttribute; import io.undertow.predicate.Predicate; import io.undertow.predicate.Predicates; import io.undertow.server.HttpHandler; @@ -27,6 +28,7 @@ import io.undertow.server.handlers.encoding.ContentEncodingRepository; import io.undertow.server.handlers.encoding.EncodingHandler; import io.undertow.server.handlers.encoding.GzipEncodingProvider; import io.undertow.servlet.api.DeploymentManager; +import io.undertow.util.HttpString; import java.lang.reflect.Field; import java.net.ServerSocket; @@ -100,8 +102,8 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine private Undertow createUndertowServer() { try { - HttpHandler servletHandler = this.manager.start(); - this.builder.setHandler(getContextHandler(servletHandler)); + HttpHandler httpHandler = this.manager.start(); + this.builder.setHandler(getContextHandler(httpHandler)); return this.builder.build(); } catch (ServletException ex) { @@ -110,25 +112,36 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine } } - private HttpHandler getContextHandler(HttpHandler servletHandler) { - HttpHandler contextHandler = configurationCompressionIfNecessary(servletHandler); + private HttpHandler getContextHandler(HttpHandler httpHandler) { + HttpHandler contextHandler = configurationCompressionIfNecessary(httpHandler); if (StringUtils.isEmpty(this.contextPath)) { return contextHandler; } return Handlers.path().addPrefixPath(this.contextPath, contextHandler); } - private HttpHandler configurationCompressionIfNecessary(HttpHandler servletHandler) { + private HttpHandler configurationCompressionIfNecessary(HttpHandler httpHandler) { if (this.compression == null || !this.compression.getEnabled()) { - return servletHandler; + return httpHandler; } - ContentEncodingRepository encodingRepository = new ContentEncodingRepository(); - Predicate mimeAndSizePredicate = Predicates.and(Predicates - .maxContentSize(this.compression.getMinResponseSize()), Predicates - .or(new CompressibleMimeTypePredicate(this.compression.getMimeTypes()))); - encodingRepository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50, - mimeAndSizePredicate); - return new EncodingHandler(encodingRepository).setNext(servletHandler); + ContentEncodingRepository repository = new ContentEncodingRepository(); + repository.addEncodingHandler("gzip", new GzipEncodingProvider(), 50, + Predicates.and(getCompressionPredicates(this.compression))); + return new EncodingHandler(repository).setNext(httpHandler); + } + + private Predicate[] getCompressionPredicates(Compression compression) { + List predicates = new ArrayList(); + predicates.add(Predicates.maxContentSize(compression.getMinResponseSize())); + predicates.add(new CompressibleMimeTypePredicate(compression.getMimeTypes())); + if (compression.getExcludedUserAgents() != null) { + for (String agent : compression.getExcludedUserAgents()) { + RequestHeaderAttribute agentHeader = new RequestHeaderAttribute( + new HttpString(HttpHeaders.USER_AGENT)); + predicates.add(Predicates.not(Predicates.regex(agentHeader, agent))); + } + } + return predicates.toArray(new Predicate[predicates.size()]); } private String getPortsDescription() { diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index 43e396c0b1..95444b6b53 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -531,35 +531,43 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { @Test public void compression() throws Exception { - assertTrue(doTestCompression(10000, null)); + assertTrue(doTestCompression(10000, null, null)); } @Test public void noCompressionForSmallResponse() throws Exception { - assertFalse(doTestCompression(100, null)); + assertFalse(doTestCompression(100, null, null)); } @Test public void noCompressionForMimeType() throws Exception { String[] mimeTypes = new String[] { "text/html", "text/xml", "text/css" }; - assertFalse(doTestCompression(10000, mimeTypes)); + assertFalse(doTestCompression(10000, mimeTypes, null)); } - private boolean doTestCompression(int contentSize, String[] mimeTypes) - throws Exception { - String testContent = setUpFactoryForCompression(contentSize, mimeTypes); + @Test + public void noCompressionForUserAgent() throws Exception { + assertFalse(doTestCompression(10000, null, new String[] { "testUserAgent" })); + } + + private boolean doTestCompression(int contentSize, String[] mimeTypes, + String[] excludedUserAgents) throws Exception { + String testContent = setUpFactoryForCompression(contentSize, mimeTypes, + excludedUserAgents); TestGzipInputStreamFactory inputStreamFactory = new TestGzipInputStreamFactory(); Map contentDecoderMap = singletonMap("gzip", (InputStreamFactory) inputStreamFactory); - String response = getResponse(getLocalUrl("/test.txt"), + String response = getResponse( + getLocalUrl("/test.txt"), new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create() + .setUserAgent("testUserAgent") .setContentDecoderRegistry(contentDecoderMap).build())); assertThat(response, equalTo(testContent)); return inputStreamFactory.wasCompressionUsed(); } - protected String setUpFactoryForCompression(int contentSize, String[] mimeTypes) - throws Exception { + protected String setUpFactoryForCompression(int contentSize, String[] mimeTypes, + String[] excludedUserAgents) throws Exception { char[] chars = new char[contentSize]; Arrays.fill(chars, 'F'); String testContent = new String(chars); @@ -572,6 +580,9 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { if (mimeTypes != null) { compression.setMimeTypes(mimeTypes); } + if (excludedUserAgents != null) { + compression.setExcludedUserAgents(excludedUserAgents); + } factory.setCompression(compression); this.container = factory.getEmbeddedServletContainer(); this.container.start(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index 97b00f90d1..3fd2610025 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -186,8 +186,8 @@ public class JettyEmbeddedServletContainerFactoryTests extends @Override @SuppressWarnings("serial") // Workaround for Jetty issue - https://bugs.eclipse.org/bugs/show_bug.cgi?id=470646 - protected String setUpFactoryForCompression(final int contentSize, String[] mimeTypes) - throws Exception { + protected String setUpFactoryForCompression(final int contentSize, + String[] mimeTypes, String[] excludedUserAgents) throws Exception { char[] chars = new char[contentSize]; Arrays.fill(chars, 'F'); final String testContent = new String(chars); @@ -197,6 +197,9 @@ public class JettyEmbeddedServletContainerFactoryTests extends if (mimeTypes != null) { compression.setMimeTypes(mimeTypes); } + if (excludedUserAgents != null) { + compression.setExcludedUserAgents(excludedUserAgents); + } factory.setCompression(compression); this.container = factory.getEmbeddedServletContainer(new ServletRegistrationBean( new HttpServlet() {