首页 > 代码库 > tomcat缓存静态资源深入

tomcat缓存静态资源深入

  之前看过apach及nginx对于静态资源(含js,图片,css等)部分的缓存,用于加速并减轻后台实际web服务器的压力。

  静态资源缓存是WEB服务器优化的一种手段,基本原理如下:

  1.客户端浏览器请求服务器一个服务(该服务含有图片,js等静态资源),通常会对于每一个网页中的独立图片或js文件发送一个http请求

  2.WEB服务器对于每个资源HTTP请求进行解析,并生成一个资源修改时间的唯一值(可以是etag或last_modified参数),放入服务器端map,key为资源url,value为资源修改时间。最后将此资源修改时间的唯一值包含在http头上返回,因为是首次请求,所以会将所有内容放在http body中一并返回给客户浏览器端

  3.客户浏览器接收服服务器响应,并将服务器返回的资源修改时间作为key放入浏览器客户端,value为http body中的实际资源内容

  4.客户浏览器再次请求静态资源时,会将资源修改时间一并发送给服务器

  5.服务端会从最新的map中取出该资源url对应的修改时间,如果值晚于客户端请求的资源修改时间,这时会返回最新的已经修改过的资源给客户端。否则返回304 not modifed


  这里记录资源修改时间的方式有etag及last_modified。最先有的是last_modified,它的工作方式就是上述介绍的,但缺点是只能精确到秒级别。也就是说当你在一秒中修改资源两次,而客户端拿到的是第一次修改,那之后就算客户端第二次再次请求也不会拿到最新的资源。

 而etag的出现正是为了解决last_modified的秒级问题,于http 1.1被提出。

 

  今天测试了下,在没有nginx等前端反向代理服务器时,tomcat竟然默认对静态资源做了缓存。

  tomcat默认运用etag及last_modifed。etag与if_no_match(客户端浏览器上传时在http head中应该放的属性名)一起使用,last_modified与If-Modified-Since一起使用。


客户端首次请求时,得到请求响应数据如下:

  GET http://localhost:8080/webTest/jsp/index.jsp [HTTP/1.1 200 OK 1ms]
  GET http://localhost:8080/webTest/js/hello.js [HTTP/1.1 200 OK 1ms]
  GET http://localhost:8080/webTest/img/a.jpg [HTTP/1.1 200 OK 2ms]

我们看一下Hello.js这个请求响应具体信息:

server Apache-Coyote/1.1 (表明服务器是tomcat)
Last-Modified:    Sun, 11 May 2014 10:54:33 GMT
Etag:    W/"175-1399805673000"
Date:    Sun, 11 May 2014 10:59:23 GMT
Content-Type:    application/javascript;charset=UTF-8
Content-Length:    175
Accept-Ranges:    bytes

从上面可以看到tomcat即返回了last_modified也返回了etag。


客户端再次请求时,请求数据如下:

If-None-Match:    W/"175-1399805673000"
If-Modified-Since:    Sun, 11 May 2014 10:54:33 GMT

响应如下:

  GET http://localhost:8080/webTest/jsp/index.jsp [HTTP/1.1 200 OK 1ms]
  GET http://localhost:8080/webTest/js/hello.js [HTTP/1.1 304 Not Modified 1ms]
  GET http://localhost:8080/webTest/img/a.jpg [HTTP/1.1 304 Not Modified 1ms] 

从中我们可以看到tomcat对于静态数据作了缓存。


接着我们分析tomcat对于这部分静态缓存的判断处理,这部分逻辑是写在DefaultServlet类中,

我们可以在doGet方法中进入ServiceContext方法中找到以下源码:

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

            // Checking If headers
            boolean included =
                (request.getAttribute(Globals.INCLUDE_CONTEXT_PATH_ATTR) != null);
            if (!included
                && !checkIfHeaders(request, response, cacheEntry.attributes)) {  //这句判断是否需要返回整个资源请求
                return;
            }

        }

上面源码的  if (!included
                && !checkIfHeaders(request, response, cacheEntry.attributes))

用于判断是否需要返回整个资源,如果indcluded与checkIfHeaders方法返回的都是false,这时就直接返回,说明资源未修改,或者是缓存不支持的请求方式。

我们接着查看checkIfHeaders方法:

 /**
     * Check if the conditions specified in the optional If headers are
     * satisfied.
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resourceAttributes The resource information
     * @return boolean true if the resource meets all the specified conditions,
     * and false if any of the conditions is not satisfied, in which case
     * request processing is stopped
     */
    protected boolean checkIfHeaders(HttpServletRequest request,
                                     HttpServletResponse response,
                                     ResourceAttributes resourceAttributes)
        throws IOException {

        return checkIfMatch(request, response, resourceAttributes)
            && checkIfModifiedSince(request, response, resourceAttributes)
            && checkIfNoneMatch(request, response, resourceAttributes)
            && checkIfUnmodifiedSince(request, response, resourceAttributes);

    }


可以看到tomcat只有当这四个属性全部返回true(也就是说全部认为资源已经改变)才会返回true,这样最终会将整个资源(最新修改过的)返回客户端。

在这里,我们从上面实际过程当中看到,浏览器第二次请求资源时在http请求header中放了

If-None-Match:    W/"175-1399805673000"
If-Modified-Since:    Sun, 11 May 2014 10:54:33 GMT

这两个属性。

因此我们查看

  && checkIfModifiedSince(request, response, resourceAttributes)
            && checkIfNoneMatch(request, response, resourceAttributes)

这两个方法

checkIfModifiedSince源码如下:

  /**
     * Check if the if-modified-since condition is satisfied.
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resourceInfo File object
     * @return boolean true if the resource meets the specified condition,
     * and false if the condition is not satisfied, in which case request
     * processing is stopped
     */
    protected boolean checkIfModifiedSince(HttpServletRequest request,
            HttpServletResponse response,
            ResourceAttributes resourceAttributes) {
        try {
            long headerValue = http://www.mamicode.com/request.getDateHeader("If-Modified-Since");
            long lastModified = resourceAttributes.getLastModified();
            if (headerValue != -1) {

                // If an If-None-Match header has been specified, if modified since
                // is ignored.
                if ((request.getHeader("If-None-Match") == null)
                    && (lastModified < headerValue + 1000)) {
                    // The entity has not been modified since the date
                    // specified by the client. This is not an error case.
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    response.setHeader("ETag", resourceAttributes.getETag());

                    return false;
                }
            }
        } catch (IllegalArgumentException illegalArgument) {
            return true;
        }
        return true;

    }

源码中可以看到:

   if ((request.getHeader("If-None-Match") == null)
                    && (lastModified < headerValue + 1000)) {

这句话表明只有在客户端浏览器发送的请求头中不包含If-None-Match,IfModifiedSince才会生效。

我们接着看checkIfNoneMatch,源码如下:

/**
     * Check if the if-none-match condition is satisfied.
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resourceInfo File object
     * @return boolean true if the resource meets the specified condition,
     * and false if the condition is not satisfied, in which case request
     * processing is stopped
     */
    protected boolean checkIfNoneMatch(HttpServletRequest request,
                                     HttpServletResponse response,
                                     ResourceAttributes resourceAttributes)
        throws IOException {

        String eTag = resourceAttributes.getETag();
        String headerValue = http://www.mamicode.com/request.getHeader("If-None-Match");
        if (headerValue != null) {

            boolean conditionSatisfied = false;

            if (!headerValue.equals("*")) {

                StringTokenizer commaTokenizer =
                    new StringTokenizer(headerValue, ",");

                while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
                    String currentToken = commaTokenizer.nextToken();
                    if (currentToken.trim().equals(eTag))
                        conditionSatisfied = true;
                }

            } else {
                conditionSatisfied = true;
            }

            if (conditionSatisfied) {

                // For GET and HEAD, we should respond with
                // 304 Not Modified.
                // For every other method, 412 Precondition Failed is sent
                // back.
                if ( ("GET".equals(request.getMethod()))
                     || ("HEAD".equals(request.getMethod())) ) {
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    response.setHeader("ETag", eTag);

                    return false;
                }
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return false;
            }
        }
        return true;

    }

这里:

  String eTag = resourceAttributes.getETag();
        String headerValue = http://www.mamicode.com/request.getHeader("If-None-Match");

这两句比较简单,就是分别从服务器缓存和http请求头中中取出etag。

接着判断这两个etag如果相等,则conditionSatisfied为true,会执行到以下语句:

  if (conditionSatisfied) {

                // For GET and HEAD, we should respond with
                // 304 Not Modified.
                // For every other method, 412 Precondition Failed is sent
                // back.
                if ( ("GET".equals(request.getMethod()))
                     || ("HEAD".equals(request.getMethod())) ) {
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    response.setHeader("ETag", eTag);

                    return false;
                }
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return false;
    }

这段语句中可以发现,如果资源未改变的情况下,并且请求方式为GET或者HEAD时,会返回304状态码。否则返回一个412状态码,同样不会返回资源内容。

如果上述最终

if ((request.getHeader("If-None-Match") == null)
                    && (lastModified < headerValue + 1000))

条件不成立,即资源更新了或者是第一次请求,这里会读取当前请求资源文件,并最终放入http响应中。