You need to create a Filter
wherein you wrap the ServletResponse
argument with a custom HttpServletResponseWrapper
implementation wherein you override the getOutputStream()
and getWriter()
to return a custom ServletOutputStream
implementation wherein you copy the written byte(s) in the base abstract OutputStream#write(int b)
method. Then, you pass the wrapped custom HttpServletResponseWrapper
to the FilterChain#doFilter()
call instead and finally you should be able to get the copied response after the the call.
In other words, the Filter
:
@WebFilter("/*")
public class ResponseLogger implements Filter {
@Override
public void init(FilterConfig config) throws ServletException {
// NOOP.
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (response.getCharacterEncoding() == null) {
response.setCharacterEncoding("UTF-8"); // Or whatever default. UTF-8 is good for World Domination.
}
HttpServletResponseCopier responseCopier = new HttpServletResponseCopier((HttpServletResponse) response);
try {
chain.doFilter(request, responseCopier);
responseCopier.flushBuffer();
} finally {
byte[] copy = responseCopier.getCopy();
System.out.println(new String(copy, response.getCharacterEncoding())); // Do your logging job here. This is just a basic example.
}
}
@Override
public void destroy() {
// NOOP.
}
}
The custom HttpServletResponseWrapper
:
public class HttpServletResponseCopier extends HttpServletResponseWrapper {
private ServletOutputStream outputStream;
private PrintWriter writer;
private ServletOutputStreamCopier copier;
public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called on this response.");
}
if (outputStream == null) {
outputStream = getResponse().getOutputStream();
copier = new ServletOutputStreamCopier(outputStream);
}
return copier;
}
@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException("getOutputStream() has already been called on this response.");
}
if (writer == null) {
copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
} else if (outputStream != null) {
copier.flush();
}
}
public byte[] getCopy() {
if (copier != null) {
return copier.getCopy();
} else {
return new byte[0];
}
}
}
The custom ServletOutputStream
:
public class ServletOutputStreamCopier extends ServletOutputStream {
private OutputStream outputStream;
private ByteArrayOutputStream copy;
public ServletOutputStreamCopier(OutputStream outputStream) {
this.outputStream = outputStream;
this.copy = new ByteArrayOutputStream(1024);
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
copy.write(b);
}
public byte[] getCopy() {
return copy.toByteArray();
}
}
BalusC solution is ok, but little outdated. Spring now has feature for it . All you need to do is use [ContentCachingResponseWrapper]
, which has method public byte[] getContentAsByteArray()
.
I Suggest to make WrapperFactory which will allow to make it configurable, whether to use default ResponseWrapper or ContentCachingResponseWrapper.
Instead of creating Custom HttpServletResponseWrapper.You can use ContentCachingResponseWrapper as it provide method getContentAsByteArray().
public void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = servletRequest;
HttpServletResponse response = servletResponse;
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper =new ContentCachingResponseWrapper(response);
try {
super.doFilterInternal(requestWrapper, responseWrapper, filterChain);
} finally {
byte[] responseArray=responseWrapper.getContentAsByteArray();
String responseStr=new String(responseArray,responseWrapper.getCharacterEncoding());
System.out.println("string"+responseStr);
/*It is important to copy cached reponse body back to response stream
to see response */
responseWrapper.copyBodyToResponse();
}
}
While BalusC's answer will work in most scenarios you have to be careful with the flush
call - it commits response and no other writing to it is possible, eg. via following filters.
We have found some problems with very simmilar approach in Websphere environment where the delivered response was only partial.
According to this question the flush should not be called at all and you should let it be called internally.
I have solved the flush problem by using TeeWriter
(it splits stream into 2 streams) and using non-buffering streams in the "branched stream" for logging purpose. It is unneccessary to call the flush
then.
private HttpServletResponse wrapResponseForLogging(HttpServletResponse response, final Writer branchedWriter) {
return new HttpServletResponseWrapper(response) {
PrintWriter writer;
@Override
public synchronized PrintWriter getWriter() throws IOException {
if (writer == null) {
writer = new PrintWriter(new TeeWriter(super.getWriter(), branchedWriter));
}
return writer;
}
};
}
Then you can use it this way:
protected void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
//...
StringBuilderWriter branchedWriter = new org.apache.commons.io.output.StringBuilderWriter();
try {
chain.doFilter(request, wrapResponseForLogging(response, branchedWriter));
} finally {
log.trace("Response: " + branchedWriter);
}
}
The code is simplified for brewity.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With