Extracted ContentCachingRequestWrapper and ContentCachingResponseWrapper

Issue: SPR-12477
This commit is contained in:
Juergen Hoeller 2014-11-27 18:01:19 +01:00
parent 3b1584904f
commit decc5cd1ae
4 changed files with 362 additions and 272 deletions

View File

@ -16,34 +16,32 @@
package org.springframework.web.filter;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* Base class for {@code Filter}s that perform logging operations before and after a request is processed.
* Base class for {@code Filter}s that perform logging operations before and after a request
* is processed.
*
* <p>Subclasses should override the {@code beforeRequest(HttpServletRequest, String)} and
* {@code afterRequest(HttpServletRequest, String)} methods to perform the actual logging around the request.
* {@code afterRequest(HttpServletRequest, String)} methods to perform the actual logging
* around the request.
*
* <p>Subclasses are passed the message to write to the log in the {@code beforeRequest} and
* {@code afterRequest} methods. By default, only the URI of the request is logged. However, setting the
* {@code includeQueryString} property to {@code true} will cause the query string of the request to be
* included also. The payload (body) of the request can be logged via the {@code includePayload} flag. Note that
* this will only log that which is read, which might not be the entire payload.
* {@code afterRequest} methods. By default, only the URI of the request is logged. However,
* setting the {@code includeQueryString} property to {@code true} will cause the query string
* of the request to be included also. The payload (body) of the request can be logged via the
* {@code includePayload} flag. Note that this will only log that which is read, which might
* not be the entire payload.
*
* <p>Prefixes and suffixes for the before and after messages can be configured using the
* {@code beforeMessagePrefix}, {@code afterMessagePrefix}, {@code beforeMessageSuffix} and
@ -87,41 +85,43 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
/**
* Set whether or not the query string should be included in the log message. <p>Should be configured using an
* {@code &lt;init-param&gt;} for parameter name "includeQueryString" in the filter definition in
* {@code web.xml}.
* Set whether the query string should be included in the log message.
* <p>Should be configured using an {@code &lt;init-param&gt;} for parameter name
* "includeQueryString" in the filter definition in {@code web.xml}.
*/
public void setIncludeQueryString(boolean includeQueryString) {
this.includeQueryString = includeQueryString;
}
/**
* Return whether or not the query string should be included in the log message.
* Return whether the query string should be included in the log message.
*/
protected boolean isIncludeQueryString() {
return this.includeQueryString;
}
/**
* Set whether or not the client address and session id should be included in the log message. <p>Should be configured
* using an {@code &lt;init-param&gt;} for parameter name "includeClientInfo" in the filter definition in
* {@code web.xml}.
* Set whether the client address and session id should be included in the
* log message.
* <p>Should be configured using an {@code &lt;init-param&gt;} for parameter name
* "includeClientInfo" in the filter definition in {@code web.xml}.
*/
public void setIncludeClientInfo(boolean includeClientInfo) {
this.includeClientInfo = includeClientInfo;
}
/**
* Return whether or not the client address and session id should be included in the log message.
* Return whether the client address and session id should be included in the
* log message.
*/
protected boolean isIncludeClientInfo() {
return this.includeClientInfo;
}
/**
* Set whether or not the request payload (body) should be included in the log message. <p>Should be configured using
* an {@code &lt;init-param&gt;} for parameter name "includePayload" in the filter definition in
* {@code web.xml}.
* Set whether the request payload (body) should be included in the log message.
* <p>Should be configured using an {@code &lt;init-param&gt;} for parameter name
* "includePayload" in the filter definition in {@code web.xml}.
*/
public void setIncludePayload(boolean includePayload) {
@ -129,14 +129,15 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
}
/**
* Return whether or not the request payload (body) should be included in the log message.
* Return whether the request payload (body) should be included in the log message.
*/
protected boolean isIncludePayload() {
return includePayload;
return this.includePayload;
}
/**
* Sets the maximum length of the payload body to be included in the log message. Default is 50 characters.
* Sets the maximum length of the payload body to be included in the log message.
* Default is 50 characters.
*/
public void setMaxPayloadLength(int maxPayloadLength) {
Assert.isTrue(maxPayloadLength >= 0, "'maxPayloadLength' should be larger than or equal to 0");
@ -147,32 +148,36 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
* Return the maximum length of the payload body to be included in the log message.
*/
protected int getMaxPayloadLength() {
return maxPayloadLength;
return this.maxPayloadLength;
}
/**
* Set the value that should be prepended to the log message written <i>before</i> a request is processed.
* Set the value that should be prepended to the log message written
* <i>before</i> a request is processed.
*/
public void setBeforeMessagePrefix(String beforeMessagePrefix) {
this.beforeMessagePrefix = beforeMessagePrefix;
}
/**
* Set the value that should be apppended to the log message written <i>before</i> a request is processed.
* Set the value that should be appended to the log message written
* <i>before</i> a request is processed.
*/
public void setBeforeMessageSuffix(String beforeMessageSuffix) {
this.beforeMessageSuffix = beforeMessageSuffix;
}
/**
* Set the value that should be prepended to the log message written <i>after</i> a request is processed.
* Set the value that should be prepended to the log message written
* <i>after</i> a request is processed.
*/
public void setAfterMessagePrefix(String afterMessagePrefix) {
this.afterMessagePrefix = afterMessagePrefix;
}
/**
* Set the value that should be appended to the log message written <i>after</i> a request is processed.
* Set the value that should be appended to the log message written
* <i>after</i> a request is processed.
*/
public void setAfterMessageSuffix(String afterMessageSuffix) {
this.afterMessageSuffix = afterMessageSuffix;
@ -200,22 +205,21 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
if (isIncludePayload()) {
if (isFirstRequest) {
request = new RequestCachingRequestWrapper(request);
}
if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request);
}
if (isFirstRequest) {
beforeRequest(request, getBeforeMessage(request));
beforeRequest(requestToUse, getBeforeMessage(requestToUse));
}
try {
filterChain.doFilter(request, response);
filterChain.doFilter(requestToUse, response);
}
finally {
if (!isAsyncStarted(request)) {
afterRequest(request, getAfterMessage(request));
if (!isAsyncStarted(requestToUse)) {
afterRequest(requestToUse, getAfterMessage(requestToUse));
}
}
}
@ -265,8 +269,8 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
msg.append(";user=").append(user);
}
}
if (isIncludePayload() && request instanceof RequestCachingRequestWrapper) {
RequestCachingRequestWrapper wrapper = (RequestCachingRequestWrapper) request;
if (isIncludePayload() && request instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, getMaxPayloadLength());
@ -301,63 +305,4 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
*/
protected abstract void afterRequest(HttpServletRequest request, String message);
private static class RequestCachingRequestWrapper extends HttpServletRequestWrapper {
private final ByteArrayOutputStream cachedContent = new ByteArrayOutputStream(1024);
private final ServletInputStream inputStream;
private BufferedReader reader;
private RequestCachingRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.inputStream = new RequestCachingInputStream(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return this.inputStream;
}
@Override
public String getCharacterEncoding() {
String enc = super.getCharacterEncoding();
return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
}
@Override
public BufferedReader getReader() throws IOException {
if (this.reader == null) {
this.reader = new BufferedReader(new InputStreamReader(this.inputStream, getCharacterEncoding()));
}
return this.reader;
}
private byte[] getContentAsByteArray() {
return this.cachedContent.toByteArray();
}
private class RequestCachingInputStream extends ServletInputStream {
private final ServletInputStream is;
public RequestCachingInputStream(ServletInputStream is) {
this.is = is;
}
@Override
public int read() throws IOException {
int ch = this.is.read();
if (ch != -1) {
cachedContent.write(ch);
}
return ch;
}
}
}
}

View File

@ -17,22 +17,17 @@
package org.springframework.web.filter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.DigestUtils;
import org.springframework.util.ResizableByteArrayOutputStream;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
/**
@ -80,8 +75,8 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
throws ServletException, IOException {
HttpServletResponse responseToUse = response;
if (!isAsyncDispatch(request)) {
responseToUse = new ShallowEtagResponseWrapper(response);
if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
responseToUse = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(request, responseToUse);
@ -92,13 +87,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
}
private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
ShallowEtagResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ShallowEtagResponseWrapper.class);
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Assert.notNull(responseWrapper, "ShallowEtagResponseWrapper not found");
HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse();
int statusCode = responseWrapper.getStatusCode();
byte[] body = responseWrapper.toByteArray();
byte[] body = responseWrapper.getContentAsByteArray();
if (rawResponse.isCommitted()) {
if (body.length > 0) {
@ -178,165 +173,4 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
return builder.toString();
}
/**
* {@link HttpServletRequest} wrapper that buffers all content written to the
* {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer},
* and allows this content to be retrieved via a {@link #toByteArray() byte array}.
*/
private static class ShallowEtagResponseWrapper extends HttpServletResponseWrapper {
private final ResizableByteArrayOutputStream content = new ResizableByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream();
private PrintWriter writer;
private int statusCode = HttpServletResponse.SC_OK;
public ShallowEtagResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void setStatus(int sc) {
super.setStatus(sc);
this.statusCode = sc;
}
@SuppressWarnings("deprecation")
@Override
public void setStatus(int sc, String sm) {
super.setStatus(sc, sm);
this.statusCode = sc;
}
@Override
public void sendError(int sc) throws IOException {
copyBodyToResponse();
super.sendError(sc);
this.statusCode = sc;
}
@Override
public void sendError(int sc, String msg) throws IOException {
copyBodyToResponse();
super.sendError(sc, msg);
this.statusCode = sc;
}
@Override
public void sendRedirect(String location) throws IOException {
copyBodyToResponse();
super.sendRedirect(location);
}
@Override
public ServletOutputStream getOutputStream() {
return this.outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (this.writer == null) {
String characterEncoding = getCharacterEncoding();
this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) :
new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING));
}
return this.writer;
}
@Override
public void setContentLength(int len) {
if (len > this.content.capacity()) {
this.content.resize(len);
}
}
// Overrides Servlet 3.1 setContentLengthLong(long) at runtime
public void setContentLengthLong(long len) {
if (len > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" +
Integer.MAX_VALUE + "): " + len);
}
if (len > this.content.capacity()) {
this.content.resize((int) len);
}
}
@Override
public void setBufferSize(int size) {
if (size > this.content.capacity()) {
this.content.resize(size);
}
}
@Override
public void resetBuffer() {
this.content.reset();
}
@Override
public void reset() {
super.reset();
this.content.reset();
}
public int getStatusCode() {
return this.statusCode;
}
public byte[] toByteArray() {
return this.content.toByteArray();
}
private void copyBodyToResponse() throws IOException {
if (this.content.size() > 0) {
getResponse().setContentLength(this.content.size());
StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream());
this.content.reset();
}
}
private class ResponseServletOutputStream extends ServletOutputStream {
@Override
public void write(int b) throws IOException {
content.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
content.write(b, off, len);
}
}
private class ResponsePrintWriter extends PrintWriter {
public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException {
super(new OutputStreamWriter(content, characterEncoding));
}
@Override
public void write(char buf[], int off, int len) {
super.write(buf, off, len);
super.flush();
}
@Override
public void write(String s, int off, int len) {
super.write(s, off, len);
super.flush();
}
@Override
public void write(int c) {
super.write(c);
super.flush();
}
}
}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright 2002-2014 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.web.util;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* {@link javax.servlet.http-HttpServletRequest} wrapper that caches all content read from
* the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader},
* and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
*
* <p>Used e.g. by {@link org.springframework.web.filter.AbstractRequestLoggingFilter}.
*
* @author Juergen Hoeller
* @since 4.1.3
*/
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private final ByteArrayOutputStream cachedContent;
private ServletInputStream inputStream;
private BufferedReader reader;
/**
* Create a new ContentCachingRequestWrapper for the given servlet request.
* @param request the original servlet request
*/
public ContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
int contentLength = request.getContentLength();
this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.inputStream == null) {
this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
}
return this.inputStream;
}
@Override
public String getCharacterEncoding() {
String enc = super.getCharacterEncoding();
return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
}
@Override
public BufferedReader getReader() throws IOException {
if (this.reader == null) {
this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
return this.reader;
}
/**
* Return the cached request content as a byte array.
*/
public byte[] getContentAsByteArray() {
return this.cachedContent.toByteArray();
}
private class ContentCachingInputStream extends ServletInputStream {
private final ServletInputStream is;
public ContentCachingInputStream(ServletInputStream is) {
this.is = is;
}
@Override
public int read() throws IOException {
int ch = this.is.read();
if (ch != -1) {
cachedContent.write(ch);
}
return ch;
}
}
}

View File

@ -0,0 +1,206 @@
/*
* Copyright 2002-2014 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.web.util;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.util.ResizableByteArrayOutputStream;
import org.springframework.util.StreamUtils;
/**
* {@link javax.servlet.http-HttpServletResponse} wrapper that caches all content written to
* the {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer},
* and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
*
* <p>Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}.
*
* @author Juergen Hoeller
* @since 4.1.3
*/
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
private final ResizableByteArrayOutputStream content = new ResizableByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream();
private PrintWriter writer;
private int statusCode = HttpServletResponse.SC_OK;
/**
* Create a new ContentCachingResponseWrapper for the given servlet response.
* @param response the original servlet response
*/
public ContentCachingResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void setStatus(int sc) {
super.setStatus(sc);
this.statusCode = sc;
}
@SuppressWarnings("deprecation")
@Override
public void setStatus(int sc, String sm) {
super.setStatus(sc, sm);
this.statusCode = sc;
}
@Override
public void sendError(int sc) throws IOException {
copyBodyToResponse();
super.sendError(sc);
this.statusCode = sc;
}
@Override
public void sendError(int sc, String msg) throws IOException {
copyBodyToResponse();
super.sendError(sc, msg);
this.statusCode = sc;
}
@Override
public void sendRedirect(String location) throws IOException {
copyBodyToResponse();
super.sendRedirect(location);
}
@Override
public ServletOutputStream getOutputStream() {
return this.outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (this.writer == null) {
String characterEncoding = getCharacterEncoding();
this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) :
new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING));
}
return this.writer;
}
@Override
public void setContentLength(int len) {
if (len > this.content.capacity()) {
this.content.resize(len);
}
}
// Overrides Servlet 3.1 setContentLengthLong(long) at runtime
public void setContentLengthLong(long len) {
if (len > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" +
Integer.MAX_VALUE + "): " + len);
}
if (len > this.content.capacity()) {
this.content.resize((int) len);
}
}
@Override
public void setBufferSize(int size) {
if (size > this.content.capacity()) {
this.content.resize(size);
}
}
@Override
public void resetBuffer() {
this.content.reset();
}
@Override
public void reset() {
super.reset();
this.content.reset();
}
/**
* Return the status code as specifed on the response.
*/
public int getStatusCode() {
return this.statusCode;
}
/**
* Return the cached response content as a byte array.
*/
public byte[] getContentAsByteArray() {
return this.content.toByteArray();
}
private void copyBodyToResponse() throws IOException {
if (this.content.size() > 0) {
getResponse().setContentLength(this.content.size());
StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream());
this.content.reset();
}
}
private class ResponseServletOutputStream extends ServletOutputStream {
@Override
public void write(int b) throws IOException {
content.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
content.write(b, off, len);
}
}
private class ResponsePrintWriter extends PrintWriter {
public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException {
super(new OutputStreamWriter(content, characterEncoding));
}
@Override
public void write(char buf[], int off, int len) {
super.write(buf, off, len);
super.flush();
}
@Override
public void write(String s, int off, int len) {
super.write(s, off, len);
super.flush();
}
@Override
public void write(int c) {
super.write(c);
super.flush();
}
}
}