Spring MVC(+Spring Boot)上での静的リソースへのアクセスを理解する

今回は、Spring MVC上で静的リソース（HTML、JavaScript、CSS、画像など）にアクセスする方法について説明します。ここでは、「Jakarta EE(Java EE)のアプリケーションサーバーの機能を使用してアクセスする方法」と「Spring MVCの独自機能を使用してアクセスする方法」について説明します。また、最後にSpring Bootアプリでアクセスする方法についても紹介します。

# 動作検証バージョン

* Spring Framework **5.3.6** ~~(4.2.5.RELEASE)~~
* Spring Boot **2.4.5** ~~(1.3.3.RELEASE)~~
* Tomcat 9.0.45

> **Note:**
>
> **[2021/5/5]**
投稿から5年くらいたっても一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。今回の更新では、`web.xml`は使わずにJava Configを使うサンプルを修正しています。また検証コードをGitHubで公開するようにしました。

# 検証アプリケーション

GitHub上で公開するようにしました。

* https://github.com/kazuki43zoo/spring-mvc-static-resources

# アプリケーションサーバーの機能を使用した静的リソースへのアクセス
まずは、Jakarta EE(Java EE)準拠のWebアプリケーションでどのように静的リソースを扱うか説明（おさらい）しておきましょう。Jakarta EE(Java EE)準拠のWebアプリケーションでは、静的リソースはドキュメントルート上の任意のディレクトリに格納します。 ドキュメントルートは、MavenやGradleプロジェクトではあれば`src/main/webapp`になります。

* Webアプリケーションのドキュメントルート上への格納例

```
Project Root
└─src
    └─ main
        └─ webapp  # Webアプリケーションのドキュメントルート
            └─ static
                └─ css
                    └─ app.css
```

例えば、上記のようなディレクトリにCSSファイルを格納した場合は、`http://localhost:8080/(context-path/)static/css/app.css`というパスでアクセスすることができます。
使用するアプリケーションサーバーがTomcatの場合は、`org.apache.catalina.servlets.DefaultServlet`というサーブレットを介してWebアプリケーション内の静的リソースにアクセスします。Tomcat以外の主要なアプリケーションサーバーでも類似のサーブレットが用意されており、これらのサーブレットのことを「**デフォルトサーブレット**」と呼びます。なお、「デフォルトサーブレット」はルートパス(「`/`」)にマッピングされています。

![javaeeStaticResources.png](https://qiita-image-store.s3.amazonaws.com/0/117313/f3aa4001-c578-9403-1884-a8a60ded5818.png)

> **Note: Servlet 3.0+ でのドキュメントルート**
> Servlet 3.0より、warファイル内のjarファイルの中にある「`META-INF/resources`」ディレクトリもWebアプリケーションのドキュメントルートとして扱われるようになっています。
>
> ```
> warファイル
> └─ WEB-INF
>     └─ lib
>         └─ foo.jar
>             └─ META-INF
>                 └─ resources # Webアプリケーションのドキュメントルート
>                     └─ static
>                         └─ css
>                             └─ foo.css
> ```
> 例えば、上記のようなディレクトリにCSSファイルを格納した場合は、`http://localhost:8080/(context-path/)static/css/foo.css`というURLでアクセスすることができます。

## Spring MVCのDispatcherServletとデフォルトサーブレットの関係

Spring MVCでは、`org.springframework.web.servlet.DispatcherServlet`で全てのリクエストを受け、リクエスト内容に対応するHandler(ControllerのHandlerメソッド)にディスパッチする仕組みになっており、多くのSpring MVCアプリケーションで`DispatcherServlet`をルートパスにマッピングしています。

```java
package com.example.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

  @Override
  protected String[] getServletMappings() {
    return new String[]{"/"}; // ルートパスにマッピング
  }

}
```

```java
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc // Spring MVCの機能を有効化
public class WebMvcConfig {
}
```

`DispatcherServlet`をルートパスにマッピングすると、アプリケーションサーバーが提供している「デフォルトサーブレット」が呼び出されなくなり、その結果として、Webアプリケーションのドキュメントルート上から静的リソースを取得できなくなります。

![javaeeStaticResourcesWithSpringMvc.png](https://qiita-image-store.s3.amazonaws.com/0/117313/55000947-f423-20f4-d8c6-f1a5438a8af6.png)

## DispatcherServletとデフォルトサーブレットの併用

`DispatcherServlet`をルートパスにマッピングしつつ、デフォルトサーブレット経由でドキュメントルート上の静的リソースにアクセスすることはできないのでしょうか？
（もちろん）できます！！ `DispatcherServlet`とデフォルトサーブレットを併用したい場合は、`DispatcherServlet`で受けたリクエストをデフォルトサーブレットにフォワードする機能(`org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler`)を有効にしてください。

```java
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc // Spring MVCの機能を有効化 （注意：Spring Bootの場合は、@EnableWebMvcはつけちゃダメ！！）
public class WebMvcConfig implements WebMvcConfigurer {
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable(); // デフォルトサーブレットへの転送機能を有効化
  }
}
```

`DispatcherServlet`で受けたリクエストをデフォルトサーブレットにフォワードする機能を有効にすると、以下のような流れで静的リソースにアクセスすることができます。

```text:webappのドキュメントルートから取得
% curl -D - http://localhost:8080/pure-spring-demo/static/css/app.css
HTTP/1.1 200 
Accept-Ranges: bytes
ETag: W/"37-1620131018000"
Last-Modified: Tue, 04 May 2021 12:23:38 GMT
Content-Type: text/css
Content-Length: 37
Date: Tue, 04 May 2021 13:37:19 GMT

body {
    background-color: black;
}
```

```text:jar内のドキュメントルートから取得
% curl -D - http://localhost:8080/pure-spring-demo/static/css/foo.css
HTTP/1.1 200 
Accept-Ranges: bytes
ETag: W/"35-1620135326000"
Last-Modified: Tue, 04 May 2021 13:35:26 GMT
Content-Type: text/css
Content-Length: 35
Date: Tue, 04 May 2021 13:37:24 GMT

body {
    background-color: red;
}
```

> **Warning: Spring BootでWebMvcConfigurerを利用する際の注意点**
>
> Spring Bootで`WebMvcConfigurer`の実装クラスを作成する場合は、`@EnableWebMvc`は絶対につけないでください。`@EnableWebMvc`をつけてしまうと、Spring BootのAutoConfigureのコンフィギュレーションが一部無効になってしまいます。これは[Spring Bootのリファレンス](http://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration)にも記述されています。

![javaeeStaticResourcesViaSpringMvc.png](https://qiita-image-store.s3.amazonaws.com/0/117313/cc1e3d01-64dd-a067-6ed0-23610ce06c7f.png)

# Spring MVCの独自機能を利用した静的リソースへのアクセス

ここまではアプリケーションサーバーの機能を使用して静的リソースへアクセスする方法を紹介しましたが、ここからは、Spring MVCが独自に提供する機能を使用して静的リソースへアクセスする方法を紹介していきます。

Spring MVCは、静的リソースにアクセスするためのHandlerとして`org.springframework.web.servlet.resource.ResourceHttpRequestHandler`というクラスを提供しています。`ResourceHttpRequestHandler`を利用すると、

* `DispatcherServlet`を経由での静的リソースへのアクセス
* リソース毎(リソースのパターン毎)のキャッシュ制御
* クラスパスや任意のディレクトリに格納されているファイルへのアクセス
* バージョン付きのパス経由での静的リソースへのアクセス
* Gzipなどで圧縮された静的リソースへのアクセス
* WebJar内の静的リソースへのアクセス

などを実現することができます。

![springStaticResources.png](https://qiita-image-store.s3.amazonaws.com/0/117313/602ee17e-aa61-8bd2-b138-89b35c59673f.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/02965728-de54-e724-4258-90236464ca22.png)


以下は、クラスパス上のファイルを静的リソースとして扱う場合のBean定義例です。

```java
@Configuration
@EnableWebMvc // 注意：Spring Bootの場合は、@EnableWebMvcはつけちゃダメ！！
public class WebMvcConfig implements WebMvcConfigurer {
  // ...(省略)...
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 「/resources/**」配下へのアクセスを受けた際にはクラスパス配下の「/static/**」から該当リソースを見つける
    registry.addResourceHandler("/resources/**").addResourceLocations("classpath:/static/"); 
  }
}

```

```
Project Root
└─src
    └─ main
        └─ resources  # クラスパス
            └─ static
                └─ css
                    └─ bar.css
```


例えば、上記のようなディレクトリにCSSファイルを格納した場合は、`http://localhost:8080/(context-path/)resources/css/bar.css`というパスでアクセスすることができ、以下のようなレスポンスヘッダーが設定されます。

```text
% curl -D - http://localhost:8080/pure-spring-demo/resources/css/bar.css
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 04 May 2021 13:35:28 GMT
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 36
Date: Tue, 04 May 2021 13:37:29 GMT

body {
    background-color: blue;
}
```

## キャッシュの有効期間の設定

デフォルトの動作では、キャッシュの有効期間は設定されないため、キャッシュに関する動作はブラウザの仕様に依存します。キャッシュの有効期間を設定したい場合は、明示的に有効期間を指定してください。有効期間を指定すると、`Cache-Control`ヘッダーの`max-age`属性に値が設定されます。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
        .addResourceLocations("classpath:/static/")
        .setCachePeriod(604800); // 有効期間(秒単位)の設定 604800=1週間
}
```

該当リソースにアクセスすると、以下のようなレスポンスヘッダーが設定されます。

```text
 % curl -D - http://localhost:8080/pure-spring-demo/resources/css/bar.css
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 04 May 2021 13:46:30 GMT
Cache-Control: max-age=604800
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 36
Date: Tue, 04 May 2021 13:47:27 GMT

body {
    background-color: blue;
}
```

キャッシュの有効期間を設定すると、有効期間内であればブラウザにキャッシュされているコンテンツが利用され、有効期間を超えている場合は、サーバー(`ResourceHttpRequestHandler`)への問い合わせが行われます。
`ResourceHttpRequestHandler`は、コンテンツが更新されていれば更新後のコンテンツを応答し、更新されていなければ304(Not Modified)を応答します。

> **Note: 有効期間に0を指定すると・・・**
> 有効期間に0を設定すると、Cache-Controlに`no-store`属性が設定されます。
>
> ```text
>% curl -D - http://localhost:8080/pure-spring-demo/resources/css/bar.css   
>HTTP/1.1 200 
>Vary: Origin
>Vary: Access-Control-Request-Method
>Vary: Access-Control-Request-Headers
>Last-Modified: Tue, 04 May 2021 13:51:30 GMT
>Cache-Control: no-store
>Accept-Ranges: bytes
>Content-Type: text/css
>Content-Length: 36
>Date: Tue, 04 May 2021 13:51:45 GMT
>
>body {
>    background-color: blue;
>}
>```

## Cache-Controlの設定

有効期間(`max-age`)以外のキャッシュ制御の設定値を指定することもできます。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
        .addResourceLocations("classpath:/static/")
        .setCacheControl(
            CacheControl.maxAge(Duration.ofDays(7))
                .sMaxAge(Duration.ofDays(14))
                .staleIfError(Duration.ofDays(1))
                .cachePrivate()
                .cachePublic()
                .mustRevalidate()
                .noTransform()
                .proxyRevalidate());
}
```

該当リソースにアクセスすると、以下のようなレスポンスヘッダーが設定されます。

```text
% curl -D - http://localhost:8080/pure-spring-demo/resources/css/bar.css
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 04 May 2021 14:00:14 GMT
Cache-Control: max-age=604800, must-revalidate, no-transform, public, private, proxy-revalidate, s-maxage=1209600, stale-if-error=86400
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 36
Date: Tue, 04 May 2021 14:00:31 GMT

body {
    background-color: blue;
}
```

## Last-Modifiedヘッダの無効化

Spring 5.3よりレスポンスヘッダに「Last-Modified」を付与しないオプションがサポートされました。これは、どうやらSpring Initializrの静的ファイルを変更したのにもかかわらず、キャッシュが使われてしまう問題を解決するために追加されたみたいです。

* https://github.com/spring-io/start.spring.io/issues/519
* https://github.com/spring-projects/spring-framework/issues/25845
* https://github.com/spring-projects/spring-boot/issues/24099

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  registry.addResourceHandler("/resources/**")
      .addResourceLocations("classpath:/static/")
      .setCachePeriod(604800)
      .setUseLastModified(false); // Last-Modifiedヘッダの出力を無効化
}
```

```text
% curl -D - http://localhost:8080/pure-spring-demo/resources/css/bar.css
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Cache-Control: max-age=604800
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 36
Date: Tue, 04 May 2021 14:56:07 GMT

body {
    background-color: blue;
}
```

## 静的リソースのバージョニング

Spring MVCの機能を利用すると、静的リソースにバージョンを付与することができます。
Built-inでサポートされているバージョニング方法は、以下の2種類です。

### コンテンツデータのMD5ハッシュ値によるバージョニング

この方法は、静的リソースのコンテンツデータ(バイト配列)をMD5のハッシュ値にした値をバージョン番号として扱い、バージョン番号をファイル名に含めます。つまり、ファイルの中身がかわると別のバージョンとして認識されます。たとえば、オリジナルのリソースパスが`/resources/css/fuga-content.css`の場合は、`/resources/css/fuga-content-bb5ea6c2162e206832eda9e911dc6406.css`という感じのリソースパスになります。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  registry.addResourceHandler("/resources/**")
      .addResourceLocations("classpath:/static/")
      .setCachePeriod(604800)
      .resourceChain(true) // プロダクション環境ではtrue/開発環境ではfalseがおすすめ
      .addResolver(new VersionResourceResolver()
          .addContentVersionStrategy("/**/*-content*.css") // コンテンツデータのMD5ハッシュ値によるバージョニング機能の有効化
      );
}
```

```
Project Root
└─src
    └─ main
        └─ resources  # クラスパス
            └─ static
                └─ css
                    └─ fuga-content.css
```

```text
% curl -D - http://localhost:8080/pure-spring-demo/resources/css/fuga-content-bb5ea6c2162e206832eda9e911dc6406.css
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 04 May 2021 15:30:00 GMT
Cache-Control: max-age=604800
ETag: W/"bb5ea6c2162e206832eda9e911dc6406"
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 42
Date: Tue, 04 May 2021 15:32:53 GMT

body {
    background-color: blueviolet;
}
```

### 指定した固定バージョンによるバージョニング

この方法は、指定した固定値をバージョン番号として扱い、バージョン番号はディレクトリになります。たとえば、オリジナルのリソースパスが`/resources/css/hoge-fix.css`の場合は、`/resources/v1.0.0/css/hoge-fix.css`という感じのリソースパスになります。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  registry.addResourceHandler("/resources/**")
      .addResourceLocations("classpath:/static/")
      .setCachePeriod(604800)
      .resourceChain(true) // プロダクション環境ではtrue/開発環境ではfalseがおすすめ
      .addResolver(new VersionResourceResolver()
            .addFixedVersionStrategy("v1.0.0", "/**/*-fix.css") // 指定した固定バージョンによるバージョニング機能の有効化
      );
}
```

```
Project Root
└─src
    └─ main
        └─ resources  # クラスパス
            └─ static
                └─ css
                    └─ hoge-fix.css
```

```text
% curl -D - http://localhost:8080/pure-spring-demo/resources/v1.0.0/css/hoge-fix.css
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 04 May 2021 15:30:00 GMT
Cache-Control: max-age=604800
ETag: W/"v1.0.0"
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 36
Date: Tue, 04 May 2021 15:36:33 GMT

body {
    background-color: aqua;
}
```

### Viewとバージョニング機能の連携

バージョニングされたリソースパスをJSPなどのView内で扱うためには、サーブレットフィルターとして`org.springframework.web.servlet.resource.ResourceUrlEncodingFilter`を登録してください。
`ResourceUrlEncodingFilter`を使用すると、オリジナルのリソースパスからバージョニングされたリソースパスを解決することができます。

```java
public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  // ...(省略)...
  @Override
  protected Filter[] getServletFilters() {
    return new Filter[]{new ResourceUrlEncodingFilter()}; // フィルタを登録
  }
  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    servletContext.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE)); // 本エントリーとは直接関係ないがセッションIDをCookieのみで扱うように設定を変更
    super.onStartup(servletContext);
  }
}
```

ViewとしてJSPを使用する場合は、JSTLの`<c:url>`やSpringの`<spring:url>`を使用してURLを生成すると、バージョニングされたリソースパスを出力することができます。なお、ViewとしてThymeleaf、Freemarker、Velocityなどを使用する場合でも、JSPと同様の仕組みがサポートされています。

```jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
    <link href="<c:url value='/static/css/app.css'/>" type="text/css" rel="stylesheet"/>
    <link href="<c:url value='/static/css/foo.css'/>" type="text/css" rel="stylesheet"/>
    <link href="<c:url value='/resources/css/bar.css'/>" type="text/css" rel="stylesheet"/>
    <link href="<c:url value='/resources/css/hoge-fix.css'/>" type="text/css" rel="stylesheet"/>
    <link href="<c:url value='/resources/css/fuga-content.css'/>" type="text/css" rel="stylesheet"/>
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

上記のJSPから、以下のようなHTMLが出力されます。

```html
<html>
<head>
    <link href="/pure-spring-demo/static/css/app.css" type="text/css" rel="stylesheet"/>
    <link href="/pure-spring-demo/static/css/foo.css" type="text/css" rel="stylesheet"/>
    <link href="/pure-spring-demo/resources/css/bar.css" type="text/css" rel="stylesheet"/>
    <link href="/pure-spring-demo/resources/v1.0.0/css/hoge-fix.css" type="text/css" rel="stylesheet"/>
    <link href="/pure-spring-demo/resources/css/fuga-content-bb5ea6c2162e206832eda9e911dc6406.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

また、CSSファイル内に指定しているURL(別のCSSファイルをインポートするためのURLや、画像ファイルのURLなど)も、バージョニングされたリソースパスに変換されます。

```css
@import url(fw-content.css);

body {
    background-color: blueviolet;
}
```

上記のCSSにアクセスすると、クライアントにはバージョニングされたリソースパスに置き換わってレスポンスされます。

```css
@import url(/pure-spring-demo/resources/css/fw-content-4d1801fd7cdbc0802def3501211925ef.css);

body {
    background-color: blueviolet;
}
```


## WebJarの利用

OSSのCSSライブラリやJavaScriptライブラリを使用する場合は、WebJarを使うこともあると思います。ここでは、Spring MVCの機能を使ってBootstrapのWebJarを利用する方法を紹介します。

まず、BootstrapのWebJarを依存ライブラリに追加します。

```xml:pom.xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.6.0-1</version>
</dependency>
```

次に、WebJarから静的リソースを取得するための設定を行います。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  // ...
  registry.addResourceHandler("/webjars/**")
      .addResourceLocations("classpath:/META-INF/resources/webjars/");
}
```

最後に、ViewからWebJar内のリソースにアクセスします。

```jsp
<html>
<head>
    <link href="<c:url value='/webjars/bootstrap/4.6.0-1/css/bootstrap.min.css'/>" type="text/css" rel="stylesheet" />
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

### ライブラリのバージョン番号の隠蔽

WebJarから静的リソースにアクセスすることはできましたが、View内でライブラリのバージョン番号を意識してしまっています。このままだとライブラリのバージョンアップした時にViewも修正する必要があるため、メンテナンス性がよくありません。ここでは、View内でライブラリのバージョン番号を意識させない方法を紹介しましょう。

まず、依存ライブラリとして`org.webjars:webjars-locator`を追加します。

```xml:pom.xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator</artifactId>
    <version>0.40</version>
</dependency>
```

次に、WebJarのバージョン番号を隠蔽する機能(`org.springframework.web.servlet.resource.WebJarsResourceResolver`)を有効化します。
なお、`ResourceUrlEncodingFilter`の登録も必要です。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  // ...(省略)...
  registry.addResourceHandler("/webjars/**")
      .addResourceLocations("classpath:/META-INF/resources/webjars/")
      .resourceChain(true); // プロダクション環境ではtrue/開発環境ではfalseがおすすめ + 自動でWebJarsResourceResolverが有効化される
}
```

最後に、ViewからWebJar内のリソースにバージョンを省いてアクセスすると、

```jsp
<html>
<head>
    <link href="<c:url value='/webjars/bootstrap/css/bootstrap.min.css'/>" type="text/css" rel="stylesheet" />
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

バージョン付きのパスに変換されます。

```html
<html>
<head>
    <link href="/pure-spring-demo/webjars/bootstrap/4.6.0-1/css/bootstrap.min.css" type="text/css" rel="stylesheet" />
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

## Gzipなどで圧縮された静的リソースへのアクセス

OSSのCSSライブラリやJavaScriptライブラリの中には、テキストベースのファイルとは別にGzipなどでエンコーディングされたファイルを同封していることがあります。静的リソースとしてGzip化されたファイルを使用すると、データの転送量を減らすことができるため、画面の表示速度が速くなることが期待できます。Spring MVCでも、Gzip化されたファイルへアクセスする機能(`org.springframework.web.servlet.resource.EncodedResourceResolver`)を提供しています。

> **NOTE:**
>
> Spring 5.1より`EncodedResourceResolver`が追加され、`GzipResourceResolver`は非推奨APIになっています。

```java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
  // ...(省略)...
  registry.addResourceHandler("/webjars/**")
      .addResourceLocations("classpath:/META-INF/resources/webjars/")
      .resourceChain(true); // プロダクション環境ではtrue/開発環境ではfalseがおすすめ
      .addResolver(new EncodedResourceResolver());
}
```

以下に、OSSのCSSライブラリのひとつであるBootstrapのWebJarを例に、`EncodedResourceResolver `を適用する時の実装例を紹介します。

```
WebJar(bootstrap-4.6.0-1.jar)
└─ META-INF
    └─ resources
        └─ webjars
            └─ bootstrap
                └─ 4.6.0-1
                    ├─ css
                    |   ├─ bootstrap.min.css
                    |   ├─ bootstrap.min.css.gz # Gzip化されたファイル
                    ...
```

Viewの実装では、Gzipファイルを意識する必要はありません。

```jsp
<html>
<head>
    <link href="<c:url value='/webjars/bootstrap/css/bootstrap.min.css'/>" type="text/css" rel="stylesheet" />
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

上記のView(JSP)から生成されるHTMLは、以下のようになります。

```html
<html>
<head>
    <link href="/pure-spring-demo/webjars/bootstrap/4.6.0-1/css/bootstrap.min.css" type="text/css" rel="stylesheet" />
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

HTMLだけ見るとGzip化されたファイルが使われているかわかりませんが、HTTPレスポンスのヘッダー情報を見るとGzip化されたファイルが使われていることがわかります。`Content-Encoding`ヘッダー値が`gzip`、`Content-Length`ヘッダー値がGzipファイルのサイズ(約24KB)になっています。

```text
% curl -D - http://localhost:8080/pure-spring-demo/webjars/bootstrap/4.6.0-1/css/bootstrap.min.css -H "Accept-Encoding: gzip"
HTTP/1.1 200 
Vary: Accept-Encoding
Content-Encoding: gzip
Last-Modified: Tue, 04 May 2021 17:02:04 GMT
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 24105
Date: Tue, 04 May 2021 17:27:29 GMT

Warning: Binary output can mess up your terminal. Use "--output -" to tell 
Warning: curl to output it to your terminal anyway, or consider "--output 
Warning: <FILE>" to save to a file.
```

なお、`GzipResourceResolver`を使用しない場合のHTTPレスポンスのヘッダー情報は、以下のようになります。`Content-Encoding`ヘッダーは出力されず、`Content-Length`ヘッダー値がテキストファイルのサイズ(約161KB)になっています。

```text
% curl -D - http://localhost:8080/pure-spring-demo/webjars/bootstrap/4.6.0-1/css/bootstrap.min.css                           
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Last-Modified: Tue, 04 May 2021 17:02:04 GMT
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 161409
Date: Tue, 04 May 2021 17:29:55 GMT

...
```

# Spring Boot上での静的リソースへのアクセス

Spring Bootは、Spring MVCの機能を使って静的リソースにアクセスします。
Spring Bootを使用すると、これまで説明してきた設定をSpring Bootの自動コンフィギュレーションの仕組みを使って行うことができます。

## 静的リソースの格納先

静的リソースは、クラスパス上の「`/static`」「`/public`」「`/resources`」「`/META-INF/resources`」の配下に格納することができます。

```
Project Root
└─src
    └─ main
        └─ resources  # クラスパス
            ├─ static
            |   └─ css
            |       └─ c.css
            ├─ public
            |   └─ css
            |       └─ a.css
            └─ resources
                └─ css
                    └─ b.css

Executable JAR
└─ BOOT-INF
     └─ lib
         └─ foo.jar
             └─ META-INF
                 └─ resources
                     └─ static
                         └─ css
                             └─ foo.css

```

例えば上記のような構成でファイルを格納すると、それぞれ以下のURLでアクセスすることができます。

* `http://localhost:8080/css/a.css`
* `http://localhost:8080/css/b.css`
* `http://localhost:8080/css/c.css`
* `http://localhost:8080/static/css/foo.css`

静的リソースの格納先を変更したい場合は、以下のような設定をプロパティファイルに指定してください。

```properties:src/main/resources/application.properties
spring.web.resources.static-locations=classpath:/contents/
```

## キャッシュの有効期間の設定

キャッシュの有効期間を設定する場合は、以下の設定をプロパティファイルに指定してください。

```properties:src/main/resources/application.properties
spring.web.resources.cache.period=7d
```

## Cache-Controlの設定

有効期間(max-age)以外のキャッシュ制御の設定値は、`spring.web.resources.cache.cachecontrol`で始まるプロパティで指定することもできます。

```properties:src/main/resources/application.properties
spring.web.resources.cache.cachecontrol.max-age=7d
spring.web.resources.cache.cachecontrol.s-max-age=14d
spring.web.resources.cache.cachecontrol.stale-if-error=1d
spring.web.resources.cache.cachecontrol.cache-private=true
spring.web.resources.cache.cachecontrol.cache-public=true
spring.web.resources.cache.cachecontrol.must-revalidate=true
spring.web.resources.cache.cachecontrol.no-transform=true
spring.web.resources.cache.cachecontrol.proxy-revalidate=true
```

## Last-Modifiedヘッダの無効化

Spring Boot 2.4よりレスポンスヘッダに「Last-Modified」を付与しないオプションをプロパティがサポートされています。背景は「[こちら](#last-modifiedヘッダの無効化)」を参照してください。

```properties:src/main/resources/application.properties
spring.web.resources.cache.use-last-modified=false
```

> **WARNING:**
>
> Spring Boot 2.4.2〜2.4.5だと、このプロパティが効かない模様で、これはSpring Boot 2.4.6で改修される予定です。
>
> * https://github.com/spring-projects/spring-boot/issues/26212

## 静的リソースのバージョニング

Spring Bootでも静的リソースのバージョニングが可能です。

### コンテンツデータのMD5ハッシュ値によるバージョニング

コンテンツデータのMD5ハッシュ値によるバージョニングを利用する場合は、以下の設定をプロパティファイルに指定してください。

```properties:src/main/resources/application.properties
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**/*-content*.css
```

### 指定した固定バージョンによるバージョニング

指定した固定バージョンによるバージョニングを利用する場合は、以下の設定をプロパティファイルに指定してください。


```properties:src/main/resources/application.properties
spring.web.resources.chain.strategy.fixed.enabled=true
spring.web.resources.chain.strategy.fixed.version=v1.0.0
spring.web.resources.chain.strategy.fixed.paths=/**/*-fix.css
```

### Viewとバージョニング機能の連携

ViewとしてThymeleaf/Velocity/Freemarkerを使う場合は、Spring Bootの自動コンフィギュレーションで`ResourceUrlEncodingFilter`が登録されます。自動コンフィギュレーション対象になっていないViewを使う場合は、`ResourceUrlEncodingFilter`のBeanを明示的に定義してください。

```java
@Bean
ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
  return new ResourceUrlEncodingFilter();
}
```

```html:ViewとしてThymeleafを利用した際の実装例
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <link th:href="@{/css/a.css}" href="/public/css/a.css" type="text/css" rel="stylesheet"/>
    <link th:href="@{/css/b.css}" href="/resources/css/c.css" type="text/css" rel="stylesheet"/>
    <link th:href="@{/css/c.css}" href="/static/css/c.css" type="text/css" rel="stylesheet"/>
    <link th:href="@{/static/css/foo.css}" href="/static/css/foo.css" type="text/css" rel="stylesheet"/>
    <link th:href="@{/css/hoge-fix.css}" href="/static/css/hoge-fix.css" type="text/css" rel="stylesheet"/>
    <link th:href="@{/css/fuga-content.css}" href="/static/css/fuga-content.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

```html:ViewとしてThymeleafを利用した際の出力例
<!DOCTYPE html>
<html>
<head>
    <link href="/css/a.css" type="text/css" rel="stylesheet"/>
    <link href="/css/b.css" type="text/css" rel="stylesheet"/>
    <link href="/css/c.css" type="text/css" rel="stylesheet"/>
    <link href="/static/css/foo.css" type="text/css" rel="stylesheet"/>
    <link href="/v1.0.0/css/hoge-fix.css" type="text/css" rel="stylesheet"/>
    <link href="/css/fuga-content-b24befa35213dee850d89e8de5d9665c.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```


## WebJarの利用

Spring Bootを使用すると、WebJar用のコンフィギュレーションを自動で行ってくれるため、使用するライブラリのWebJarを追加するだけです。また、バージョン番号を隠蔽したい場合は、依存ライブラリとして`org.webjars:webjars-locator`を追加してください（こちらもライブラリを追加するだけで自動で有効化されます :v: ）。Viewでの実装は、「Spring MVCの独自機能を使用してアクセスする方法」で紹介した内容と一緒です。

```html:ViewとしてThymeleafを利用した際の実装例
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <link th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```

```html:ViewとしてThymeleafを利用した際の出力例
<!DOCTYPE html>
<html>
<head>
  <link href="/webjars/bootstrap/4.6.0-1/css/bootstrap.min.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<h2>Hello World!</h2>
</body>
</html>
```


## Gzipなどで圧縮された静的リソースへのアクセス

Spring BootでGzipなどで圧縮された静的リソースへアクセスする場合は、以下の設定をプロパティファイルに指定してください。

```properties:src/main/resources/application.properties
spring.web.resources.chain.compressed=true
```

## リソースチェーンのキャッシュの無効化

バージョニング機能やGzip機能などを使う際に、デフォルトの動作だと読み込んだリソース情報をキャッシュします。これは試験環境やプロダクション環境では有効な設定ですが、ローカルの開発環境では開発効率を低下させる可能性があります。ローカルの開発環境では、キャッシュを無効化する方がようでしょう。キャッシュを無効化する場合は、以下の設定をプロパティファイルに指定してください。

```properties:src/main/resources/application.properties
spring.web.resources.chain.cache=false
```

## 自動コンフィギュレーションの無効化

Spring Bootが行う自動コンフィギュレーションを無効化したい場合は、以下の設定をプロパティファイルに指定してください。

```properties:src/main/resources/application.properties
spring.web.resources.add-mappings=false
```

# まとめ

今回は、Spring MVCが提供する静的リソースがらみの機能を紹介しました。Spring Bootでアプリケーションを作っているとあまり意識しない(しなくてもいい)部分かもしれませんが、Spring MVCはこんな仕組みで静的リソースへアクセスしています。

# 補足

## Gzip機能とバージョニング機能の併用について

~~初回投稿時に、Gzip機能とバージョニング機能の併用の仕方＋Spring Bootで併用ができない旨を記載していましたが、どうもSpring Framework的にはサポート外な使い方みたいなので、記載を削除しました（すみません・・・ :persevere:）。詳しくは 「[Spring Bootのgh-5876](https://github.com/spring-projects/spring-boot/issues/5876)」をご覧ください。 (2016/5/14 追加)~~
↓
Spring 5.0で併用できるように改修された模様です (2021/5/5 追加)。

* https://github.com/spring-projects/spring-framework/issues/18836

# 参考サイト

* https://docs.spring.io/spring-framework/docs/5.3.6/reference/html/web.html#mvc-config-static-resources
* https://docs.spring.io/spring-framework/docs/5.3.6/reference/html/web.html#mvc-default-servlet-handler
* https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/#boot-features-spring-mvc-static-content