1. 概要

このチュートリアルでは、HTTPキャッシングについて学習します。 また、クライアントとSpringMVCアプリケーションの間にこのメカニズムを実装するさまざまな方法についても見ていきます。

2. HTTPキャッシングの紹介

ブラウザでWebページを開くと、通常、Webサーバーから多くのリソースがダウンロードされます。

たとえば、この例では、ブラウザは1つの/ログインページに対して3つのリソースをダウンロードする必要があります。 ブラウザがすべてのWebページに対して複数のHTTPリクエストを作成するのは一般的です。 このようなページを頻繁にリクエストすると、ネットワークトラフィックが多くなり、これらのページの配信に時間がかかります

ネットワークの負荷を軽減するために、HTTPプロトコルでは、ブラウザがこれらのリソースの一部をキャッシュすることができます。 有効にすると、ブラウザはリソースのコピーをローカルキャッシュに保存できます。 その結果、ブラウザは、ネットワーク経由で要求する代わりに、ローカルストレージからこれらのページを提供できます。

Webサーバーは、応答に Cache-Control ヘッダーを追加することにより、特定のリソースをキャッシュするようにブラウザーに指示できます。

リソースはローカルコピーとしてキャッシュされるため、ブラウザから古いコンテンツを提供するリスクがあります。 したがって、Webサーバーは通常、Cache-Controlヘッダーに有効期限を追加します。

次のセクションでは、SpringMVCコントローラーからの応答にこのヘッダーを追加します。 後で、有効期限に基づいてキャッシュされたリソースを検証するSpringAPIも表示されます。

3. コントローラの応答のCache-Control

3.1. ResponseEntityを使用する

これを行う最も簡単な方法は、 Springが提供するCacheControlビルダークラスを使用することです。

@GetMapping("/hello/{name}")
@ResponseBody
public ResponseEntity<String> hello(@PathVariable String name) {
    CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS)
      .noTransform()
      .mustRevalidate();
    return ResponseEntity.ok()
      .cacheControl(cacheControl)
      .body("Hello " + name);
}

これにより、応答にCache-Controlヘッダーが追加されます。

@Test
void whenHome_thenReturnCacheHeader() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/baeldung"))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.header()
        .string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}

3.2. HttpServletResponseを使用する

多くの場合、コントローラーはハンドラーメソッドからビュー名を返す必要があります。 ただし、 ResponseEntityクラスでは、ビュー名を返すと同時にリクエスト本文を処理することはできません

または、このようなコントローラーの場合、HttpServletResponseCache-Controlヘッダーを直接設定できます。

@GetMapping(value = "/home/{name}")
public String home(@PathVariable String name, final HttpServletResponse response) {
    response.addHeader("Cache-Control", "max-age=60, must-revalidate, no-transform");
    return "home";
}

これにより、前のセクションと同様に、HTTP応答にCache-Controlヘッダーも追加されます。

@Test
void whenHome_thenReturnCacheHeader() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get("/home/baeldung"))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.header()
        .string("Cache-Control","max-age=60, must-revalidate, no-transform"))
      .andExpect(MockMvcResultMatchers.view().name("home"));
}

4. 静的リソースのCache-Control

一般的に、Spring MVCアプリケーションは、HTML、CSS、JSファイルなどの多くの静的リソースを提供します。 このようなファイルは多くのネットワーク帯域幅を消費するため、ブラウザがそれらをキャッシュすることが重要です。 応答のCache-Controlヘッダーを使用して、これを再度有効にします。

Springを使用すると、リソースマッピングでこのキャッシュ動作を制御できます。

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
      .setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS)
        .noTransform()
        .mustRevalidate());
}

これにより、 /resourcesで定義されたすべてのリソースが応答のCache-Controlヘッダーとともに返されるようになります。

5. インターセプターのCache-Control

Spring MVCアプリケーションインターセプターを使用して、すべてのリクエストに対して前処理と後処理を行うことができます。 これは、アプリケーションのキャッシュ動作を制御できるもう1つのプレースホルダーです。

カスタムインターセプターを実装する代わりに、Springが提供するWebContentInterceptorを使用します。

@Override
public void addInterceptors(InterceptorRegistry registry) {
    WebContentInterceptor interceptor = new WebContentInterceptor();
    interceptor.addCacheMapping(CacheControl.maxAge(60, TimeUnit.SECONDS)
      .noTransform()
      .mustRevalidate(), "/login/*");
    registry.addInterceptor(interceptor);
}

ここでは、 WebContentInterceptor を登録し、最後のいくつかのセクションと同様にCache-Controlヘッダーを追加しました。 特に、さまざまなURLパターンに対してさまざまなCache-Controlヘッダーを追加できます。

上記の例では、 / login で始まるすべてのリクエストに対して、次のヘッダーを追加します。

@Test
void whenInterceptor_thenReturnCacheHeader() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get("/login/baeldung"))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.header()
        .string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}

6. SpringMVCでのキャッシュ検証

これまで、応答にCache-Controlヘッダーを含めるさまざまな方法について説明してきました。 これは、max-ageなどの構成プロパティに基づいてリソースをキャッシュするクライアントまたはブラウザーを示します。

通常、各リソースにキャッシュの有効期限を追加することをお勧めします。 その結果、ブラウザはキャッシュから期限切れのリソースを提供することを回避できます。

ブラウザは常に有効期限をチェックする必要がありますが、毎回リソースを再フェッチする必要はない場合があります。 ブラウザがサーバー上でリソースが変更されていないことを検証できる場合、ブラウザはキャッシュされたバージョンのリソースを引き続き提供できます。 そしてこの目的のために、HTTPは2つの応答ヘッダーを提供します。

  1. Etag –キャッシュされたリソースがサーバー上で変更されたかどうかを判断するための一意のハッシュ値を格納するHTTP応答ヘッダー–対応するIf-None-Matchリクエストヘッダーは最後のEtagを保持する必要があります価値
  2. LastModified –リソースが最後に更新された時刻の単位を格納するHTTP応答ヘッダー–対応する If-Unmodified-Since リクエストヘッダーには、最終変更日が含まれている必要があります

これらのヘッダーのいずれかを使用して、期限切れのリソースを再フェッチする必要があるかどうかを確認できます。ヘッダーを検証した後、 サーバーはリソースを再送信するか、304HTTPコードをに送信できます。変更なしを意味します。 後者のシナリオでは、ブラウザはキャッシュされたリソースを引き続き使用できます。

LastModified ヘッダーは、秒単位の精度までの時間間隔のみを保存できます。 これは、より短い有効期限が必要な場合の制限になる可能性があります。 このため、代わりにEtagを使用することをお勧めします。 Etagヘッダーはハッシュ値を格納するため、ナノ秒などのより細かい間隔まで一意のハッシュを作成できます。

そうは言っても、それがどのように使用されるかを確認しましょう最終更新日。

Springには、リクエストに有効期限ヘッダーが含まれているかどうかを確認するためのユーティリティメソッドがいくつか用意されています。

@GetMapping(value = "/productInfo/{name}")
public ResponseEntity<String> validate(@PathVariable String name, WebRequest request) {
 
    ZoneId zoneId = ZoneId.of("GMT");
    long lastModifiedTimestamp = LocalDateTime.of(2020, 02, 4, 19, 57, 45)
      .atZone(zoneId).toInstant().toEpochMilli();
     
    if (request.checkNotModified(lastModifiedTimestamp)) {
        return ResponseEntity.status(304).build();
    }
     
    return ResponseEntity.ok().body("Hello " + name);
}

Springは、 checkNotModified()メソッドを提供して、最後のリクエスト以降にリソースが変更されているかどうかを確認します。

@Test
void whenValidate_thenReturnCacheHeader() throws Exception {
    HttpHeaders headers = new HttpHeaders();
    headers.add(IF_UNMODIFIED_SINCE, "Tue, 04 Feb 2020 19:57:25 GMT");
    this.mockMvc.perform(MockMvcRequestBuilders.get("/productInfo/baeldung").headers(headers))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().is(304));
}

7. 結論

この記事では、SpringMVCのCache-Control応答ヘッダーを使用したHTTPキャッシングについて学習しました。 ResponseEntity クラスを使用するか、静的リソースのリソースマッピングを使用して、コントローラーの応答にヘッダーを追加できます。

Springインターセプターを使用して、特定のURLパターンにこのヘッダーを追加することもできます。

いつものように、コードはGitHubから入手できます。