[置顶] 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压缩之后输出


posted @ 2012-06-02 08:45  zhwj184  阅读(275)  评论(0编辑  收藏  举报