[置顶] j2ee页面静态化方案encache web cache框架源码分析2
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压缩之后输出