Spring Boot には 静的リソースをホストする機能 がありますが、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 が一意に決まる場合に限ります。
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 の振る舞いを変更し、charset を強制的に出力させることができます。
# 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 では一律で出力されてしまうため、このクラスを派生させて独自のフィルタを実装すればよさそうに思えます。
一見するとうまくいきそうなのですが、この方法はリクエストURLから一意に判定できる場合に限ります。
次のようにパス解決できなかった場合のフォールバックページとして index.html を返す、といったことをしていると(私のケースではしていたのですが)、必ずしもリクエストURLに拡張子まで含まれているとは限らないため、判定に必要な情報が足りません。
@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");
}
});
}
}