Spring Boot で静的リソースのエンコーディングを指定する

Spring Boot には [静的リソースをホストする機能](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-spring-mvc-static-content) がありますが、Controller からのレスポンスとは違って、静的リソースのレスポンスヘッダでは Content-Type に charset を付与してくれなくてハマりました。

```:期待するレスポンスヘッダ
Content-Type: text/html;charset=UTF-8
```

```:実際のレスポンスヘッダ
Content-Type: text/html
```

### 確認環境

* Spring Boot 2.1.5
* AdoptOpenJDK 11.0.3+7
* macOS 10.14.3

### 結論

WebServerFactoryCustomizer を実装して内蔵 Tomcat コンテナの動作をカスタマイズすることで、特定の拡張子のファイルに対してエンコーディングを明示できます。ただし、拡張子に対して charset が一意に決まる場合に限ります。

```java
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class ServletCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
        mappings.add("html", "text/html;charset=UTF-8");
        factory.setMimeMappings(mappings);
    }
}
```

### ボツ案

ここに行き着くまでに試したボツ案を一応残しておきます。

#### 1. CharacterEncodingFilter のカスタマイズ

application.properties の設定を調整することで、[CharacterEncodingFilter](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/filter/CharacterEncodingFilter.html) の振る舞いを変更し、charset を強制的に出力させることができます。

```properties:application.properties
# Charset of HTTP requests and responses. 
spring.http.encoding.charset=UTF-8
# Whether to force the encoding to the configured charset on HTTP responses.
spring.http.encoding.force-response=true
```
HTML ファイルに対しては期待通りに動作してくれるのですが、その名の通り画像ファイルなどにも強制的に一律で付与されてしまうので残念な感じです。

```
Content-Type: image/jpeg;charset=UTF-8
```

#### 2. CharacterEncodingFilter の拡張

CharacterEncodingFilter では一律で出力されてしまうため、このクラスを派生させて独自のフィルタを実装すればよさそうに思えます。

* [Should CharacterEncodingFilter apply to all assets? #5459](https://github.com/spring-projects/spring-boot/issues/5459)

一見するとうまくいきそうなのですが、この方法はリクエストURLから一意に判定できる場合に限ります。

次のようにパス解決できなかった場合のフォールバックページとして index.html を返す、といったことをしていると（私のケースではしていたのですが）、必ずしもリクエストURLに拡張子まで含まれているとは限らないため、判定に必要な情報が足りません。

```java
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**/*")
            .addResourceLocations("classpath:/static/")
            .resourceChain(true)
            .addResolver(new PathResourceResolver() {
                @Override
                protected Resource getResource(String resourcePath, Resource location) throws IOException {
                    Resource requestedResource = location.createRelative(resourcePath);
                    return requestedResource.exists() && requestedResource.isReadable()
                        ? requestedResource : new ClassPathResource("/static/index.html");
                }
            });
    }
}
```

### 参考情報

* [Spring MVC (Boot) does not send MIME type for certain files (WOFF, etc) - Stack Overflow](https://stackoverflow.com/questions/27617275/spring-mvc-boot-does-not-send-mime-type-for-certain-files-woff-etc)