Servlet的Filter实现页面缓存
java有多个开源的缓存系统都支持页面缓存的,如OScache、Ehcache。
这个例子就是从Ehcache里挖出来的,并做了些改造和简化,但原理在此例子中都是完全体现出来了。该例子只供大家学习用,企业应用还是需要做一些修改的。因为页面数据只是直接存放到HashMap里。
CacheFilter.java
页面数据就是存放到HashMap里,key是url。
public class CacheFilter implements Filter {public static final String HEADER_LAST_MODIFIED = "Last-Modified";public static final String HEADER_CONTENT_TYPE = "Content-Type";public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";public static final String HEADER_EXPIRES = "Expires";public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";public static final String HEADER_CACHE_CONTROL = "Cache-Control";public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";private static final String REQUEST_FILTERED = "cache_filter_" + CacheFilter.class.getName();private final Map<String, ResponseContent> cache = new HashMap<String, ResponseContent>();// Last Modified parameterprivate static final long LAST_MODIFIED_INITIAL = -1;// Expires parameterprivate static final long EXPIRES_ON = 1;private int time = 60 * 60;private long lastModified = LAST_MODIFIED_INITIAL;private long expires = EXPIRES_ON;private long cacheControlMaxAge = -60;@Overridepublic void destroy() {}@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,ServletException {HttpServletRequest request = (HttpServletRequest) req;//避免重复调用if (isFilteredBefore(request)) {chain.doFilter(request, res);return;}request.setAttribute(REQUEST_FILTERED, Boolean.TRUE);String key = getCacheKey(request);ResponseContent responseContent = cache.get(key);if (responseContent != null) {//如果当前的URL已经有对应的响应内容responseContent.writeTo(res);return;}//用CacheHttpServletResponseWrapper来代替HttpServletResponse,用于记录HttpServletResponse输出的内容。CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse) res,time * 1000L, lastModified, expires, cacheControlMaxAge);chain.doFilter(request, cacheResponse);cacheResponse.flushBuffer();// Store as the cache content the result of the responsecache.put(key, cacheResponse.getContent());}private String getCacheKey(HttpServletRequest request) {StringBuilder builder = new StringBuilder(request.getRequestURI());if (StringUtils.isNotEmpty(request.getQueryString())) {builder.append("_").append(request.getQueryString());}return builder.toString();}/** * Checks if the request was filtered before, so guarantees to be executed * once per request. You can override this methods to define a more specific * behaviour. * * @param request checks if the request was filtered before. * @return true if it is the first execution */public boolean isFilteredBefore(ServletRequest request) {return request.getAttribute(REQUEST_FILTERED) != null;}@Overridepublic void init(FilterConfig arg0) throws ServletException {// TODO Auto-generated method stub}}
import java.io.IOException;import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.util.Locale;import javax.servlet.ServletOutputStream;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpServletResponseWrapper;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;/** * 缓存的HttpServletResponseWrapper,它会把{@link HttpServletResponse}的部分数据记录到{@link ResponseContent}里。 */public class CacheHttpServletResponseWrapper extends HttpServletResponseWrapper {private final Log log = LogFactory.getLog(this.getClass());private PrintWriter cachedWriter = null;private ResponseContent result = null;private SplitServletOutputStream cacheOut = null;private int status = SC_OK;private long cacheControl = -60;public CacheHttpServletResponseWrapper(HttpServletResponse response) {this(response, Long.MAX_VALUE, CacheFilter.EXPIRES_ON, CacheFilter.LAST_MODIFIED_INITIAL, -60);}public CacheHttpServletResponseWrapper(HttpServletResponse response, long time, long lastModified, long expires,long cacheControl) {super(response);this.result = new ResponseContent();this.cacheControl = cacheControl;// setting a default last modified value based on object creation and// remove the millisif (lastModified == CacheFilter.LAST_MODIFIED_INITIAL) {long current = System.currentTimeMillis();current = current - (current % 1000);result.setLastModified(current);super.setDateHeader(CacheFilter.HEADER_LAST_MODIFIED, result.getLastModified());}// setting the expires valueif (expires == CacheFilter.EXPIRES_TIME) {result.setExpires(result.getLastModified() + time);super.setDateHeader(CacheFilter.HEADER_EXPIRES, result.getExpires());}// setting the cache control with max-ageif (this.cacheControl == CacheFilter.MAX_AGE_TIME) {// set the count downlong maxAge = System.currentTimeMillis();maxAge = maxAge - (maxAge % 1000) + time;result.setMaxAge(maxAge);super.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + time / 1000);} else if (this.cacheControl != CacheFilter.MAX_AGE_NO_INIT) {result.setMaxAge(this.cacheControl);super.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + (-this.cacheControl));} else if (this.cacheControl == CacheFilter.MAX_AGE_NO_INIT) {result.setMaxAge(this.cacheControl);}}/** * Get a response content * * @return The content */public ResponseContent getContent() {// Flush the buffertry {flush();} catch (IOException ignore) {}// Create the byte arrayresult.commit();// Return the result from this responsereturn result;}/** * Set the content type * * @param value The content type */public void setContentType(String value) {if (log.isDebugEnabled()) {log.debug("ContentType: " + value);}super.setContentType(value);result.setContentType(value);}/** * Set a header field * * @param name The header name * @param value The header value */public void setHeader(String name, String value) {if (log.isDebugEnabled()) {log.debug("header: " + name + ": " + value);}if (CacheFilter.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {result.setContentType(value);}if (CacheFilter.HEADER_CONTENT_ENCODING.equalsIgnoreCase(name)) {result.setContentEncoding(value);}super.setHeader(name, value);}/** * Add a header field * * @param name The header name * @param value The header value */public void addHeader(String name, String value) {if (log.isDebugEnabled()) {log.debug("header: " + name + ": " + value);}if (CacheFilter.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {result.setContentType(value);}if (CacheFilter.HEADER_CONTENT_ENCODING.equalsIgnoreCase(name)) {result.setContentEncoding(value);}super.addHeader(name, value);}/** * We override this so we can catch the response status. Only responses with * a status of 200 (<code>SC_OK</code>) will be cached. */public void setStatus(int status) {super.setStatus(status);this.status = status;}/** * We override this so we can catch the response status. Only responses with * a status of 200 (<code>SC_OK</code>) will be cached. */public void sendError(int status, String string) throws IOException {super.sendError(status, string);this.status = status;}/** * We override this so we can catch the response status. Only responses with * a status of 200 (<code>SC_OK</code>) will be cached. */public void sendError(int status) throws IOException {super.sendError(status);this.status = status;}/** * We override this so we can catch the response status. Only responses with * a status of 200 (<code>SC_OK</code>) will be cached. */public void setStatus(int status, String string) {super.setStatus(status, string);this.status = status;}/** * We override this so we can catch the response status. Only responses with * a status of 200 (<code>SC_OK</code>) will be cached. */public void sendRedirect(String location) throws IOException {this.status = SC_MOVED_TEMPORARILY;super.sendRedirect(location);}/** * Retrieves the captured HttpResponse status. */public int getStatus() {return status;}/** * Set the locale * * @param value The locale */public void setLocale(Locale value) {super.setLocale(value);result.setLocale(value);}/** * Get an output stream * * @throws IOException */public ServletOutputStream getOutputStream() throws IOException {// Pass this faked servlet output stream that captures what is sentif (cacheOut == null) {cacheOut = new SplitServletOutputStream(result.getOutputStream(), super.getOutputStream());}return cacheOut;}/** * Get a print writer * * @throws IOException */public PrintWriter getWriter() throws IOException {if (cachedWriter == null) {String encoding = getCharacterEncoding();if (encoding != null) {cachedWriter = new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding));} else { // using the default character encodingcachedWriter = new PrintWriter(new OutputStreamWriter(getOutputStream()));}}return cachedWriter;}/** * Flushes all streams. * * @throws IOException */private void flush() throws IOException {if (cacheOut != null) {cacheOut.flush();}if (cachedWriter != null) {cachedWriter.flush();}}public void flushBuffer() throws IOException {super.flushBuffer();flush();}}
import java.io.BufferedOutputStream;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.OutputStream;import java.io.Serializable;import java.util.Locale;import java.util.zip.GZIPInputStream;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletResponse;/** * Holds the servlet response in a byte array so that it can be held in the * cache (and, since this class is serializable, optionally persisted to disk). * * @version $Revision: 362 $ * @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a> */public class ResponseContent implements Serializable {private static final long serialVersionUID = 1L;private transient ByteArrayOutputStream bout = new ByteArrayOutputStream(1000);private Locale locale = null;private String contentEncoding = null;private String contentType = null;private byte[] content = null;private long expires = Long.MAX_VALUE;private long lastModified = -1;private long maxAge = -60;public String getContentType() {return contentType;}/** * Set the content type. We capture this so that when we serve this data * from cache, we can set the correct content type on the response. */public void setContentType(String value) {contentType = value;}public long getLastModified() {return lastModified;}public void setLastModified(long value) {lastModified = value;}public String getContentEncoding() {return contentEncoding;}public void setContentEncoding(String contentEncoding) {this.contentEncoding = contentEncoding;}/** * Set the Locale. We capture this so that when we serve this data from * cache, we can set the correct locale on the response. */public void setLocale(Locale value) {locale = value;}/** * @return the expires date and time in miliseconds when the content will be * stale */public long getExpires() {return expires;}/** * Sets the expires date and time in miliseconds. * * @param value time in miliseconds when the content will expire */public void setExpires(long value) {expires = value;}/** * Returns the max age of the content in miliseconds. If expires header and * cache control are enabled both, both will be equal. * * @return the max age of the content in miliseconds, if -1 max-age is * disabled */public long getMaxAge() {return maxAge;}/** * Sets the max age date and time in miliseconds. If the parameter is -1, * the max-age parameter won't be set by default in the Cache-Control * header. * * @param value sets the intial */public void setMaxAge(long value) {maxAge = value;}/** * Get an output stream. This is used by the * {@link SplitServletOutputStream} to capture the original (uncached) * response into a byte array. * * @return the original (uncached) response, returns null if response is * already committed. */public OutputStream getOutputStream() {return bout;}/** * Gets the size of this cached content. * * @return The size of the content, in bytes. If no content exists, this * method returns <code>-1</code>. */public int getSize() {return (content != null) ? content.length : (-1);}/** * Called once the response has been written in its entirety. This method * commits the response output stream by converting the output stream into a * byte array. */public void commit() {if (bout != null) {content = bout.toByteArray();bout = null;}}/** * Writes this cached data out to the supplied <code>ServletResponse</code>. * * @param response The servlet response to output the cached content to. * @throws IOException */public void writeTo(ServletResponse response) throws IOException {writeTo(response, false, false);}/** * Writes this cached data out to the supplied <code>ServletResponse</code>. * * @param response The servlet response to output the cached content to. * @param fragment is true if this content a fragment or part of a page * @param acceptsGZip is true if client browser supports gzip compression * @throws IOException */public void writeTo(ServletResponse response, boolean fragment, boolean acceptsGZip) throws IOException {// Send the content type and data to this responseif (contentType != null) {response.setContentType(contentType);}if (fragment) {// Don't support gzip compression if the content is a fragment of a// pageacceptsGZip = false;} else {// add special headers for a complete pageif (response instanceof HttpServletResponse) {HttpServletResponse httpResponse = (HttpServletResponse) response;// add the last modified headerif (lastModified != -1) {httpResponse.setDateHeader(CacheFilter.HEADER_LAST_MODIFIED, lastModified);}// add the expires headerif (expires != Long.MAX_VALUE) {httpResponse.setDateHeader(CacheFilter.HEADER_EXPIRES, expires);}// add the cache-control header for max-ageif (maxAge == CacheFilter.MAX_AGE_NO_INIT || maxAge == CacheFilter.MAX_AGE_TIME) {// do nothing} else if (maxAge > 0) { // set max-age based on life timelong currentMaxAge = maxAge / 1000 - System.currentTimeMillis() / 1000;if (currentMaxAge < 0) {currentMaxAge = 0;}httpResponse.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + currentMaxAge);} else {httpResponse.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + (-maxAge));}}}if (locale != null) {response.setLocale(locale);}OutputStream out = new BufferedOutputStream(response.getOutputStream());if (isContentGZiped()) {if (acceptsGZip) {((HttpServletResponse) response).addHeader(CacheFilter.HEADER_CONTENT_ENCODING, "gzip");response.setContentLength(content.length);out.write(content);} else {// client doesn't support, so we have to uncompress itByteArrayInputStream bais = new ByteArrayInputStream(content);GZIPInputStream zis = new GZIPInputStream(bais);ByteArrayOutputStream baos = new ByteArrayOutputStream();int numBytesRead = 0;byte[] tempBytes = new byte[4196];while ((numBytesRead = zis.read(tempBytes, 0, tempBytes.length)) != -1) {baos.write(tempBytes, 0, numBytesRead);}byte[] result = baos.toByteArray();response.setContentLength(result.length);out.write(result);}} else {// the content isn't compressed// regardless if the client browser supports gzip we will just// return the contentresponse.setContentLength(content.length);out.write(content);}out.flush();}/** * @return true if the content is GZIP compressed */public boolean isContentGZiped() {return "gzip".equals(contentEncoding);}
package com.dukuai.metis.search.servlet;import java.io.IOException;import java.io.OutputStream;import javax.servlet.ServletOutputStream;/** * Extends the base <code>ServletOutputStream</code> class so that the stream * can be captured as it gets written. This is achieved by overriding the * <code>write()</code> methods and outputting the data to two streams - the * original stream and a secondary stream that is designed to capture the * written data. * * @version $Revision: 393 $ * @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a> */public class SplitServletOutputStream extends ServletOutputStream {OutputStream captureStream = null;OutputStream passThroughStream = null;/** * Constructs a split output stream that both captures and passes through * the servlet response. * * @param captureStream The stream that will be used to capture the data. * @param passThroughStream The pass-through * <code>ServletOutputStream</code> that will write the * response to the client as originally intended. */public SplitServletOutputStream(OutputStream captureStream, OutputStream passThroughStream) {this.captureStream = captureStream;this.passThroughStream = passThroughStream;}/** * Writes the incoming data to both the output streams. * * @param value The int data to write. * @throws IOException */public void write(int value) throws IOException {captureStream.write(value);passThroughStream.write(value);}/** * Writes the incoming data to both the output streams. * * @param value The bytes to write to the streams. * @throws IOException */public void write(byte[] value) throws IOException {captureStream.write(value);passThroughStream.write(value);}/** * Writes the incoming data to both the output streams. * * @param b The bytes to write out to the streams. * @param off The offset into the byte data where writing should begin. * @param len The number of bytes to write. * @throws IOException */public void write(byte[] b, int off, int len) throws IOException {captureStream.write(b, off, len);passThroughStream.write(b, off, len);}/** * Flushes both the output streams. * * @throws IOException */public void flush() throws IOException {super.flush();captureStream.flush(); // why not?passThroughStream.flush();}/** * Closes both the output streams. * * @throws IOException */public void close() throws IOException {super.close();captureStream.close();passThroughStream.close();}}