在Spring Boot中實現HTTP快取
快取是HTTP協議的一個強大功能,但由於某些原因,它主要用於靜態資源,如影象,CSS樣式表或JavaScript檔案,但是,HTTP快取不僅限於這些,還可以將其用於動態計算的資源。
通過少量工作,您可以加快應用程式並改善整體使用者體驗。在本文中,您將學習如何使用內建的HTTP響應快取機制來實現快取SpringBoot控制器的結果 。
1.如何以及何時使用HTTP響應快取?
您可以在應用程式的多個層上進行快取。資料庫具有其快取儲存,Web客戶端也在其需要重用的資訊。HTTP協議負責網路通訊。快取機制允許我們通過減少客戶端和伺服器之間傳輸的資料量來優化網路流量。
何時優化:當Web資源不經常更改或您確切知道何時更新時 ,就可以使用HTTP快取進行優化。一旦確定了HTTP快取的競爭者,就需要選擇合適的方法來管理快取的驗證。HTTP協議定義了幾個請求和響應標頭,您可以使用它們來控制客戶端何時清除快取 。
選擇適當的HTTP標頭取決於您要優化的特定情況。但是無論用例如何,我們可以根據快取的驗證發生在哪裡進行快取管理選項的劃分。
2.客戶端快取驗證
當您知道請求的資源在給定的時間內不會更改時,伺服器可以將此類資訊作為響應標頭髮送到客戶端。基於該資訊,客戶端決定是否應該再次獲取資源或重用先前下載的資源。
有兩種可能的選項可以描述客戶端何時應該再次獲取資源並刪除儲存的快取值。所以讓我們看看他們是如何執行的。
HTTP快取在固定的時間內有效:如果要阻止客戶端在指定時間內重新獲取資源 ,則應該使用Cache-Control 標頭,可以在其中指定應該重新獲取所獲取資料的時間。
通過將標頭的值設定為max-age = <seconds>, 可以通知客戶端多長時間不再需要再次獲取資源。快取值的有效性與請求的時間有關。
為了設定在Spring的控制器中的HTTP標頭,就要在RESTContoller用ofollow,noindex" target="_blank"> ResponseEntity 包裝類 。
@GetMapping(<font>"/{id}"</font><font>) ResponseEntity<Product> getProduct(@PathVariable <b>long</b> id) { </font><font><i>// …</i></font><font> CacheControl cacheControl = CacheControl.maxAge(30, TimeUnit.MINUTES); <b>return</b> ResponseEntity.ok() .cacheControl(cacheControl) .body(product); } </font>
HTTP標頭的值只是一個常規字串,但是Cache-Control Spring為我們提供了一個特殊的構建器類,它可以防止我們犯下像拼寫錯誤這樣的小錯誤。
HTTP快取有效到固定日期:有時您知道資源何時會發生變化。對於公佈的資料而言,這是常見的情況,如天氣預報或昨天交易時段計算的股市指標。資源的確切到期日期可以向客戶端公開。 應該使用Expires HTTP標頭。應使用標準化資料格式 之一格式化日期值。
Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
幸運的是,Java附帶了第一個這些格式的預定義格式化程式。可以在下面找到將標題設定為當天結束的示例。
@GetMapping(<font>"/forecast"</font><font>) ResponseEntity<Forecast> getTodaysForecast() { </font><font><i>// ...</i></font><font> ZonedDateTime expiresDate = ZonedDateTime.now().with(LocalTime.MAX); String expires = expiresDate.format(DateTimeFormatter.RFC_1123_DATE_TIME); <b>return</b> ResponseEntity.ok() .header(HttpHeaders.EXPIRES, expires) .body(weatherForecast); } </font>
請注意,HTTP日期格式需要有關時區的資訊 。這就是上面的例子使用ZonedDateTime的原因 。如果您嘗試使用LocalDateTime ,則最終會在執行時出現以下錯誤訊息:
java.time.temporal.UnsupportedTemporalTypeException:不支援的欄位:OffsetSeconds
如果響應中存在Cache-Control 和Expires 標頭,則客戶端僅使用Cache-Control 。
3.伺服器端快取驗證
在基於使用者輸入的動態生成的內容中,更常見的是伺服器不知道何時將改變所請求的資源。在這種情況下,客戶端可以使用先前獲取的資料,但首先,它需要詢問伺服器該資料是否仍然有效。
自第一次握手以來資源是否被修改?如果跟蹤Web資源的修改日期,則可以將此類日期作為響應的一部分公開給客戶端。在下一個請求中,客戶端將此日期傳送回伺服器,以便它可以驗證自上一個請求以來資源是否已被修改。如果資源未更改,則伺服器不必再次重新發送資料。相反,它使用304 HTTP程式碼響應,沒有任何有效負載。
要公開資源的修改日期,您應該設定Last-Modified 標頭。Spring的ResponseEntity構建器有 一個名為lastModified() [url=https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.HeadersBuilder.html#lastModified-long-]的特殊方法[/url],它可以幫助您以正確的格式分配值。你會在一分鐘內看到這一點。
但在傳送完整響應之前,應檢查客戶端是否 在請求中包含 If-Modified-Since 標頭 。客戶端根據Last-Modified 標頭的值設定其值,該標頭是與此特定資源的先前響應一起傳送的。
如果If-Modified-Since 標頭的值與所請求資源的修改日期匹配,則可以節省一些頻寬並使用空主體響應客戶端。
Spring再次提供了一個輔助方法,簡化了上述日期的比較。這個名為checkNotModified()的 方法可以在WebRequest 包裝器類中找到,您可以將其作為輸入新增到控制器的方法中。
讓我們仔細看看完整的例子。
@GetMapping(<font>"/{id}"</font><font>) ResponseEntity<Product> getProduct(@PathVariable <b>long</b> id, WebRequest request) { Product product = repository.find(id); <b>long</b> modificationDate = product.getModificationDate() .toInstant().toEpochMilli(); <b>if</b> (request.checkNotMod<b>if</b>ied(mod<b>if</b>icationDate)) { <b>return</b> <b>null</b>; } <b>return</b> ResponseEntity.ok() .lastModified(modificationDate) .body(product); } </font>
首先,我們獲取所請求的資源並訪問其修改日期。我們將日期轉換為自格林威治標準時間1970年1月1日以來的毫秒數,因為這是Spring框架期望的格式。
然後,我們將日期與If-Modified-Since 標頭的值進行比較,並在正匹配上返回一個空。否則,伺服器傳送具有Last-Modified 標頭的適當值的完整響應主體。
憑藉所有這些知識,您幾乎可以涵蓋所有常見的快取設定選項。但是有一個更重要的機制你應該知道的是......
使用ETag進行資源版本控制
到目前為止,我們定義了有效期的精確度,精確度為1秒。但是如果你需要更好的精度而不僅僅是一秒 呢?這就是ETag的用武之地。
可以將ETag定義為唯一的字串值,該值在該時間點明確地標識資源。 通常,伺服器根據給定資源的屬性計算ETag,或者,如果可用,則計算其最新修改日期。
客戶端和伺服器之間的通訊流程與修改日期檢查的情況幾乎相同。只有標題的名稱和值不同。
伺服器在名為ETag 的標題中設定ETag值。當客戶端再次訪問資源時,它應該在名為If-None-Match 的頭中傳送其值。如果該值與資源的新計算的ETag匹配,則伺服器可以使用空內容和HTTP程式碼304進行響應。
在Spring中,您可以實現ETag伺服器流程,如下所示:
@GetMapping(<font>"/{id}"</font><font>) ResponseEntity<Product> getProduct(@PathVariable <b>long</b> id, WebRequest request) { Product product = repository.find(id); String modificationDate = product.getModificationDate().toString(); String eTag = DigestUtils.md5DigestAsHex(modificationDate.getBytes()); <b>if</b> (request.checkNotMod<b>if</b>ied(eTag)) { <b>return</b> <b>null</b>; } <b>return</b> ResponseEntity.ok() .eTag(eTag) .body(product); } </font>
與前一個樣本幾乎相同,並且修改日期檢查。我們只是使用不同的值進行比較(以及MD5演算法來計算ETag)。請注意, WebRequest 有一個過載的 checkNotModified() 方法來處理表示為字串的ETag。
如果Last-Modified 和ETag 工作幾乎相同,為什麼我們需要兩者嗎?
Last-Modified vs ETag
正如我已經提到的,Last-Modified 標頭不太精確, 因為它具有一秒的精度。為了獲得更高的精度,請選擇ETag 。
當您不跟蹤 資源的修改日期 時,您也被迫使用ETag 。伺服器可以根據資源的屬性計算其值。將其視為物件的雜湊碼。
如果資源具有其修改日期並且您可以使用一秒精度,請使用Last-Modified 標頭。為什麼?因為ETag計算可能是一項昂貴的操作 。
順便提一下,值得一提的是HTTP協議沒有指定用於計算ETag的演算法。選擇演算法時,您應該關注它的速度。
本文重點介紹快取GET請求,但您應該知道伺服器可以使用ETag 來同步更新操作。
Spring ETag過濾器
因為ETag只是內容的字串表示,所以伺服器可以使用響應的位元組表示來計算其值。意思是你可以實際將ETag分配給任何響應。
Spring框架為您提供了ETag響應過濾器實現 ,它可以為您完成。您所要做的就是在應用程式中配置過濾器。
在Spring應用程式中新增HTTP過濾器的最簡單方法是通過配置類中的FilterRegistrationBean 。
@Bean <b>public</b> FilterRegistrationBean filterRegistrationBean () { ShallowEtagHeaderFilter eTagFilter = <b>new</b> ShallowEtagHeaderFilter(); FilterRegistrationBean registration = <b>new</b> FilterRegistrationBean(); registration.setFilter(eTagFilter); registration.addUrlPatterns(<font>"/*"</font><font>); <b>return</b> registration; } </font>
在這種情況下,對addUrlPatterns() 的呼叫是多餘的,因為預設情況下所有路徑都匹配。我把它放在這裡證明你可以控制Spring應該新增ETag值的資源。
除了ETag生成之外,過濾器還會在可能的情況下響應HTTP 304和空體內容。
但要注意。
ETag計算可能很昂貴。對於某些應用程式啟用此過濾器實際上可能會導致弊大於利 。在使用之前考慮一下您的解決方案。
結論
現在您已瞭解如何使用HTTP快取優化應用程式,哪種方法最適合您,因為應用程式有不同的需求。
您瞭解到客戶端快取驗證是最有效的方法,因為不涉及資料傳輸。在適用時,您應該始終支援客戶端快取驗證。
我們還討論了伺服器端驗證並比較了Last-Modified 和ETag 標頭。最後,您瞭解瞭如何在Spring應用程式中設定全域性ETag過濾器。