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: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 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: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();
}

}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本工程用于研究如何借助Ehcache缓存框架实现页面缓存 本工程编码方式:UTF-8 本工程开发工具:MyEclipse 说明: 1、ehcache.xml和ehcache.xsd两个文件可以在下在下载下来的名为“ehcache-core-x.x.x-distribution.tar.gz”压缩文件中找到 2、由于要实现Ehcache缓存页面,所以必须要添加“ehcache-web-2.0.4.jar” jar包,该jar包主要用于辅助Ehcache实现页面缓存 注意: 本web工程的发布不要使用Tomcat7,否则会出现如下异常: 2015-3-25 9:53:50 org.apache.catalina.loader.WebappClassLoader loadClass 信息: Illegal access: this web application instance has been stopped already. Could not load net.sf.ehcache.store.disk.DiskStore$KeySet. The eventual following stack trace is caused by an error thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access, and has no functional impact. java.lang.IllegalStateException at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1600) at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1559) at net.sf.ehcache.store.disk.DiskStore.keySet(DiskStore.java:560) at net.sf.ehcache.store.disk.DiskStorageFactory$DiskExpiryTask.run(DiskStorageFactory.java:838) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:441) at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:317) at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:150) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(ScheduledThreadPoolExecutor.java:98) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.runPeriodic(ScheduledThreadPoolExecutor.java:181) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:205) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) at java.lang.Thread.run(Thread.java:619) 相关jar包下载地址: Ehcache 对象、数据缓存:http://ehcache.org/downloads/destination?name=ehcache-core-2.5.2-distribution.tar.gz&bucket=tcdistributions&file=ehcache

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值