encache的web cache代码分析
1.抽象filter分析
public abstract class Filter implements javax.servlet.Filter {
......
public final void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws ServletException, IOException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
//NO_FILTER set for RequestDispatcher forwards to avoid double gzipping
if (filterNotDisabled(httpRequest)) { //判断是否需要进行改cache filter的处理,防止再次进入,简单的通过获取attribute属性来判断
doFilter(httpRequest, httpResponse, chain);
} else {
chain.doFilter(request, response);
}
} catch (final Throwable throwable) {
logThrowable(throwable, httpRequest);
}
}
protected boolean filterNotDisabled(final HttpServletRequest httpRequest) {
return httpRequest.getAttribute(NO_FILTER) == null;
}
......
//提供几个抽象方法供子类覆盖
/**
* A template method that performs any Filter specific destruction tasks.
* Called from {@link #destroy()}
*/
protected abstract void doDestroy();
/**
* A template method that performs the filtering for a request.
* Called from {@link #doFilter(ServletRequest,ServletResponse,FilterChain)}.
*/
protected abstract void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
final FilterChain chain) throws Throwable;
/**
* A template method that performs any Filter specific initialisation tasks.
* Called from {@link #init(FilterConfig)}.
* @param filterConfig
*/
protected abstract void doInit(FilterConfig filterConfig) throws Exception;
}
2.实现上面filter的CachingFilter类分析
public abstract class CachingFilter extends Filter{
//初始化参数列表
public void doInit(FilterConfig filterConfig) throws CacheException {
synchronized (this.getClass()) {
if (blockingCache == null) {
setCacheNameIfAnyConfigured(filterConfig); //从web.xml里面取出encache对应配置的cache名称
final String localCacheName = getCacheName();
Ehcache cache = getCacheManager().getEhcache(localCacheName); //根据cachename从encache管理器获取cache实例
if (cache == null) {
throw new CacheException("cache '" + localCacheName
+ "' not found in configuration");
}
if (!(cache instanceof BlockingCache)) {
// decorate and substitute
BlockingCache newBlockingCache = new BlockingCache(cache);
getCacheManager().replaceCacheWithDecoratedCache(cache,
newBlockingCache);
}
blockingCache = (BlockingCache) getCacheManager().getEhcache(
localCacheName); //这里使用的是blockingCache
Integer blockingTimeoutMillis = parseBlockingCacheTimeoutMillis(filterConfig);//从init-param里面获取设置的blocking的超时时间
if (blockingTimeoutMillis != null && blockingTimeoutMillis > 0) {
blockingCache.setTimeoutMillis(blockingTimeoutMillis);
}
}
}
}
protected void doFilter(final HttpServletRequest request,
final HttpServletResponse response, final FilterChain chain)
throws AlreadyGzippedException, AlreadyCommittedException,
FilterNonReentrantException, LockTimeoutException, Exception {
if (response.isCommitted()) {
throw new AlreadyCommittedException(
"Response already committed before doing buildPage.");
}
logRequestHeaders(request);//记录request的日志
PageInfo pageInfo = buildPageInfo(request, response, chain);//从缓存里面取出缓存页面信息,如果缓存中不存在,则通过 chain.doFilter(request, wrapper);处理后,再把页面存入缓存,
if (pageInfo.isOk()) {
if (response.isCommitted()) {
throw new AlreadyCommittedException(
"Response already committed after doing buildPage"
+ " but before writing response from PageInfo.");
}
writeResponse(request, response, pageInfo);//更新request的header,status,cookie等相关信息,因为如果从缓存获取的话,需要把所有信息都写到response
}
}
//再来看看buildPageInfo的处理
/**
* Build page info either using the cache or building the page directly.
* <p/>
* Some requests are for page fragments which should never be gzipped, or
* for other pages which are not gzipped.
*/
protected PageInfo buildPageInfo(final HttpServletRequest request,
final HttpServletResponse response, final FilterChain chain)
throws Exception {
// Look up the cached page
final String key = calculateKey(request);//构造key,抽象方法,由子类覆盖
PageInfo pageInfo = null;
try {
checkNoReentry(request); //检查是否是同一个请求再次今夕,通过localthread来判断,如果已经进入过,则抛出FilterNonReentrantException异常,否则记录已经该请求进入处理
Element element = blockingCache.get(key); //从缓存取出对象
if (element == null || element.getObjectValue() == null) {
try {
// Page is not cached - build the response, cache it, and
// send to client
pageInfo = buildPage(request, response, chain);//如果缓存不存在,则正常处理,
if (pageInfo.isOk()) {//如果处理ok,
if (LOG.isDebugEnabled()) {
LOG.debug("PageInfo ok. Adding to cache "
+ blockingCache.getName() + " with key "
+ key);
}
blockingCache.put(new Element(key, pageInfo));//则把页面内容重新存入缓存
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("PageInfo was not ok(200). Putting null into cache "
+ blockingCache.getName()
+ " with key "
+ key);
}
blockingCache.put(new Element(key, null));//如果处理出错,则缓存设置为null
}
} catch (final Throwable throwable) {
// Must unlock the cache if the above fails. Will be logged
// at Filter
blockingCache.put(new Element(key, null));
throw new Exception(throwable);
}
} else {
pageInfo = (PageInfo) element.getObjectValue();//如果缓存存在,则取出内容
}
} catch (LockTimeoutException e) {
// do not release the lock, because you never acquired it
throw e;
} finally {
// all done building page, reset the re-entrant flag
visitLog.clear();
}
return pageInfo;
}
/**
* Builds the PageInfo object by passing the request along the filter chain
*
* @param request
* @param response
* @param chain
* @return a Serializable value object for the page or page fragment
* @throws AlreadyGzippedException
* if an attempt is made to double gzip the body
* @throws Exception
*/
//缓存不存在时的处理
protected PageInfo buildPage(final HttpServletRequest request,
final HttpServletResponse response, final FilterChain chain)
throws AlreadyGzippedException, Exception {
// Invoke the next entity in the chain
final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
final GenericResponseWrapper wrapper = new GenericResponseWrapper(
response, outstr);//这个responsewrapper,用于保存正常处理后的状态,cookie,header,结果信息,用于缓存
chain.doFilter(request, wrapper);//缓存不存在,则跳过由后续filter处理
wrapper.flush();
long timeToLiveSeconds = blockingCache.getCacheConfiguration()
.getTimeToLiveSeconds();
// Return the page info
return new PageInfo(wrapper.getStatus(), wrapper.getContentType(),
wrapper.getCookies(), outstr.toByteArray(), true,
timeToLiveSeconds, wrapper.getAllHeaders());//构造信息的该请求的缓存对象信息
}
/**
* Writes the response from a PageInfo object.
* <p/>
* Headers are set last so that there is an opportunity to override
*
* @param request
* @param response
* @param pageInfo
* @throws IOException
* @throws DataFormatException
* @throws ResponseHeadersNotModifiableException
*
*/
//从pageinfo里面取出response的相关信息写入到当前的response中
protected void writeResponse(final HttpServletRequest request,
final HttpServletResponse response, final PageInfo pageInfo)
throws IOException, DataFormatException,
ResponseHeadersNotModifiableException {
boolean requestAcceptsGzipEncoding = acceptsGzipEncoding(request);
setStatus(response, pageInfo);//设置response的状态
setContentType(response, pageInfo);//设置response的contentType
setCookies(pageInfo, response);//设置response的cookie
// do headers last so that users can override with their own header sets
setHeaders(pageInfo, requestAcceptsGzipEncoding, response);//设置response的header
writeContent(request, response, pageInfo);//设置response的内容
}
//比如:
protected void setContentType(final HttpServletResponse response,
final PageInfo pageInfo) {
String contentType = pageInfo.getContentType();
if (contentType != null && contentType.length() > 0) {
response.setContentType(contentType);
}
}
/**
* Set the serializableCookies
*
* @param pageInfo
* @param response
*/
protected void setCookies(final PageInfo pageInfo,
final HttpServletResponse response) {
final Collection cookies = pageInfo.getSerializableCookies();
for (Iterator iterator = cookies.iterator(); iterator.hasNext();) {
final Cookie cookie = ((SerializableCookie) iterator.next())
.toCookie();
response.addCookie(cookie);
}
}
/**
* Status code
*
* @param response
* @param pageInfo
*/
protected void setStatus(final HttpServletResponse response,
final PageInfo pageInfo) {
response.setStatus(pageInfo.getStatusCode());
}
protected void writeContent(final HttpServletRequest request,
final HttpServletResponse response, final PageInfo pageInfo)
throws IOException, ResponseHeadersNotModifiableException {
byte[] body;
boolean shouldBodyBeZero = ResponseUtil.shouldBodyBeZero(request,
pageInfo.getStatusCode());
if (shouldBodyBeZero) {
body = new byte[0];
} else if (acceptsGzipEncoding(request)) {
body = pageInfo.getGzippedBody();
if (ResponseUtil.shouldGzippedBodyBeZero(body, request)) {
body = new byte[0];
} else {
ResponseUtil.addGzipHeader(response);
}
} else {
body = pageInfo.getUngzippedBody();
}
response.setContentLength(body.length);
OutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(body);
out.flush();
}
}
对于checkNoReentry的实现,很简单
/**
* Check that this caching filter is not being reentered by the same
* recursively. Recursive calls will block indefinitely because the first
* request has not yet unblocked the cache.
* <p/>
* This condition usually indicates an error in filter chaining or
* RequestDispatcher dispatching.
*
* @param httpRequest
* @throws FilterNonReentrantException
* if reentry is detected
*/
protected void checkNoReentry(final HttpServletRequest httpRequest)
throws FilterNonReentrantException {
String filterName = getClass().getName();
if (visitLog.hasVisited()) {
throw new FilterNonReentrantException(
"The request thread is attempting to reenter" + " filter "
+ filterName + ". URL: "
+ httpRequest.getRequestURL());
} else {
// mark this thread as already visited
visitLog.markAsVisited();
if (LOG.isDebugEnabled()) {
LOG.debug("Thread {} has been marked as visited.", Thread
.currentThread().getName());
}
}
}
/**
* threadlocal class to check for reentry
*
* @author hhuynh
*
*/
private static class VisitLog extends ThreadLocal<Boolean> {
@Override
protected Boolean initialValue() {
return false;
}
public boolean hasVisited() {
return get();
}
public void markAsVisited() {
set(true);
}
public void clear() {
super.remove();
}
}
当然还有两个抽象方法
/**
* Gets the CacheManager for this CachingFilter. It is therefore up to
* subclasses what CacheManager to use.
* <p/>
* This method was introduced in ehcache 1.2.1. Older versions used a
* singleton CacheManager instance created with the default factory method.
*
* @return the CacheManager to be used
* @since 1.2.1
*/
protected abstract CacheManager getCacheManager();
/**
* CachingFilter works off a key.
* <p/>
* The key should be unique. Factors to consider in generating a key are:
* <ul>
* <li>The various hostnames that a request could come through
* <li>Whether additional parameters used for referral tracking e.g. google
* should be excluded to maximise cache hits
* <li>Additional parameters can be added to any page. The page will still
* work but will miss the cache. Consider coding defensively around this
* issue.
* </ul>
* <p/>
* Implementers should differentiate between GET and HEAD requests otherwise
* blank pages can result. See SimplePageCachingFilter for an example
* implementation.
*
* @param httpRequest
* @return the key, generally the URL plus request parameters
*/
protected abstract String calculateKey(final HttpServletRequest httpRequest);
3.SimplePageCachingFilter的处理
public class SimplePageCachingFilter extends CachingFilter {
public static final String DEFAULT_CACHE_NAME = "SimplePageCachingFilter";
private static final Logger LOG = LoggerFactory.getLogger(SimplePageCachingFilter.class);
protected String getCacheName() {
if (cacheName != null && cacheName.length() > 0) {
LOG.debug("Using configured cacheName of {}.", cacheName);
return cacheName;
} else {
LOG.debug("No cacheName configured. Using default of {}.", DEFAULT_CACHE_NAME);
return DEFAULT_CACHE_NAME;
}
}
protected CacheManager getCacheManager() {
return CacheManager.getInstance();
}
protected String calculateKey(HttpServletRequest httpRequest) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(httpRequest.getMethod()).append(httpRequest.getRequestURI()).append(httpRequest.getQueryString());
String key = stringBuffer.toString();
return key;
}
}
而SimpleCachingHeadersPageCachingFilter的处理
public class SimpleCachingHeadersPageCachingFilter extends SimplePageCachingFilter{
@Override
protected PageInfo buildPage(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws AlreadyGzippedException, Exception {
PageInfo pageInfo = super.buildPage(request, response, chain);
final List<Header<? extends Serializable>> headers = pageInfo.getHeaders();
long ttlMilliseconds = calculateTimeToLiveMilliseconds();
//Remove any conflicting headers
for (final Iterator<Header<? extends Serializable>> headerItr = headers.iterator(); headerItr.hasNext();) {
final Header<? extends Serializable> header = headerItr.next();
final String name = header.getName();
if ("Last-Modified".equalsIgnoreCase(name) ||
"Expires".equalsIgnoreCase(name) ||
"Cache-Control".equalsIgnoreCase(name) ||
"ETag".equalsIgnoreCase(name)) {
headerItr.remove();
}
}
//add expires and last-modified headers
//trim the milliseconds off the value since the header is only accurate down to the second
long lastModified = pageInfo.getCreated().getTime();
lastModified = TimeUnit.MILLISECONDS.toSeconds(lastModified);
lastModified = TimeUnit.SECONDS.toMillis(lastModified);
headers.add(new Header<Long>("Last-Modified", lastModified));
headers.add(new Header<Long>("Expires", System.currentTimeMillis() + ttlMilliseconds));
headers.add(new Header<String>("Cache-Control", "max-age=" + ttlMilliseconds / MILLISECONDS_PER_SECOND));
headers.add(new Header<String>("ETag", generateEtag(ttlMilliseconds)));
return pageInfo;
}
@Override
protected void writeResponse(HttpServletRequest request, HttpServletResponse response, PageInfo pageInfo)
throws IOException, DataFormatException, ResponseHeadersNotModifiableException {
final List<Header<? extends Serializable>> headers = pageInfo.getHeaders();
for (final Header<? extends Serializable> header : headers) {
if ("ETag".equals(header.getName())) {
String requestIfNoneMatch = request.getHeader("If-None-Match");
if (header.getValue().equals(requestIfNoneMatch)) {
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
// use the same date we sent when we created the ETag the first time through
//response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"));
return;
}
break;
}
if ("Last-Modified".equals(header.getName())) {
long requestIfModifiedSince = request.getDateHeader("If-Modified-Since");
if (requestIfModifiedSince != -1) {
final Date requestDate = new Date(requestIfModifiedSince);
final Date pageInfoDate;
switch (header.getType()) {
case STRING:
pageInfoDate = this.getHttpDateFormatter().parseDateFromHttpDate((String)header.getValue());
break;
case DATE:
pageInfoDate = new Date((Long)header.getValue());
break;
default:
throw new IllegalArgumentException("Header " + header + " is not supported as type: " + header.getType());
}
if (!requestDate.before(pageInfoDate)) {
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"));
return;
}
}
}
}
super.writeResponse(request, response, pageInfo);
}
}
这个类对于缓存的内容的头信息 Last-Modified Expires Cache-Control ETag删除,替换成当前encache里面的系统信息,输出的时候也输出encache处理过的头信息,这样我们就不管浏览器的缓存处理了,只能等待encache的缓存过期。
4.PageFragmentCachingFilter的简单处理
protected PageInfo buildPage(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain chain) throws AlreadyGzippedException, Exception {
// Invoke the next entity in the chain
final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
final GenericResponseWrapper wrapper = new GenericResponseWrapper(response, outstr);
chain.doFilter(request, wrapper);
wrapper.flush();
long timeToLiveSeconds = blockingCache.getCacheConfiguration().getTimeToLiveSeconds();
// Return the page info
return new PageInfo(wrapper.getStatus(), wrapper.getContentType(),
wrapper.getCookies(),
outstr.toByteArray(), false, timeToLiveSeconds, wrapper.getAllHeaders()); //其中第五个参数false表示不存储gzip过的信息
}
/**
* Assembles a response from a cached page include.
* These responses are never gzipped
* The content length should not be set in the response, because it is a fragment of a page.
* Don't write any headers at all.
*/
//也不处理gzip过的信息
protected void writeResponse(final HttpServletResponse response, final PageInfo pageInfo) throws IOException {
// Write the page
final byte[] cachedPage = pageInfo.getUngzippedBody();
//needed to support multilingual
final String page = new String(cachedPage, response.getCharacterEncoding());
String implementationVendor = response.getClass().getPackage().getImplementationVendor();
if (implementationVendor != null && implementationVendor.equals("\"Evermind\"")) {
response.getOutputStream().print(page);
} else {
response.getWriter().write(page);
}
}
5.PageInfo缓存对象的几个要点
public class PageInfo implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(PageInfo.class);
private static final int FOUR_KB = 4196;
private static final int GZIP_MAGIC_NUMBER_BYTE_1 = 31;
private static final int GZIP_MAGIC_NUMBER_BYTE_2 = -117;
private static final long ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;
//存储的信息包括状态码,contentType,cookie,body,是否gzip过,过期时间,headers等信息
public PageInfo(final int statusCode, final String contentType,
final Collection cookies,
final byte[] body, boolean storeGzipped, long timeToLiveSeconds,
final Collection<Header<? extends Serializable>> headers) throws AlreadyGzippedException {
//Note that the ordering is switched with headers at the end to deal with the erasure issues with Java generics causing
//a conflict with the deprecated PageInfo header
this.init(statusCode, contentType, headers, cookies, body, storeGzipped, timeToLiveSeconds);
}
/**
* @param ungzipped the bytes to be gzipped
* @return gzipped bytes
*/
//提供的gzip处理方法
private byte[] gzip(byte[] ungzipped) throws IOException, AlreadyGzippedException {
if (isGzipped(ungzipped)) {
throw new AlreadyGzippedException("The byte[] is already gzipped. It should not be gzipped again.");
}
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bytes);
gzipOutputStream.write(ungzipped);
gzipOutputStream.close();
return bytes.toByteArray();
}
private byte[] ungzip(final byte[] gzipped) throws IOException {
final GZIPInputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(gzipped));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(gzipped.length);
final byte[] buffer = new byte[FOUR_KB];
int bytesRead = 0;
while (bytesRead != -1) {
bytesRead = inputStream.read(buffer, 0, FOUR_KB);
if (bytesRead != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
}
byte[] ungzipped = byteArrayOutputStream.toByteArray();
inputStream.close();
byteArrayOutputStream.close();
return ungzipped;
}
}
6.GzipFilter的filter,这个filter对于浏览器支持gzip的则进行gzip压缩之后输出