分析源码
在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视图,也就是:
添加错误页面
发送一个错误请求:
可以看到,错误页面已经生效了。
但如果有除了404和500以外的错误,还是会返回默认错误页,例如400错误:
这时可以添加4xx.html和5xx.html页面。
这时400错误也跳到了自定义错误页面
定制错误页面
在自动配置类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>
访问错误页:
可以看到除了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
再次访问错误页面:
可以看到message已经正确获取到。
你写得非常清晰明了,让我很容易理解你的观点。
非常感谢你的鼓励,很高兴能帮助到你