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 = 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 = 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 = 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響應中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM