Java Enterpise Platform - Response Observer Filter
Motivation
Filters are a great way to observe (or even modify) the response generated for an HTTP request. This tutorial presents a reusable filter that makes use of the observer pattern to allow developers to monitor responses to HTTP requests as they are constructed.
The Code
The response observer filter is comprised of several classes and interfaces which collaborate to expose the events that transpire while constructing the response to an HTTP request. The interface below defines those events.
package com.bigohsoftware.common.filters.response;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public interface ResponseObserver
{
public void doBeforeResponseStarted(ServletRequest req, ServletResponse resp);
public void doAfterResponseFinished(ServletRequest req, ServletResponse resp);
public void doAfterCharsWritten(char[] charsWritten);
public void doAfterBytesWriten(byte[] bytesWritten);
}
Next a simple, abstract filter class serves as the base class for (potentially) many filters that observe the events defined in the ResponseObserver interface above.
package com.bigohsoftware.common.filters.response;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
/*
* This filter can be used to monitor attributes of an HTTPServletResponse.
*/
public abstract class ResponseObserverFilter implements Filter, ResponseObserver
{
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
{
doBeforeResponseStarted(req, resp);
ResponseObserverResponseWrapper wrappedResponse = new ResponseObserverResponseWrapper((HttpServletResponse) resp, this);
chain.doFilter(req, wrappedResponse);
doAfterResponseFinished(req, resp);
}
}
The abstract filter class makes use of a ResponseWrapper class. The ServletOutputStream and PrintWriter returned by that ResponseWrapper store a reference to the ResponseObserver filter.
package com.bigohsoftware.common.filters.response;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
public class ResponseObserverResponseWrapper extends HttpServletResponseWrapper
{
private final ResponseObserver observer;
private ResponseObserverPrintWriter writer;
private ResponseObserverOutputStream stream;
ResponseObserverResponseWrapper(HttpServletResponse response, ResponseObserver observer)
{
super(response);
this.observer = observer;
}
public ServletOutputStream getOutputStream() throws IOException
{
if (stream == null)
{
stream = new ResponseObserverOutputStream(super.getOutputStream(), observer);
}
return stream;
}
public PrintWriter getWriter() throws IOException
{
if (writer == null)
{
writer = new ResponseObserverPrintWriter(super.getWriter(), observer);
}
return writer;
}
}
Both the ServletOutputStream and PrintWriter classes utilized by the response observer filter call the event observer methods defined in the ResponseObserver interface.
package com.bigohsoftware.common.filters.response;
import java.io.IOException;
import javax.servlet.ServletOutputStream;
public class ResponseObserverOutputStream extends ServletOutputStream
{
private final ServletOutputStream servletOutputStream;
private final ResponseObserver observer;
public ResponseObserverOutputStream(ServletOutputStream servletOutputStream, ResponseObserver observer)
{
this.servletOutputStream = servletOutputStream;
this.observer = observer;
}
@Override
public void print(boolean b) throws IOException
{
servletOutputStream.print(b);
observer.doAfterBytesWriten(String.valueOf(b).getBytes());
}
@Override
public void print(char c) throws IOException
{
servletOutputStream.print(c);
observer.doAfterBytesWriten(String.valueOf(c).getBytes());
}
@Override
public void print(double d) throws IOException
{
servletOutputStream.print(d);
observer.doAfterBytesWriten(String.valueOf(d).getBytes());
}
@Override
public void print(float f) throws IOException
{
servletOutputStream.print(f);
observer.doAfterBytesWriten(String.valueOf(f).getBytes());
}
@Override
public void print(int i) throws IOException
{
servletOutputStream.print(i);
observer.doAfterBytesWriten(String.valueOf(i).getBytes());
}
@Override
public void print(long l) throws IOException
{
servletOutputStream.print(l);
observer.doAfterBytesWriten(String.valueOf(l).getBytes());
}
@Override
public void print(String s) throws IOException
{
servletOutputStream.print(s);
observer.doAfterBytesWriten(s.getBytes());
}
@Override
public void println() throws IOException
{
servletOutputStream.println();
observer.doAfterBytesWriten("\r\n".getBytes());
}
@Override
public void println(boolean b) throws IOException
{
print(b);
println();
}
@Override
public void println(char c) throws IOException
{
print(c);
println();
}
@Override
public void println(double d) throws IOException
{
print(d);
println();
}
@Override
public void println(float f) throws IOException
{
print(f);
println();
}
@Override
public void println(int i) throws IOException
{
print(i);
println();
}
@Override
public void println(long l) throws IOException
{
print(l);
println();
}
@Override
public void println(String s) throws IOException
{
print(s);
println();
}
@Override
public void write(byte[] b, int off, int len) throws IOException
{
servletOutputStream.write(b, off, len);
byte[] bytesWritten = new byte[len];
for (int i = 0; i < len; i++) {
bytesWritten[i] = b[off + i];
}
observer.doAfterBytesWriten(bytesWritten);
}
@Override
public void write(byte[] b) throws IOException
{
servletOutputStream.write(b);
observer.doAfterBytesWriten(b);
}
@Override
public void write(int b) throws IOException
{
servletOutputStream.write(b);
byte[] bytesWritten = new byte[1];
bytesWritten[0] = (byte) b;
observer.doAfterBytesWriten(bytesWritten);
}
@Override
public void close() throws IOException
{
servletOutputStream.close();
}
@Override
public void flush() throws IOException
{
servletOutputStream.flush();
}
}
package com.bigohsoftware.common.filters.response;
import java.io.PrintWriter;
public class ResponseObserverPrintWriter extends PrintWriter
{
private final ResponseObserver observer;
ResponseObserverPrintWriter(PrintWriter writer, ResponseObserver observer)
{
super(writer);
this.observer = observer;
}
public void write(int c)
{
super.write(c);
char[] charsWritten = new char[1];
charsWritten[0] = (char) c;
observer.doAfterCharsWritten(charsWritten);
}
public void write(char[] buf, int off, int len)
{
super.write(buf, off, len);
char[] charsWritten = new char[len];
for (int i = 0; i < len; i++) {
charsWritten[i] = buf[off + i];
}
observer.doAfterCharsWritten(charsWritten);
}
public void write(char[] buf)
{
super.write(buf);
observer.doAfterCharsWritten(buf);
}
public void write(String s, int off, int len)
{
super.write(s, off, len);
observer.doAfterCharsWritten(s.substring(off, off + len).toCharArray());
}
public void write(String s)
{
super.write(s);
observer.doAfterCharsWritten(s.toCharArray());
}
}
An Example: Response Size Monitor Filter
Because the abstract ResponseObserverFilter utilizes the observer pattern, we can use it to define many useful, concrete filters without having to write much code. For example, it is quite easy to create a filter that announces the size of the response generated for an HTTP request.
package com.bigohsoftware.common.filters.response;
import java.text.DecimalFormat;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class ResponseSizeMonitorFilter extends ResponseObserverFilter
{
private final ConcurrentHashMap<Thread, Long> threadToByteCountMap = new ConcurrentHashMap<Thread, Long>();
public void init(FilterConfig filterConfig) throws ServletException
{
}
public void doBeforeResponseStarted(ServletRequest req, ServletResponse resp)
{
threadToByteCountMap.put(Thread.currentThread(), new Long(0));
}
public void doAfterResponseFinished(ServletRequest req, ServletResponse resp)
{
long numBytesInResponse = threadToByteCountMap.get(Thread.currentThread());
System.out.println("The response for the requested url (" + getUrl(req) + ") contained about " + new DecimalFormat("###,##0.00").format(numBytesInResponse / 1024.0) + " kilobytes.");
// remove entry in threadToByteCountMap so that the Long value can be
// garbage collected
threadToByteCountMap.remove(Thread.currentThread());
}
private String getUrl(ServletRequest req)
{
if (req instanceof HttpServletRequest)
{
return ((HttpServletRequest) req).getRequestURL().toString();
}
else
{
return "unknown; non HTTP";
}
}
public void doAfterCharsWritten(char[] charsWritten)
{
synchronized (threadToByteCountMap)
{
Long currentByteCount = threadToByteCountMap.get(Thread.currentThread());
threadToByteCountMap.put(Thread.currentThread(), new Long(currentByteCount + (charsWritten.length * 2)));
}
}
public void doAfterBytesWriten(byte[] bytesWritten)
{
synchronized (threadToByteCountMap)
{
Long currentByteCount = threadToByteCountMap.get(Thread.currentThread());
threadToByteCountMap.put(Thread.currentThread(), new Long(currentByteCount + bytesWritten.length));
}
}
public void destroy()
{
}
}
The ResponseSizeMonitorFilter detailed above logs the size of HTTP responses in the following format:
The response for the requested url (http://127.0.0.1:8080/CommonWeb/ImageServlet) contained about 4.09 kilobytes.