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 cache = new HashMap();
// 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 (SC_OK
) 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 (SC_OK
) 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 (SC_OK
) 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 (SC_OK
) 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 (SC_OK
) 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 Serge Knystautas
*/
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 -1
.
*/
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 ServletResponse
.
*
* @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 ServletResponse
.
*
* @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 ServletOutputStream
class so that the stream
* can be captured as it gets written. This is achieved by overriding the
* write()
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 Serge Knystautas
*/
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
* ServletOutputStream
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();
}
}