分析源码

在BasicErrorController源码中的errorHtml方法中

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
   HttpStatus status = getStatus(request);
   Map<String, Object> model = Collections
         .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
   response.setStatus(status.value());
   ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

会返回一个ModelAndView,其中包含请求信息,响应信息,状态码,和模型数据。

ModelAndView modelAndView = resolveErrorView(request, response, status, model);中的resolveErrorView方法为:

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
         Map<String, Object> model) {
      for (ErrorViewResolver resolver : this.errorViewResolvers) {
         ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
         if (modelAndView != null) {
            return modelAndView;
         }
      }
      return null;
   }

}

它会遍历所有的errorViewResolvers,找到一个匹配的返回。

在其中的

ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);

接口resolveErrorView,其实现在DefaultErrorViewResolver中:

@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;
}

该方法中的resolve(String.valueOf(status.value()), model);

具体方法为:

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);
}

其要求当前传过来的错误状态码,会拼接在error/下,所以错误页面资源必须在error文件夹下才能被识别到。

由该方法可知,其会优先精准匹配error文件夹下的错误页面(404.html,500.html等),然后返回该页面( return resolveResource(errorViewName, model););如果没有则会使用SERIES_VIEWS.containsKey(status.series())再去查找系列错误状态码,例如4xx.html,5xx.html

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);
}

然后调用resolve方法拼接。

最后调用resolveResource`方法,寻找资源,若找到则返回,否则返回null。

BasicErrorController中的:

ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);

可以看出如果没有匹配到自定义的异常页面,则会直接返回new ModelAndView("error", model);即默认error视图,也就是:

image-20230426203231308
image-20230426203231308

添加错误页面

image-20230426213724504
image-20230426213724504

发送一个错误请求:

image-20230426213825474
image-20230426213825474

可以看到,错误页面已经生效了。

但如果有除了404和500以外的错误,还是会返回默认错误页,例如400错误:

image-20230426214229914
image-20230426214229914

这时可以添加4xx.html和5xx.html页面。

image-20230426214412241
image-20230426214412241

这时400错误也跳到了自定义错误页面

image-20230426214512325
image-20230426214512325

定制错误页面

在自动配置类ErrorMvcAutoConfiguration中有一个:

@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
   return new DefaultErrorAttributes();
}

DefaultErrorAttributes中存储了一些默认属性。

而在BasicErrorController中调用了多次getErrorAttributes()获取错误属性的方法。

getErrorAttributes()又调用了:

return this.errorAttributes.getErrorAttributes(webRequest, options);
default Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    return Collections.emptyMap();
}

接着找到:

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
   Map<String, Object> errorAttributes = 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;
}

又调用了方法getErrorAttributes(),在该方法中:

private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
   Map<String, Object> errorAttributes = new LinkedHashMap<>();
   errorAttributes.put("timestamp", new Date());
   addStatus(errorAttributes, webRequest);
   addErrorDetails(errorAttributes, webRequest, includeStackTrace);
   addPath(errorAttributes, webRequest);
   return errorAttributes;
}

添加时间戳

errorAttributes.put("timestamp", new Date());

添加错误状态码和错误信息

addStatus()方法中:

errorAttributes.put("status", status);

errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());

添加异常信息

addErrorDetails()方法中:

errorAttributes.put("exception", error.getClass().getName());

(多层调用)errorAttributes.put("message", getMessage(webRequest, error));

添加错误请求的路径

addPath()方法中:

errorAttributes.put("path", path);

所以在页面上可以获取到timestamp,status,error,exception,message这几个数据。

在原有页面中添加thymeleaf标签:

<html lang="en" xmlns:th="http://www.thymeleaf.org">

<section class="error-wrapper text-center">
            <h1><img alt="" src="images/404-error.png" ></h1>
            <h2>page not found</h2>
            <h3>We Couldn’t Find This Page</h3>
            <a class="back-btn" href="index.html"> Back To Home</a>
</section>

修改为:

<section class="error-wrapper text-center">
    <h1><img alt="" src="images/404-error.png" th:src="@{images/404-error.png}"></h1>
    <h2 th:text="${timestamp}"></h2>
    <h3 th:text="${status}">We Couldn’t Find This Page</h3>
    <h3 th:text="${error}">We Couldn’t Find This Page</h3>
    <h3 th:text="${exception}">We Couldn’t Find This Page</h3>
    <h3 th:text="${message}">We Couldn’t Find This Page</h3>
    <h3 th:text="${path}">We Couldn’t Find This Page</h3>
    <a class="back-btn" th:href="@{login.html}"> Back To Home</a>
</section>

访问错误页:

image-20230426224246907
image-20230426224246907

image-20230426224344700
image-20230426224344700

可以看到除了message之外其他数据都拿到了。

在自动配置类

@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
    ......
}

ServerProperties.class

private final ErrorProperties error = new ErrorProperties();

可以看到:

/**
 * When to include "message" attribute.
 */
private IncludeAttribute includeMessage = IncludeAttribute.NEVER;
public enum IncludeAttribute {

   /**
    * Never add error attribute.
    */
   NEVER,
   ......
   }

所以看不到message信息。需要去配置文件中修改该配置才能获取到message信息,如下

#定制错误页信息
server.error.include-exception=true
server.error.include-message=always

再次访问错误页面:

image-20230426225347153
image-20230426225347153

可以看到message已经正确获取到。