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:
committed by
Brian Clozel
parent
40cd1be14c
commit
213a3fd779
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user