1. 程式人生 > >SpringMVC(17) - HTTP快取支援

SpringMVC(17) - HTTP快取支援

參考:https://docs.spring.io/spring/docs/4.3.20.RELEASE/spring-framework-reference/htmlsingle/#mvc-caching

 

良好的HTTP快取策略可以顯著提高Web應用程式的效能和客戶端的體驗。 'Cache-Control'HTTP響應頭主要負責這一點,以及條件性響應頭,如'Last-Modified'和'ETag'。

'Cache-Control'HTTP響應頭建議私有快取(例如瀏覽器)和公共快取(例如代理)如何快取HTTP響應以供進一步重用。

ETag(entity tag:實體標籤)是由HTTP/1.1相容的Web伺服器返回的HTTP響應頭,用於確定給定URL的內容更改。它可以被認為是Last-Modified頭的更復雜的後繼者。當伺服器返回帶有ETag標頭的表示時,在If-None-Match

頭中,客戶端可以在後續GET中使用此標頭。如果內容未更改,則伺服器返回 304:Not Modified

 

1. Cache-Control HTTP頭
Spring Web MVC支援許多用例和方法來為應用程式配置“Cache-Control”頭。雖然RFC 7234第5.2.2節完整地描述了該頭及其可能的指令(參考:https://tools.ietf.org/html/rfc7234#section-5.2.2),但有幾種方法可以解決最常見的情況。

Spring Web MVC在其幾個API中使用配置約定:setCachePeriod(int seconds):

  • -1值:不會生成“Cache-Control”響應頭。
  • 0值:將使用“Cache-Control:no-store”指令阻止快取。
  • n > 0值:將使用'Cache-Control: max-age=n'指令將給定的響應快取n秒。

CacheControl構建器類簡單地描述了可用的“Cache-Control”指令,並使構建自己的HTTP快取策略變得更容易。構建完成後,可以在幾個Spring Web MVC API中接受CacheControl例項作為引數。

// 快取一小時 - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// 阻止快取 - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// 在公共和私有快取中快取十天,公共快取不應該轉換響應
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();

 

2. 靜態資源的HTTP快取支援
應使用適當的“Cache-Control”和條件頭提供靜態資源,以獲得最佳效能。 配置ResourceHttpRequestHandler以提供靜態資源不僅可以通過讀取檔案的元資料本地寫入“Last-Modified”頭,還可以在正確配置的情況下寫入“Cache-Control”頭。

可以在ResourceHttpRequestHandler上設定cachePeriod屬性,也可以使用CacheControl例項,該例項支援更具體的指令:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public-resources/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
    }

}

在XML中:

<mvc:resources mapping="/resources/**" location="/public-resources/">
    <mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>

 

3. 控制器中Cache-Control、ETag和Last-Modified響應頭的支援
控制器可以支援'Cache-Control','ETag' 或 'If-Modified-Since'HTTP請求;如果要在響應上設定“Cache-Control”頭,確實建議這樣做。 這涉及計算給定請求的lastModified long 或 Etag值,將其與“If-Modified-Since”請求頭值進行比較,並可能返回狀態程式碼為304(未修改)的響應。

控制器可以使用HttpEntity型別與請求/響應進行互動。 返回ResponseEntity的控制器可以在響應中包含HTTP快取資訊,如下所示:

@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
                .ok()
                .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
                .eTag(version) // lastModified is also available
                .body(book);
}

執行此操作不僅會在響應中包含“ETag”和“Cache-Control”頭,而且如果客戶端傳送的條件頭與控制器設定的快取資訊匹配,它還會將響應轉換為具有空響應體的 HTTP 304 Not Modified 響應。

@RequestMapping方法也可能希望支援相同的行為。 這可以通過以下方式實現:

@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {

    long lastModified = // 1. application-specific calculation

    if (request.checkNotModified(lastModified)) {
        // 2. shortcut exit - no further processing necessary
        return null;
    }

    // 3. or otherwise further request processing, actually preparing content
    model.addAttribute(...);
    return "myViewName";
}

這裡有兩個關鍵元素:呼叫request.checkNotModified(lastModified)並返回null。前者在返回true之前設定適當的響應狀態和響應頭。後者與前者相結合,導致Spring MVC不再對請求進行進一步處理。

請注意,有3種情況:

  • request.checkNotModified(lastModified):將lastModified與'If-Modified-Since'或'If-Unmodified-Since'請求頭進行比較
  • request.checkNotModified(eTag):將eTag與'If-None-Match'請求頭進行比較
  • request.checkNotModified(eTag, lastModified):同時執行這兩項操作,這意味著兩個條件都應該有效

當接收到條件 'GET'/'HEAD' 請求時,checkNotModified 將檢查資源是否未被修改,如果是,則將導致HTTP 304 Not Modified響應。在條件 'POST'/'PUT'/'DELETE' 請求的情況下,checkNotModified將檢查資源是否已被修改,如果已經修改,則將導致HTTP 409 Precondition Failed響應以防止併發修改。

 

4. 簡單ETag支援
Servlet過濾器ShallowEtagHeaderFilter提供對ETag的支援。它是一個普通的Servlet過濾器,因此可以與任何Web框架結合使用。 ShallowEtagHeaderFilter過濾器建立所謂的簡單ETag(與深ETag相反,稍後將詳細介紹)。過濾器快取呈現的JSP(或其他內容)的內容,生成MD5雜湊,並將其作為ETag返回響應中的頭。客戶端下次傳送對同一資源的請求時,會將該雜湊值用作If-None-Match值。過濾器檢測到此情況,再次呈現檢視,並比較兩個雜湊值。如果它們相等,則返回304。

請注意,此策略可以節省網路頻寬,但不能節省CPU,因為必須為每個請求計算完整響應。控制器級別的其他策略(如上所述)可以節省網路頻寬並避免計算。

此過濾器具有writeWeakETag引數,該引數將過濾器配置為寫入弱ETag,如下所示:W/"02a2d595e6ed9a0b24f027f2b63b134d6",如RFC 7232第2.3節中所定義(https://tools.ietf.org/html/rfc7232#section-2.3)。

在web.xml中配置ShallowEtagHeaderFilter:

<filter>
    <filter-name>etagFilter</filter-name>
    <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
    <!-- Optional parameter that configures the filter to write weak ETags
    <init-param>
        <param-name>writeWeakETag</param-name>
        <param-value>true</param-value>
    </init-param>
    -->
</filter>

<filter-mapping>
    <filter-name>etagFilter</filter-name>
    <servlet-name>petclinic</servlet-name>
</filter-mapping>

或者在Servlet 3.0+環境中,

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new ShallowEtagHeaderFilter() };
    }

}

有關更多詳細資訊,參考下文。

 

5. 基於程式碼的Servlet容器初始化
在Servlet 3.0+環境中,可以選擇以程式設計方式配置Servlet容器作為替代方法,也可以與web.xml檔案結合使用。 下面是註冊DispatcherServlet的示例:

import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }

}

WebApplicationInitializer是Spring MVC提供的一個介面,可確保檢測到自定義的實現並自動用於初始化任何Servlet 3容器。 名為AbstractDispatcherServletInitializer的WebApplicationInitializer的抽象基類實現通過簡單地重寫方法來指定servlet對映和DispatcherServlet配置的位置,從而更容易註冊DispatcherServlet。

對於使用基於Java的Spring配置的應用程式,建議使用此方法:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MyWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

如果使用基於XML的Spring配置,則應直接從AbstractDispatcherServletInitializer擴充套件:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

AbstractDispatcherServletInitializer還提供了一種方便的方法來新增Filter例項並使它們自動對映到DispatcherServlet:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }

}

每個過濾器都根據其具體型別新增預設名稱,並自動對映到DispatcherServlet。

AbstractDispatcherServletInitializer的isAsyncSupported受保護方法提供了一個單獨的位置來啟用DispatcherServlet上的非同步支援以及對映到它的所有過濾器。 預設情況下,此標誌設定為true。

最後,如果需要進一步自定義DispatcherServlet本身,則可以覆蓋createDispatcherServlet方法。