# springboot-errorpage **Repository Path**: stormlong/springboot-errorpage ## Basic Information - **Project Name**: springboot-errorpage - **Description**: 自定义错误页面 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-10-29 - **Last Updated**: 2024-10-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 自定义错误页面 背景:当我们访问应用程序不存在的接口路径或者参数传递不规范时,springboot 默认提示 如下页面 ![image-20241029145253083](index_files/image-20241029145253083.png) 该页面对用户不友好,我们可以自定义展示错误页来改善。 优化后的简洁效果,可对 html 页面进一步美化,这里只说明修改默认错误页方法。 ![image-20241029145755333](index_files/image-20241029145755333.png) ## 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 ![image-20241028172717928](index_files/image-20241028172717928.png) ![image-20241028172806362](index_files/image-20241028172806362.png) ## 静态页面 resources\public\error 不引入任何模板引擎时,在这个目录下寻找 且文件名和错误状态码保持一致 ## 动态页面 resources\templates\error 引入模板引擎时,在这个目录下寻找 例 : ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` 且文件名和错误状态码保持一致 ## 优先级 html 文件命名也可以为 4xx 或者是 5xx ,这里的 xx 代表了 404, 401等异常错误 不引入任何模板引擎时,优先具体的错误码页面,然后是 4xx页面,即 1. \public\error\404.html 2. \public\error\4xx.html 引入模板引擎时,优先级 1. \templates\error 目录下的 404.html 2. \public\error 目录下的 404.html 3. \templates\error 目录下的 4xx.html 4. \public\error 目录下的 4xx.html 没有任何错误页面时,默认来到 SpringBoot 默认的错误提示页面 总结:优先具体的错误码页面,然后动态目录下的 4xx 页面 ![image-20241029102623516](index_files/image-20241029102623516.png) ## 原理解析 ### 处理异常类 在spring boot中,我们可以找到 BasicErrorController,这个类主要用来处理异常 ```java // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.boot.autoconfigure.web.servlet.error; import java.util.Collections; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller //如果没有配置server.error.path就去error.path找,如果没有配置默认路径为/error @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { this(errorAttributes, errorProperties, Collections.emptyList()); } public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List errorViewResolvers) { super(errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorProperties = errorProperties; } @RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity> error(HttpServletRequest request) { HttpStatus status = this.getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity(status); } else { Map body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity(body, status); } } @ExceptionHandler({HttpMediaTypeNotAcceptableException.class}) public ResponseEntity mediaTypeNotAcceptable(HttpServletRequest request) { HttpStatus status = this.getStatus(request); return ResponseEntity.status(status).build(); } protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) { ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); if (this.errorProperties.isIncludeException()) { options = options.including(new Include[]{Include.EXCEPTION}); } if (this.isIncludeStackTrace(request, mediaType)) { options = options.including(new Include[]{Include.STACK_TRACE}); } if (this.isIncludeMessage(request, mediaType)) { options = options.including(new Include[]{Include.MESSAGE}); } if (this.isIncludeBindingErrors(request, mediaType)) { options = options.including(new Include[]{Include.BINDING_ERRORS}); } return options; } protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) { switch(this.getErrorProperties().getIncludeStacktrace()) { case ALWAYS: return true; case ON_PARAM: return this.getTraceParameter(request); default: return false; } } protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) { switch(this.getErrorProperties().getIncludeMessage()) { case ALWAYS: return true; case ON_PARAM: return this.getMessageParameter(request); default: return false; } } protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) { switch(this.getErrorProperties().getIncludeBindingErrors()) { case ALWAYS: return true; case ON_PARAM: return this.getErrorsParameter(request); default: return false; } } protected ErrorProperties getErrorProperties() { return this.errorProperties; } } ``` 即我们可以通过 server.error.path 来定义错误的目录,缺省则使用默认 /error 目录 ```yaml server: error: path: /error ``` 默认 yaml 配置 ```yaml server: port: 8080 error: path: /error spring: thymeleaf: cache: false # 以下均为默认配置 可从该类 ThymeleafProperties 下看到 prefix: classpath:/templates/ suffix: .html mode: html encoding: UTF-8 servlet: content-type: text/html ``` ### 返回错误信息 SpringBoot默认返回的错误信息,通过DefaultErrorAttruites类可以找到 ```java // // 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 getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map 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 getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map 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 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 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 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 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 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 errorAttributes, Throwable error) { StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); stackTrace.flush(); errorAttributes.put("trace", stackTrace.toString()); } private void addPath(Map 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 getAttribute(RequestAttributes requestAttributes, String name) { return requestAttributes.getAttribute(name, 0); } } ``` ## 自定义扩展返回 如果要想自定义扩展返回的信息,我们可以自定义一个类来继承这个类,代码如下: ```java 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 { @Override public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map map = new HashMap<>(); map.put("compay", "深证腾讯计算公司"); //调用父类来添加之前Spring的错误信息 map.putAll(super.getErrorAttributes(webRequest, options)); return map; } } ``` 页面获取代码如下 ```html 动态404错误

this is 404 page :(

timestamp:[[${timestamp}]]

status:[[${status}]]

path:[[${path}]]

error:[[${error}]]

message:[[${message}]]

exception:[[${exception}]]

trace:[[${trace}]]

compay:[[${compay}]]

``` ## tomcat 自定义错误页面(扩展) 官方文档: https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Error_Report_Valve tomcat-404错误,使用自定义页面屏蔽敏感信息 1. 在项目中新增404页面,例如:/resource/templates/errorpage/400.html ```html Error

400 Bad Request

``` 2. 在server.xml配置文件里进行配置 配置节点下新增 部分 https://tomcat.apache.org/tomcat-9.0-doc/config/host.html https://tomcat.apache.org/tomcat-9.0-doc/config/context.html ```xml ``` 访问不成功时可尝试 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应用程序。特定主机内的所有上下文路径都必须是唯一的。