Tomcat处理静态文件DefaultServlet分析

本文探讨了Tomcat7中DefaultServlet处理静态文件的方式,特别是如何根据HTTP缓存策略决定返回304 Not Modified或200状态。在F5刷新时,静态资源如CSS和GIF通常返回304,而JSP作为Servlet始终返回200。源码分析显示,DefaultServlet会检查服务端缓存,通过If-None-Match和If-Modified-Since头来判断是否需要重新发送资源。对于大文件,会考虑使用sendfile优化。此外,还介绍了缓存的有效期和更新机制,以及哪些路径下的文件不会被缓存。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题的起因是,用网页打开tomcat7服务器上一个只有静态内容的jsp页面,里面链接了gif文件,F5刷新的时候,css和gif文件请求返回304    not  Mofified 头,而jsp请求还是返回200(想搞破坏,把jsp也可以在通过304返回头直接读取客户端缓存,但jsp是servlet只能在servlet容器中运行。。。)

因为jsp文件请求时tomcat的JspServlet处理的,而css和html、gif等静态文件默认是tomcat的DefaultServlet处理的(在tomcat配置文件conf/web.xml中有配置)。 

 先来看下tomcat7的DefaultServlet的源码:

  服务端通过resource.lookupCache(path)从服务端缓存中读取资源获得CacheEntry,如果请求资源不存在CacheEntry.exists为false,则返回404。然后通过checkIfHeaders(request, response, cacheEntry.attributes)方法根据客户端请求头的If-None-Match,If-Modified-Since来判断请求资源是否被修改,如果未被修改,则返回304头,户端直接从客户端缓存中读取资源文件。如果第一次访问或者ctrl+F5强制刷新或者资源已修改过,默认返回Etag和Last-Modified头,,告知客户端下次访问可以通过If-None-Match,If-Modified-Since比较返回304来判断是否可使用客户端缓存。还有设置文件内容,content-length,range头等,最后输出内容:

if (!checkSendfile(request, response, cacheEntry, contentLength, null))
                        copy(cacheEntry, renderResult, ostream);

如果文件超过48k,判断是否使用sendfile来输出大文件。

请求头中通过range请求部分下载,则:
                        if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))
                            copy(cacheEntry, ostream, range);

protected void serveResource(HttpServletRequest request,
                                 HttpServletResponse response,
                                 boolean content)
        throws IOException, ServletException {

        boolean serveContent = content;

        // Identify the requested resource path
        String path = getRelativePath(request);
        if (debug > 0) {
            if (serveContent)
                log("DefaultServlet.serveResource:  Serving resource '" +
                    path + "' headers and data");
            else
                log("DefaultServlet.serveResource:  Serving resource '" +
                    path + "' headers only");
        }
        // 从服务端缓存中读取资源
        CacheEntry cacheEntry = resources.lookupCache(path);
        //请求资源不存在,则返回404
        if (!cacheEntry.exists) {
            // Check if we're included so we can return the appropriate
            // missing resource name in the error
            String requestUri = (String) request.getAttribute(
                    RequestDispatcher.INCLUDE_REQUEST_URI);
            if (requestUri == null) {
                requestUri = request.getRequestURI();
            } else {
                // We're included
                // SRV.9.3 says we must throw a FNFE
                throw new FileNotFoundException(
                        sm.getString("defaultServlet.missingResource",
                    requestUri));
            }

            response.sendError(HttpServletResponse.SC_NOT_FOUND,
                               requestUri);
            return;
        }

        // If the resource is not a collection, and the resource path
        // ends with "/" or "\", return NOT FOUND
        if (cacheEntry.context == null) {
            if (path.endsWith("/") || (path.endsWith("\\"))) {
                // Check if we're included so we can return the appropriate
                // missing resource name in the error
                String requestUri = (String) request.getAttribute(
                        RequestDispatcher.INCLUDE_REQUEST_URI);
                if (requestUri == null) {
                    requestUri = request.getRequestURI();
                }
                response.sendError(HttpServletResponse.SC_NOT_FOUND,
                                   requestUri);
                return;
            }
        }

        boolean isError =
            response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST;

        // Check if the conditions specified in the optional If headers are
        // satisfied.
        if (cacheEntry.context == null) {

            // Checking If headers
            boolean included = (request.getAttribute(
                    RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
            //checkIfHeaders根据客户端请求头的If-None-Match,If-Modified-Since
            //来判断请求资源是否被修改,如果未被修改,则返回304头
            //客户端直接从客户端缓存中读取资源文件。
            if (!included && !isError &&
                    !checkIfHeaders(request, response, cacheEntry.attributes)) {
                return;
            }

        }

        // Find content type.
        String contentType = cacheEntry.attributes.getMimeType();
        if (contentType == null) {
            contentType = getServletContext().getMimeType(cacheEntry.name);
            cacheEntry.attributes.setMimeType(contentType);
        }

        ArrayList<Range> ranges = null;
        long contentLength = -1L;

        if (cacheEntry.context != null) {

            // Skip directory listings if we have been configured to
            // suppress them
            if (!listings) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND,
                                   request.getRequestURI());
                return;
            }
            contentType = "text/html;charset=UTF-8";

        } else {
            if (!isError) {
            	//第一次访问或者ctrl+F5强制刷新或者前面资源已修改过,静态文件处理
            	//默认返回Etag和Last-Modified,告知客户端下次访问可以
            	//通过If-None-Match,If-Modified-Since比较返回304来判断是否可使用客户端缓存
                if (useAcceptRanges) {
                    // Accept ranges header
                    response.setHeader("Accept-Ranges", "bytes");
                }

                // Parse range specifier
                ranges = parseRange(request, response, cacheEntry.attributes);

                // ETag header
                response.setHeader("ETag", cacheEntry.attributes.getETag());

                // Last-Modified header
                response.setHeader("Last-Modified",
                        cacheEntry.attributes.getLastModifiedHttp());
            }

            // Get content length
            contentLength = cacheEntry.attributes.getContentLength();
            // Special case for zero length files, which would cause a
            // (silent) ISE when setting the output buffer size
            if (contentLength == 0L) {
                serveContent = false;
            }

        }

        ServletOutputStream ostream = null;
        PrintWriter writer = null;

        if (serveContent) {

            // Trying to retrieve the servlet output stream

            try {
                ostream = response.getOutputStream();
            } catch (IllegalStateException e) {
                // If it fails, we try to get a Writer instead if we're
                // trying to serve a text file
                if ( (contentType == null)
                        || (contentType.startsWith("text"))
                        || (contentType.endsWith("xml"))
                        || (contentType.contains("/javascript")) ) {
                    writer = response.getWriter();
                    // Cannot reliably serve partial content with a Writer
                    ranges = FULL;
                } else {
                    throw e;
                }
            }

        }

        // Check to see if a Filter, Valve of wrapper has written some content.
        // If it has, disable range requests and setting of a content length
        // since neither can be done reliably.
        ServletResponse r = response;
        long contentWritten = 0;
        while (r instanceof ServletResponseWrapper) {
            r = ((ServletResponseWrapper) r).getResponse();
        }
        if (r instanceof ResponseFacade) {
            contentWritten = ((ResponseFacade) r).getContentWritten();
        }
        if (contentWritten > 0) {
            ranges = FULL;
        }

        if ( (cacheEntry.context != null)
                || isError
                || ( ((ranges == null) || (ranges.isEmpty()))
                        && (request.getHeader("Range") == null) )
                || (ranges == FULL) ) {

            // Set the appropriate output headers
            if (contentType != null) {
                if (debug > 0)
                    log("DefaultServlet.serveFile:  contentType='" +
                        contentType + "'");
                response.setContentType(contentType);
            }
            if ((cacheEntry.resource != null) && (contentLength >= 0)
                    && (!serveContent || ostream != null)) {
                if (debug > 0)
                    log("DefaultServlet.serveFile:  contentLength=" +
                        contentLength);
                // Don't set a content length if something else has already
                // written to the response.
                if (contentWritten == 0) {
                    if (contentLength < Integer.MAX_VALUE) {
                        response.setContentLength((int) contentLength);
                    } else {
                        // Set the content-length as String to be able to use a
                        // long
                        response.setHeader("content-length",
                                "" + contentLength);
                    }
                }
            }

            InputStream renderResult = null;
            if (cacheEntry.context != null) {

                if (serveContent) {
                    // Serve the directory browser
                    renderResult = render(getPathPrefix(request), cacheEntry);
                }

            }

            // Copy the input stream to our output stream (if requested)
            if (serveContent) {
                try {
                    response.setBufferSize(output);
                } catch (IllegalStateException e) {
                    // Silent catch
                }
                if (ostream != null) {
                	//这里是content输出,判断是否使用sendfile来输出大文件
                    if (!checkSendfile(request, response, cacheEntry, contentLength, null))
                        copy(cacheEntry, renderResult, ostream);
                } else {
                    copy(cacheEntry, renderResult, writer);
                }
            }

        } else {
        	//
            if ((ranges == null) || (ranges.isEmpty()))
                return;

            // Partial content response.

            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

            if (ranges.size() == 1) {

                Range range = ranges.get(0);
                response.addHeader("Content-Range", "bytes "
                                   + range.start
                                   + "-" + range.end + "/"
                                   + range.length);
                long length = range.end - range.start + 1;
                if (length < Integer.MAX_VALUE) {
                    response.setContentLength((int) length);
                } else {
                    // Set the content-length as String to be able to use a long
                    response.setHeader("content-length", "" + length);
                }

                if (contentType != null) {
                    if (debug > 0)
                        log("DefaultServlet.serveFile:  contentType='" +
                            contentType + "'");
                    response.setContentType(contentType);
                }

                if (serveContent) {
                    try {
                        response.setBufferSize(output);
                    } catch (IllegalStateException e) {
                        // Silent catch
                    }
                    if (ostream != null) {
                    	//Range来实现资源文件部分内容传输
                        if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))
                            copy(cacheEntry, ostream, range);
                    } else {
                        // we should not get here
                        throw new IllegalStateException();
                    }
                }

            } else {

                response.setContentType("multipart/byteranges; boundary="
                                        + mimeSeparation);

                if (serveContent) {
                    try {
                        response.setBufferSize(output);
                    } catch (IllegalStateException e) {
                        // Silent catch
                    }
                    if (ostream != null) {
                        copy(cacheEntry, ostream, ranges.iterator(),
                             contentType);
                    } else {
                        // we should not get here
                        throw new IllegalStateException();
                    }
                }

            }

        }
 }
 静态文件优先从缓存中读取:

首先从存在的资源缓存cache中查找,未找到则从不存在的资源的缓存notFoundCache中查找。如果找到,检查缓存是否有效。cache默认有效期5秒,5秒之内不检查原文件是否有修改,超过有效期,需要验证原文件的lastModified和ContendLenth和缓存中的是否一致,不一致清除缓存,一致则更新缓存的timestap再次5秒有效期。(这样的话,如果修改css或js等静态文件,如果测试的人一直访问(5秒间隔内)这个页面,导致静态文件一直从服务端缓存中读取,那样无论是否强制刷新修改都不会生效啊。)未找到则生成CacheEntry,加载到相应的cache 或notFoundCache中。当然,nonCacheable数组默认/WEB-INF/lib/, /WEB-INF/classes/路径下文件都不从缓存获取。这里cache缓存设计成一个有序数组,而notFoundCache设计为一个HashMap,cache操作稍微复杂点。难道是因为cache释放内存需要更细粒度的控制?

 

    protected CacheEntry cacheLookup(String lookupName) {
        if (cache == null)
            return (null);
        String name;
        if (lookupName == null) {
            name = "";
        } else {
            name = lookupName;
        }
        //无法被缓存的资源:/WEB-INF/lib/, /WEB-INF/classes/
        for (int i = 0; i < nonCacheable.length; i++) {
            if (name.startsWith(nonCacheable[i])) {
                return (null);
            }
        } 
// 
 CacheEntry cacheEntry = cache.lookup(name);
        if (cacheEntry == null) {
            cacheEntry = new CacheEntry();
            cacheEntry.name = name;
            // Load entry
            cacheLoad(cacheEntry);
        } else {
        	/*cache有效期5秒,5秒之内不检查原文件是否有修改,超过有效期,
        	需要验证原文件的lastModified和ContendLenth和缓存中的是否一致,
        	不一致清除缓存,一致则更新缓存的timestap再次5秒有效期。*/
            if (!validate(cacheEntry)) {
                if (!revalidate(cacheEntry)) {
                    cacheUnload(cacheEntry.name);
                    return (null);
                } else {
                    cacheEntry.timestamp = 
                        System.currentTimeMillis() + cacheTTL;
                }
            }
            cacheEntry.accessCount++;
        }
        return (cacheEntry);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值