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 parameter
	private static final long LAST_MODIFIED_INITIAL = -1;

	// Expires parameter
	private 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;

	@Override
	public void destroy() {

	}

	@Override
	public 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 response
		cache.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;
	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		// TODO Auto-generated method stub
	}
}



HttpServletResponseWrapper.java
用ResponseContent记录HttpServletResponse输出的信息
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 millis
		if (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 value
		if (expires == CacheFilter.EXPIRES_TIME) {
			result.setExpires(result.getLastModified() + time);
			super.setDateHeader(CacheFilter.HEADER_EXPIRES, result.getExpires());
		}
		// setting the cache control with max-age
		if (this.cacheControl == CacheFilter.MAX_AGE_TIME) {
			// set the count down
			long 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 buffer
		try {
			flush();
		} catch (IOException ignore) {
		}
		// Create the byte array
		result.commit();
		// Return the result from this response
		return 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 sent
		if (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 encoding
				cachedWriter = 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();
	}
}





ResponseContent.java
响应后的内容,就是需要cache的对象,包含页面内容和Content-Type、Last-Modified、Content-Encoding等一些响应头的信息。
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:[email protected]">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 response
		if (contentType != null) {
			response.setContentType(contentType);
		}

		if (fragment) {
			// Don't support gzip compression if the content is a fragment of a
			// page
			acceptsGZip = false;
		} else {
			// add special headers for a complete page
			if (response instanceof HttpServletResponse) {
				HttpServletResponse httpResponse = (HttpServletResponse) response;

				// add the last modified header
				if (lastModified != -1) {
					httpResponse.setDateHeader(CacheFilter.HEADER_LAST_MODIFIED, lastModified);
				}

				// add the expires header
				if (expires != Long.MAX_VALUE) {
					httpResponse.setDateHeader(CacheFilter.HEADER_EXPIRES, expires);
				}

				// add the cache-control header for max-age
				if (maxAge == CacheFilter.MAX_AGE_NO_INIT || maxAge == CacheFilter.MAX_AGE_TIME) {
					// do nothing
				} else if (maxAge > 0) { // set max-age based on life time
					long 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 it
				ByteArrayInputStream 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 content
			response.setContentLength(content.length);
			out.write(content);
		}
		out.flush();
	}

	/**
	 * @return true if the content is GZIP compressed
	 */
	public boolean isContentGZiped() {
		return "gzip".equals(contentEncoding);
	}


SplitServletOutputStream.java

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:[email protected]">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();
	}

}



你可能感兴趣的:(apache,servlet,cache,企业应用)