boot项目的优点就是帮助我们简化了配置,并且为我们提供了一系列的扩展点供我们使用,其中不乏错误页面的个性化开发。
理解错误响应流程
我们来到org.springframework.boot.autoconfigure.web.servlet.error下的ErrorMvcAutoConfiguration这里面配置了错误响应的规则。主要介绍里面注册的这几个bean(DefaultErrorAttributes,BasicErrorController,ErrorPageCustomizer,DefaultErrorViewResolver),当报错时来到BasicErrorController,这个可以理解为boot帮我们写好的controller层,然后进行视图的解析,也就是对数据与模型页面的解析,然后返回给客户端。
先来到BasicErrorController
当有请求为/error会来到这,进行解析返回对应的ModelAndView,其中的error,与errorHtml方法分别是为非浏览器请求服务(例如用postman来发起请求测试,就是返回json数据)、与为浏览器服务返回的不是json数据。那么咋知道是浏览器的请求还是非浏览器的请求呢?如果是浏览器发起一个请求它的Content-Type:text/html;charset=UTF-8,而非浏览器发起的请求的Content-Type:application/json;charset=UTF-8。看见没区别就是有无text/html。代码中也有体现,注意看errorHtml上的那个注解。
有了view也就是页面,model也就是数据那么我们的错误页面不就来了吗。
@Controller
@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<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
public String getErrorPath() {
return this.errorProperties.getPath();
}
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(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<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
IncludeStacktrace include = this.getErrorProperties().getIncludeStacktrace();
if (include == IncludeStacktrace.ALWAYS) {
return true;
} else {
return include == IncludeStacktrace.ON_TRACE_PARAM ? this.getTraceParameter(request) : false;
}
}
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}
model的来由
emmmm发现getErrorAttributes是从DefaultErrorAttributes这来的。
//BasicErrorController
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
//model来由
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//view来由
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
//AbstractErrorController
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
public interface ErrorAttributes {
Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);
Throwable getError(WebRequest webRequest);
}
}
view的来由(resolveErrorView)
我们接着来研究BasicErrorController中的errorHtml方法看里面是怎么解析的,发现resolveErrorView原来是一个接口,默认的实现类是DefaultErrorViewResolver牛逼,嵌套这么多层!!!!!!注意resolveErrorView传进去的status是getStatus(request)得到的状态码,点进去getStatus方法中发现status=request.getAttribute(“javax.servlet.error.status_code”)。就是这个属性
//BasicErrorController
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
//AbstractErrorController
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
Iterator var5 = this.errorViewResolvers.iterator();
ModelAndView modelAndView;
do {
if (!var5.hasNext()) {
return null;
}
ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
modelAndView = resolver.resolveErrorView(request, status, model);
} while(modelAndView == null);
return modelAndView;
}
//点进来发现ErrorViewResolver 是一个接口,
//查看实现类只有DefaultErrorViewResolver
@FunctionalInterface
public interface ErrorViewResolver {
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}
真正的幕后解析大佬DefaultErrorViewResolver
通过阅读DefaultErrorViewResolver中的resolveErrorView方法(源码我写了注释),大体上的逻辑是如果传入的状态码有对应的页面精确匹配(/error/404.html这种),那么则跳转到这个页面,否者匹配跳到4xx、5xx这个模糊匹配的页面(/error/4xx.html这种)。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private int order = 2147483647;
public DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}
DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties, TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//精确匹配到就返回这个modelandview
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
//如果精确匹配不到,那么匹配4xx或者5xx的页面
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
//返回对应的modelandview
return modelAndView;
}
//如果此时的viewName(状态码)有明确的页面匹配则返回一个modelandview
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//view路径映射格式:error/状态码
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
;
}
}
return null;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
//初始化4xx,5xx到一个map
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);
}
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(this.getContentType());
FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream());
}
}
}
定制化开发步骤
一:创建自己的定制化错误页面
因为用的是boot项目用的thymeleaf模版引擎来获取的值,注意要导入<html lang="en" xmlns:th="http://www.thymeleaf.org">
并且boot项目默认的解析页面的位置是在templates下,因此我们的页面也应放在这个下面
二:编写自己的异常类
直接继承RuntimeException就好了,在有异常出现的时候,会自动匹配异常的类型,省心省事的。
三:利用@ControllerAdvice+@ExceptionHandler捕获异常请求转发到/error
这里我们可以根据不同的异常配置不同的状态码,根据不同的异常配置这个异常独有的错误提示信息(个性化的体现就是在这里)
注意:1:是转发不是重定向(数据会丢失)。
2:必须设置状态码javax.servlet.error.status_code。
3:图中的e就是我们捕获的异常。
四:编写自己的定制化数据解析规则
出现异常后的所有请求在到达错误页面之前都会从这拿取数据。因此在这里我们可以配置一些通用的错误信息。如时间戳…等
五:编写controller测试
如果id为0则会抛出我们的自定义异常,然后被@ControllerAdvice那捕获,转发到/error,然后因为我们配置的状态码是500,但是/error下面没有500.html页面,所以经过视图解析会到/error/5xx.html这,然后在到达页面之前我们会从customExceptionAttribute中在拿取我们的通用错误信息,然后返回到达页面。
六:效果展示
数据没有设置样式丑是丑了点。。。。但是可以达到了