通过Spring Boot来自定义404 Not Found等错误的显示方式

总结

    Web アプリケーション全体で発生する 404 Not Found などのエラーについて、Spring Boot での表示内容をカスタマイズする

这次的环境

    • Spring Boot 2.2.0

 

    • Spring Boot Thymeleaf Starter 2.2.0

 

    Thymeleaf 3.0.11

默认情况下,将返回Whitelabel Error Page或JSON。

当通过Web浏览器访问时发生错误,默认情况下会显示一个名为“白标签错误页面”的页面。

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Nov 06 18:38:41 JST 2019
There was an unexpected error (type=Internal Server Error, status=500).
This is a sample error.

Spring Boot参考文档

对于浏览器客户端,有一个“白标签”错误视图,它以 HTML 格式呈现相同的数据(如需自定义,请添加一个视图以解析错误信息)。

另外,对于不是常规网络浏览器而是机器性质的客户端,如curl命令等,将生成包含错误、HTTP状态和异常消息详细信息的JSON响应。

{"timestamp":"2019-11-06T09:30:58.493+0000","status":500,"error":"Internal Server Error","message":"This is a sample error.","path":"/sample/"}

Spring Boot 参考文档

对于机器客户端,它会生成一个包括错误详情、HTTP状态以及异常信息的JSON响应。

参考: Spring Boot中白标签错误页与JSON响应 – Qiita

默认提供的 /error 映射(error.html)

即使没有进行特殊设置,Spring Boot也会以适当的方式处理所有错误并提供”/error”映射。这被注册为Servlet容器的”全局”错误页面。

Spring Boot参考文档

默认情况下,Spring Boot提供了一个/error映射来合理处理所有错误,并且它会在Servlet容器中注册为“全局”错误页面。

在Thymeleaf中,如果视图名称为/error,那么可以将位于src/main/resources/templates/error.html的文件作为错误页面处理。

错误.html

只需准备以下Thymeleaf所需的HTML模板文件即可。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>/error</title>
</head>
<body>
  <div th:text="'timestamp: ' + ${timestamp}"></div>
  <div th:text="'status: ' + ${status}"></div>
  <div th:text="'error: ' + ${error}"></div>
  <div th:text="'exception: ' + ${exception}"></div>
  <div th:text="'message: ' + ${message}"></div>
  <div th:text="'errors: ' + ${errors}"></div>
  <div th:text="'trace: ' + ${trace}"></div>
  <div th:text="'path: ' + ${path}"></div>
</body>
</html>

可以在error.html中输出的属性

在错误页面中,可以输出一些信息。
“/error” 映射是由 Spring Boot 的 DefaultErrorAttributes 类实现的,在 API 参考文档中列出了可以输出的项目。

默认的错误属性(Spring Boot Docs 2.2.0.RELEASE API)

当可能时,默认实现错误属性。提供以下属性:
– 时间戳 – 提取错误的时间
– 状态 – 状态码
– 错误 – 错误原因
– 异常 – 根异常的类名(如果已配置)
– 消息 – 异常消息
– 错误 – 来自BindingResult异常的任何ObjectErrors
– 追踪 – 异常的堆栈跟踪
– 路径 – 异常发生时的URL路径

错误页面的示例输出

如果出现500内部服务器错误的情况。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>/error</title>
</head>
<body>
  <div>timestamp: Wed Nov 06 18:26:31 JST 2019</div>
  <div>status: 500</div>
  <div>error: Internal Server Error</div>
  <div>exception: null</div>
  <div>message: This is a sample error.</div>
  <div>errors: null</div>
  <div>trace: null</div>
  <div>path: /sample/</div>
</body>
</html>

如果遇到 404 Not Found 的情况。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>/error</title>
</head>
<body>
  <div>timestamp: Wed Nov 06 18:25:40 JST 2019</div>
  <div>status: 404</div>
  <div>error: Not Found</div>
  <div>exception: null</div>
  <div>message: No message available</div>
  <div>errors: null</div>
  <div>trace: null</div>
  <div>path: /aaa</div>
</body>
</html>

如果访问 /error,状态码将变成 999。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>/error</title>
</head>
<body>
  <div>timestamp: Wed Nov 06 18:24:30 JST 2019</div>
  <div>status: 999</div>
  <div>error: None</div>
  <div>exception: null</div>
  <div>message: No message available</div>
  <div>errors: null</div>
  <div>trace: null</div>
  <div>path: null</div>
</body>
</html>

可以使用 /error/400.html 或者 /error/4xx.html 来取代 error.html。

Spring Boot的DefaultErrorViewResolver类负责处理这部分逻辑。

spring-boot/DefaultErrorViewResolver.java在v2.2.0.RELEASE版本上的土星星球的链接是spring-projects/spring-boot的一个仓库。

  static {
    Map<Series, String> views = new EnumMap<>(Series.class);
    views.put(Series.CLIENT_ERROR, "4xx");
    views.put(Series.SERVER_ERROR, "5xx");
    SERIES_VIEWS = Collections.unmodifiableMap(views);
  }
  @Override
  public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
      modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
  }
  private ModelAndView resolve(String viewName, Map<String, Object> model) {
    String errorViewName = "error/" + viewName;
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
        this.applicationContext);
    if (provider != null) {
      return new ModelAndView(errorViewName, model);
    }
    return resolveResource(errorViewName, model);
  }

默认的错误视图解析器 (Spring Boot 文档 2.2.0.RELEASE API)

默认的ErrorViewResolver实现,尝试使用众所周知的约定解析错误视图。将使用状态码和状态系列在”/error”下搜索模板和静态资源。
例如,HTTP 404将按照以下顺序搜索:

・’//error/404.’
・’//error/404.html’
・’//error/4xx.’
・’//error/4xx.html’

可以在 server.error.path 中更改 /error。

错误页面的显示由Spring Boot的BasicErrorController处理。

spring-boot/BasicErrorController.java 在 v2.2.0.RELEASE 版本的 spring-projects/spring-boot 的 GitHub 上。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

可以通过在 application.properties 文件中进行设置来更改 server.error.path 的设定值。参考:常用应用属性。

默认的/error映射的问题

/错误 您即使在服务器上放置了与错误映射对应的error.html文件,也会在/error页面上遇到999错误,而且机械化的客户端比如curl等,仍然会遇到意外的JSON响应问题。

以下提供一种解决这些问题的方法。

如何自定义错误处理?

替换默认错误处理的方法包括实现ErrorController接口,或者继续使用现有的BasicErrorController等现有处理方法并实现ErrorAttributes接口以替换内容。

《Spring Boot参考文档》

要完全替代默认行为,你可以实现ErrorController并注册一个该类型的bean定义,或者添加一个ErrorAttributes类型的bean来使用现有机制但替换内容。

使用ErrorController自定义错误处理。

通过实现ErrorController来自定义错误处理的方法在这里进行演示。

准备一个实现ErrorController接口的类。

准备实现ErrorController接口的类,以确定错误页面的视图并设置要显示的信息。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Web アプリケーション全体のエラーコントローラー。
 * エラー情報を HTML や JSON で出力する。
 * ErrorController インターフェースの実装クラス。
 */
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // エラーページへのマッピング
public class MyErrorController implements ErrorController {

  /**
   * エラーページのパス。
   */
  @Value("${server.error.path:${error.path:/error}}")
  private String errorPath;

  /**
   * エラーページのパスを返す。
   *
   * @return エラーページのパス
   */
  @Override
  public String getErrorPath() {
    return errorPath;
  }

  /**
   * HTML レスポンス用の ModelAndView オブジェクトを返す。
   *
   * @param request リクエスト情報
   * @return HTML レスポンス用の ModelAndView オブジェクト
   */
  @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  public ModelAndView myErrorHtml(HttpServletRequest request) {

    // HTTP ステータスを決める
    // ここでは 404 以外は全部 500 にする
    Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    if (statusCode != null && statusCode.toString().equals("404")) {
      status = HttpStatus.NOT_FOUND;
    }

    // 出力したい情報をセットする
    ModelAndView mav = new ModelAndView();
    mav.setStatus(status); // HTTP ステータスをセットする
    mav.setViewName("error"); // error.html
    mav.addObject("timestamp", new Date());
    mav.addObject("status", status.value());
    mav.addObject("path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI));

    return mav;
  }

  /**
   * JSON レスポンス用の ResponseEntity オブジェクトを返す。
   *
   * @param request リクエスト情報
   * @return JSON レスポンス用の ResponseEntity オブジェクト
   */
  @RequestMapping
  public ResponseEntity<Map<String, Object>> myErrorJson(HttpServletRequest request) {

    // HTTP ステータスを決める
    // ここでは 404 以外は全部 500 にする
    Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    if (statusCode != null && statusCode.toString().equals("404")) {
      status = HttpStatus.NOT_FOUND;
    }

    // 出力したい情報をセットする
    Map<String, Object> body = new HashMap<String, Object>();
    body.put("timestamp", new Date());
    body.put("status", status.value());
    body.put("path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI));

    return new ResponseEntity<>(body, status);
  }
}

参考:

    • ErrorController (Spring Boot Docs 2.2.0.RELEASE API)

 

    • spring-boot/BasicErrorController.java at v2.2.0.RELEASE · spring-projects/spring-boot · GitHub

 

    Spring Boot エラーページの最低限のカスタマイズ (ErrorController インターフェースの実装) – Qiita

准备一个HTML模板作为视图。

准备一个针对视图名称的HTML模板文件error.html.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>/error</title>
</head>
<body>
  <div th:text="'timestamp: ' + ${timestamp}"></div>
  <div th:text="'status: ' + ${status}"></div>
  <div th:text="'path: ' + ${path}"></div>
</body>
</html>

现在,当出现404 Not Found等错误时,这些实现类将负责处理。

使用ErrorAttributes和ErrorViewResolver来自定义错误处理。

在这里,我们通过实现ErrorAttributes和ErrorViewResolver来定制错误处理的方式。

准备一个实现ErrorAttributes接口的类

首先,准备一个实现ErrorAttributes接口的类,用于返回设置为响应JSON或ModelAndView的信息。

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * ログに記録したりユーザーに提示するエラー情報へのアクセスを提供する。
 * ErrorAttributes インターフェースを実装するため、DefaultErrorAttributes クラスを継承している。
 */
@Component // DIコンテナに登録する
public class MyErrorAttributes extends DefaultErrorAttributes {

  /**
   * エラー情報を返す。
   * エラー情報は、エラーページ ModelAndView のモデルとして使用するか
   * または @ResponseBody の JSON データとして使用する。
   * @param webRequest リクエスト情報
   * @param includeStackTrace スタックトレースを含める必要がある場合は true
   * @return エラー情報
   */
  @Override
  public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {

    // エラー情報の Map オブジェクト
    // ここにレスポンス JSON やエラーページに必要な情報を追加していく
    Map<String, Object> attr = new HashMap<String, Object>();

    // timestamp
    attr.put("timestamp", new Date());

    // status
    Object status = webRequest.getAttribute("javax.servlet.error.status_code", RequestAttributes.SCOPE_REQUEST);
    if (status == null) {
      status = 999; // ここでは 999 にしておく
    }
    attr.put("status", status);

    // path
    Object path = webRequest.getAttribute("javax.servlet.error.request_uri", RequestAttributes.SCOPE_REQUEST);
    if (path != null) {
      attr.put("path", path);
    }

    // 独自のエラーメッセージを追加する
    attr.put("myErrorMessage", status);

    return attr;
  }
}

可以参考:

    • ErrorAttributes (Spring Boot Docs 2.2.0.RELEASE API)

 

    spring-boot/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error at v2.2.0.RELEASE · spring-projects/spring-boot · GitHub

准备一个实现ErrorViewResolver接口的类

接下来,我们需要准备一个ErrorViewResolver接口的实现类,以确定错误页面的视图并设置要显示的信息。

import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * エラービューを決める bean クラス。
 */
@Component // DIコンテナに登録する
public class MyErrorViewResolver implements ErrorViewResolver {

  public MyErrorViewResolver() {
    System.out.println("MyErrorViewResolver created");
  }

  /**
   * エラービューを決定する。
   *
   * @param request リクエスト情報
   * @param status  エラーの HTTP ステータス
   * @param model   ビューで使うために提示されたモデル
   * @return 決定した ModelAndView または null
   */
  @Override
  public ModelAndView resolveErrorView(
    HttpServletRequest request,
    HttpStatus status,
    Map<String, Object> model) {

    // モデルとビューの情報を構築
    ModelAndView mav = new ModelAndView();

    // MyErrorAttributes#getErrorAttributes の戻り値をセット
    mav.addAllObjects(model);

    // ビュー名を指定
    mav.setViewName("myerror"); // resources/templates/myerror.html

    // ステータスに応じて処理
    if (status == HttpStatus.NOT_FOUND) {
      // 404 Not Found
      mav.setStatus(HttpStatus.NOT_FOUND);
      mav.addObject("myErrorMessage", "404 Not Found");
    } else {
      // 404 以外は全部 500 にする
      mav.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
      mav.addObject("myErrorMessage", "500 Internal Server Error");
    }

    return mav;
  }
}

参考:

    • ErrorViewResolver (Spring Boot Docs 2.2.0.RELEASE API)

 

    spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error at v2.2.0.RELEASE · spring-projects/spring-boot · GitHub

准备一个可以作为视图的HTML模板。

为视图名称准备一个与之匹配的HTML模板文件myerror.html。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>/myerror</title>
</head>
<body>
  <div th:text="'myErrorMessage: ' + ${myErrorMessage}"></div>
  <div th:text="'timestamp: ' + ${timestamp}"></div>
  <div th:text="'status: ' + ${status}"></div>
  <div th:text="'path: ' + ${path}"></div>
</body>
</html>

這樣,當出現404 Not Found等錯誤時,這些實現類將處理該錯誤。

请提供参考资料。

    • Spring Boot Reference Documentation – The “Spring Web MVC Framework” – Error Handling

 

    • SpringBootを使うときに最低限やっておきたいセキュリティ対策 – Qiita

 

    Spring Boot で Boot した後に作る Web アプリケーション基盤/spring-boot-application-infrastructure – Speaker Deck