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

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

在这里插入图片描述六:效果展示
数据没有设置样式丑是丑了点。。。。但是可以达到了
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42875345/article/details/108908696