環境
- JDK 21
- Spring Boot 3.2.4
- Embedded Tomcat 10.1.19
やりたいこと
Spring Bootにもエラーページの機能があります。具体的にはsrc/main/resources/templates直下にerror.html、src/main/resources/templates/error直下に404.html・500.htmlなどを作成すればOKです(公式ドキュメント)。
しかし、サーブレットフィルターのレベルで例外がスローされると、前述のエラーページではなくTomcatデフォルトのエラーページが表示されてしまいます。
これを自作のエラーページに差し替えたいです。
自作エラーページの作成
静的なHTMLを作成して、適当な場所・適当なファイル名で配置します。今回はsrc/main/resources/templates/tomcatフォルダにtomcat-error.htmlという名前で配置することにします。
ErrorReportValve
のカスタマイズ
Tomcatデフォルトのエラーページをレスポンスしているのが、TomcatのErrorReportValve
クラスです。これを継承+オーバーライドすることで、作成した自作エラーページをレスポンスするよう変更します。
CustomErrorReportValve.java
package com.example;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.coyote.ActionCode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Tomcatエラーページをカスタマイズします。
*/
public class CustomErrorReportValve extends ErrorReportValve {
/**
* カスタマイズしたエラーページをレスポンスします。
* @see ErrorReportValve#report(Request, Response, Throwable)
* @param request The request being processed
* @param response The response being generated
* @param throwable The exception that occurred (which possibly wraps a root cause exception
*/
@Override
protected void report(Request request, Response response, Throwable throwable) {
int statusCode = response.getStatus();
// Do nothing on a 1xx, 2xx and 3xx status
// Do nothing if anything has been written already
// Do nothing if the response hasn't been explicitly marked as in error
// and that error has not been reported.
if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
return;
}
// If an error has occurred that prevents further I/O, don't waste time
// producing an error report that will never be read
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (!result.get()) {
return;
}
// ここまでのコード👆は、ErrorReportValve#report()からコピーしました
// カスタマイズしたエラーページをレスポンス
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
try (OutputStream os = response.getOutputStream();
InputStream is = this.getClass().getResourceAsStream("/templates/tomcat/tomcat-error.html")) {
IOTools.flow(is, os);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
TomcatがCustomErrorReportValve
を使うように設定
ServerConfig.java
package com.example;
import org.apache.catalina.Container;
import org.apache.catalina.core.StandardHost;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 組み込みサーバーに関する設定です。
*/
@Configuration
public class ServerConfig {
/**
* Tomcatのエラー画面を自作のものに変更します。
*/
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> webServerFactoryCustomizer() {
return factory -> factory.addContextCustomizers(context -> {
Container parent = context.getParent();
if (parent instanceof StandardHost host) {
host.setErrorReportValveClass(CustomErrorReportValve.class.getName());
}
});
}
}