Performance improvements in ShallowEtagHeaderFilter

Prior to this change, the ShallowEtagHeaderFilter would use a
ResizableByteArrayOutputStream to internally write data and calculate
the ETag. While that implementation is faster than the regular
ByteArrayOutputStream (since it has a better strategy for growing the
internal buffer), a lot of buffer copying/writing still happens.

This change adds a new FastByteArrayOutputStream implementation that
internally uses a LinkedList<Byte[]> to store the content. So when
writing bytes to that OutputStream implementation, new byte[] are
added to the list when the previous ones are full. This saves most
of the instantiating/copying operations.

Note that new methods were added in DigestUtils to allow usage of
Streams instead of byte[], which is more efficient in our case.

Fixes #653

Issue: SPR-12081
This commit is contained in:
Craig Andrews
2015-01-07 16:53:09 +01:00
committed by Brian Clozel
parent 40cd1be14c
commit 213a3fd779
7 changed files with 909 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -17,6 +17,7 @@
package org.springframework.web.filter;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -26,7 +27,6 @@ import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.DigestUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
@@ -93,15 +93,12 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse();
int statusCode = responseWrapper.getStatusCode();
byte[] body = responseWrapper.getContentAsByteArray();
if (rawResponse.isCommitted()) {
if (body.length > 0) {
StreamUtils.copy(body, rawResponse.getOutputStream());
}
responseWrapper.copyBodyToResponse();
}
else if (isEligibleForEtag(request, responseWrapper, statusCode, body)) {
String responseETag = generateETagHeaderValue(body);
else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) {
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream());
rawResponse.setHeader(HEADER_ETAG, responseETag);
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
if (responseETag.equals(requestETag)) {
@@ -115,20 +112,14 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag +
"], sending normal response");
}
if (body.length > 0) {
rawResponse.setContentLength(body.length);
StreamUtils.copy(body, rawResponse.getOutputStream());
}
responseWrapper.copyBodyToResponse();
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Response with status code [" + statusCode + "] not eligible for ETag");
}
if (body.length > 0) {
rawResponse.setContentLength(body.length);
StreamUtils.copy(body, rawResponse.getOutputStream());
}
responseWrapper.copyBodyToResponse();
}
}
@@ -143,11 +134,11 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
* @param request the HTTP request
* @param response the HTTP response
* @param responseStatusCode the HTTP response status code
* @param responseBody the response body
* @param inputStream the response body
* @return {@code true} if eligible for ETag generation; {@code false} otherwise
*/
protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response,
int responseStatusCode, byte[] responseBody) {
int responseStatusCode, InputStream inputStream) {
if (responseStatusCode >= 200 && responseStatusCode < 300 &&
HttpMethod.GET.name().equals(request.getMethod())) {
@@ -162,13 +153,18 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
/**
* Generate the ETag header value from the given response body byte array.
* <p>The default implementation generates an MD5 hash.
* @param bytes the response body as byte array
* @param inputStream the response body as an InputStream
* @return the ETag header value
* @see org.springframework.util.DigestUtils
*/
protected String generateETagHeaderValue(byte[] bytes) {
protected String generateETagHeaderValue(InputStream inputStream) {
StringBuilder builder = new StringBuilder("\"0");
DigestUtils.appendMd5DigestAsHex(bytes, builder);
try {
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
}
catch (IOException e) {
throw new RuntimeException(e);
}
builder.append('"');
return builder.toString();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -17,6 +17,7 @@
package org.springframework.web.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
@@ -24,8 +25,7 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.util.ResizableByteArrayOutputStream;
import org.springframework.util.StreamUtils;
import org.springframework.util.FastByteArrayOutputStream;
/**
* {@link javax.servlet.http-HttpServletResponse} wrapper that caches all content written to
@@ -39,7 +39,7 @@ import org.springframework.util.StreamUtils;
*/
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
private final ResizableByteArrayOutputStream content = new ResizableByteArrayOutputStream(1024);
private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream();
@@ -107,9 +107,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public void setContentLength(int len) {
if (len > this.content.capacity()) {
this.content.resize(len);
}
this.content.resize(len);
}
// Overrides Servlet 3.1 setContentLengthLong(long) at runtime
@@ -118,16 +116,12 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" +
Integer.MAX_VALUE + "): " + len);
}
if (len > this.content.capacity()) {
this.content.resize((int) len);
}
this.content.resize((int) len);
}
@Override
public void setBufferSize(int size) {
if (size > this.content.capacity()) {
this.content.resize(size);
}
this.content.resize(size);
}
@Override
@@ -142,7 +136,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
}
/**
* Return the status code as specifed on the response.
* Return the status code as specified on the response.
*/
public int getStatusCode() {
return this.statusCode;
@@ -155,14 +149,24 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
return this.content.toByteArray();
}
private void copyBodyToResponse() throws IOException {
public void copyBodyToResponse() throws IOException {
if (this.content.size() > 0) {
getResponse().setContentLength(this.content.size());
StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream());
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if(! rawResponse.isCommitted()){
rawResponse.setContentLength(this.content.size());
}
this.content.writeTo(rawResponse.getOutputStream());
this.content.reset();
}
}
public int getContentSize(){
return this.content.size();
}
public InputStream getContentInputStream(){
return this.content.getInputStream();
}
private class ResponseServletOutputStream extends ServletOutputStream {