springboot中优雅的个性定制化错误页面+源码解析

    科技2022-07-10  148

    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中在拿取我们的通用错误信息,然后返回到达页面。

    六:效果展示 数据没有设置样式丑是丑了点。。。。但是可以达到了

    Processed: 0.038, SQL: 8