自定义错误页面
背景:当我们访问应用程序不存在的接口路径或者参数传递不规范时,springboot 默认提示 如下页面
该页面对用户不友好,我们可以自定义展示错误页来改善。
优化后的简洁效果,可对 html 页面进一步美化,这里只说明修改默认错误页方法。
demo地址
https://gitee.com/stormlong/springboot-errorpage
官方文档
https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/web.html#web.servlet.spring-mvc.error-handling.error-pages
静态页面
resources\public\error
不引入任何模板引擎时,在这个目录下寻找
且文件名和错误状态码保持一致
动态页面
resources\templates\error
引入模板引擎时,在这个目录下寻找
例 :
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
且文件名和错误状态码保持一致
优先级
html 文件命名也可以为 4xx 或者是 5xx ,这里的 xx 代表了 404, 401等异常错误
不引入任何模板引擎时,优先具体的错误码页面,然后是 4xx页面,即
- \public\error\404.html
- \public\error\4xx.html
引入模板引擎时,优先级
- \templates\error 目录下的 404.html
- \public\error 目录下的 404.html
- \templates\error 目录下的 4xx.html
- \public\error 目录下的 4xx.html
没有任何错误页面时,默认来到 SpringBoot 默认的错误提示页面
总结:优先具体的错误码页面,然后动态目录下的 4xx 页面
原理解析
处理异常类
在spring boot中,我们可以找到 BasicErrorController,这个类主要用来处理异常
@Controller
//如果没有配置server.error.path就去error.path找,如果没有配置默认路径为/error
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {...
}
即我们可以通过 server.error.path 来定义错误的目录,缺省则使用默认 /error 目录
server:error:path: /error
默认 yaml 配置
server:port: 8080error:path: /errorspring:thymeleaf:cache: false# 以下均为默认配置 可从该类 ThymeleafProperties 下看到 prefix: classpath:/templates/suffix: .htmlmode: htmlencoding: UTF-8servlet:content-type: text/html
返回错误信息
SpringBoot默认返回的错误信息,通过DefaultErrorAttruites类可以找到
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package org.springframework.boot.web.servlet.error;import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;@Order(-2147483648)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";public DefaultErrorAttributes() {}public int getOrder() {return -2147483648;}public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {this.storeErrorAttributes(request, ex);return null;}private void storeErrorAttributes(HttpServletRequest request, Exception ex) {request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);}public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));if (!options.isIncluded(Include.EXCEPTION)) {errorAttributes.remove("exception");}if (!options.isIncluded(Include.STACK_TRACE)) {errorAttributes.remove("trace");}if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {errorAttributes.remove("message");}if (!options.isIncluded(Include.BINDING_ERRORS)) {errorAttributes.remove("errors");}return errorAttributes;}private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map<String, Object> errorAttributes = new LinkedHashMap();errorAttributes.put("timestamp", new Date());this.addStatus(errorAttributes, webRequest);this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);this.addPath(errorAttributes, webRequest);return errorAttributes;}private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");if (status == null) {errorAttributes.put("status", 999);errorAttributes.put("error", "None");} else {errorAttributes.put("status", status);try {errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());} catch (Exception var5) {errorAttributes.put("error", "Http Status " + status);}}}private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace) {Throwable error = this.getError(webRequest);if (error != null) {while(true) {if (!(error instanceof ServletException) || error.getCause() == null) {errorAttributes.put("exception", error.getClass().getName());if (includeStackTrace) {this.addStackTrace(errorAttributes, error);}break;}error = error.getCause();}}this.addErrorMessage(errorAttributes, webRequest, error);}private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {BindingResult result = this.extractBindingResult(error);if (result == null) {this.addExceptionErrorMessage(errorAttributes, webRequest, error);} else {this.addBindingResultErrorMessage(errorAttributes, result);}}private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {errorAttributes.put("message", this.getMessage(webRequest, error));}protected String getMessage(WebRequest webRequest, Throwable error) {Object message = this.getAttribute(webRequest, "javax.servlet.error.message");if (!ObjectUtils.isEmpty(message)) {return message.toString();} else {return error != null && StringUtils.hasLength(error.getMessage()) ? error.getMessage() : "No message available";}}private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, BindingResult result) {errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount());errorAttributes.put("errors", result.getAllErrors());}private BindingResult extractBindingResult(Throwable error) {return error instanceof BindingResult ? (BindingResult)error : null;}private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {StringWriter stackTrace = new StringWriter();error.printStackTrace(new PrintWriter(stackTrace));stackTrace.flush();errorAttributes.put("trace", stackTrace.toString());}private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {String path = (String)this.getAttribute(requestAttributes, "javax.servlet.error.request_uri");if (path != null) {errorAttributes.put("path", path);}}public Throwable getError(WebRequest webRequest) {Throwable exception = (Throwable)this.getAttribute(webRequest, ERROR_INTERNAL_ATTRIBUTE);if (exception == null) {exception = (Throwable)this.getAttribute(webRequest, "javax.servlet.error.exception");}webRequest.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, exception, 0);return exception;}private <T> T getAttribute(RequestAttributes requestAttributes, String name) {return requestAttributes.getAttribute(name, 0);}
}
自定义扩展返回
如果要想自定义扩展返回的信息,我们可以自定义一个类来继承这个类,代码如下:
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;import java.util.HashMap;
import java.util.Map;@Component
public class CustomDefaultErrorAttribute extends DefaultErrorAttributes {@Overridepublic Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {Map<String, Object> map = new HashMap<>();map.put("compay", "深证腾讯计算公司");//调用父类来添加之前Spring的错误信息map.putAll(super.getErrorAttributes(webRequest, options));return map;}}
页面获取代码如下
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>动态404错误</title>
</head>
<body>
<h1>this is 404 page :(</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>status:[[${status}]]</h2>
<h2>path:[[${path}]]</h2>
<h2>error:[[${error}]]</h2>
<h2>message:[[${message}]]</h2>
<h2>exception:[[${exception}]]</h2>
<h2>trace:[[${trace}]]</h2>
<h2>compay:[[${compay}]]</h2>
</body>
</html>
tomcat 自定义错误页面(扩展)
官方文档:
https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Error_Report_Valve
tomcat-404错误,使用自定义页面屏蔽敏感信息
- 在项目中新增404页面,例如:/resource/templates/errorpage/400.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Error</title>
</head>
<body>
<h1>400 Bad Request</h1>
</body>
</html>
-
在server.xml配置文件里进行配置 配置节点下新增 部分
https://tomcat.apache.org/tomcat-9.0-doc/config/host.html
https://tomcat.apache.org/tomcat-9.0-doc/config/context.html
<Host appBase="/app/webapps"><Context path="/abc/def"></Context><Valve className="org.apache.catalina.valves.ErrorReportValve" errorCode.404="/app/webapps/abc/def/WEB-INF/classes/templates/errorpage/400.html" showReport="false" showServerInfo="false" />
</Host>
访问不成功时可尝试 abc/ 替换成 abc#
/app/webapps/abc#def/WEB-INF/classes/templates/errorpage/400.html
配置说明
showReport:如果设置为false,则不会在HTML响应中返回错误报告。默认值:true
showServerInfo:如果设置为false,则不会在HTML响应中返回服务器版本。默认值:true
errorCode.xxx: 要为xxx表示的HTTP错误代码返回的UTF-8编码的HTML文件的位置。例如,errorCode.404指定HTTP 404错误返回的文件。位置可以是相对的或绝对的。如果是相对的,则必须是相对于 $CATALINA_BASE
的。
className要使用的实现的Java类名。必须将其设置为 org.apache.catalina.valves.ErrorReportValve 以使用默认的错误报告值。
appBase: 此虚拟主机的应用程序基目录。这是一个目录的路径名,该目录可能包含要在此虚拟主机上部署的Web应用程序。如果未指定,则将使用默认的 webapps。
path: 此Web应用程序的上下文路径,它与每个请求URI的开头相匹配,以选择要处理的适当Web应用程序。特定主机内的所有上下文路径都必须是唯一的。