通过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