首页 > 编程语言 >springboot中优雅的个性定制化错误页面+源码解析

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

时间:2024-01-20 22:03:51浏览次数:25  
标签:status return springboot request 源码 error ModelAndView model 页面


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

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

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

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下,因此我们的页面也应放在这个下面

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

二:编写自己的异常类

直接继承RuntimeException就好了,在有异常出现的时候,会自动匹配异常的类型,省心省事的。

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

三:利用@ControllerAdvice+@ExceptionHandler捕获异常请求转发到/error
这里我们可以根据不同的异常配置不同的状态码,根据不同的异常配置这个异常独有的错误提示信息(个性化的体现就是在这里)
注意:1:是转发不是重定向(数据会丢失)。
2:必须设置状态码javax.servlet.error.status_code。
3:图中的e就是我们捕获的异常。

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

四:编写自己的定制化数据解析规则

出现异常后的所有请求在到达错误页面之前都会从这拿取数据。因此在这里我们可以配置一些通用的错误信息。如时间戳…等

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

五:编写controller测试
如果id为0则会抛出我们的自定义异常,然后被@ControllerAdvice那捕获,转发到/error,然后因为我们配置的状态码是500,但是/error下面没有500.html页面,所以经过视图解析会到/error/5xx.html这,然后在到达页面之前我们会从customExceptionAttribute中在拿取我们的通用错误信息,然后返回到达页面。

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

六:效果展示

数据没有设置样式丑是丑了点。。。。但是可以达到了

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


标签:status,return,springboot,request,源码,error,ModelAndView,model,页面
From: https://blog.51cto.com/u_16414043/9346307

相关文章

  • ConcurrentHashMap源码逐行解读基于jdk1.8
    前导知识//node数组最大容量:2^30=1073741824privatestaticfinalintMAXIMUM_CAPACITY=1<<30;//默认初始值,必须是2的幕数privatestaticfinalintDEFAULT_CAPACITY=16;//数组可能最大值,需要与toArray()相关方法关联st......
  • MyBatis 系列:MyBatis 源码环境搭建
    目录一、环境准备二、下载MyBatis源码和MyBatis-Parent源码三、创建空项目、导入项目四、编译mybatis-parent五、编译mybatis六、测试总结一、环境准备jdk:17maven:3.9.5二、下载MyBatis源码和MyBatis-Parent源码Mybatis:https://github.com/mybatis/mybatis-3.gitMy......
  • Feign源码解析6:如何集成discoveryClient获取服务列表
    背景我们上一篇介绍了feign调用的整体流程,在@FeignClient没有写死url的情况下,就会生成一个支持客户端负载均衡的LoadBalancerClient。这个LoadBalancerClient可以根据服务名,去获取服务对应的实例列表,然后再用一些客户端负载均衡算法,从这堆实例列表中选择一个实例,再进行http调用即......
  • 将小部分源码设计精髓带入到开发中来(工厂模式、适配器模式、抽象类、监听器)
    前言咋说呢,大学期间阅读过很多源码(Aop、Mybatis、Ioc、SpringMvc…),刚开始看这些源码的时候觉得云里雾里,一个没什么代码量的人突然去接触这种商业帝国级别的成品源码的时候,根本无从下手,这种感觉很难受,但是也庆幸自己熬过了那段难忘且充实的日子,随着自己代码量的慢慢增多,也开始慢慢......
  • 直播app系统源码,通过延迟加载非关键资源实现首屏优化
    直播app系统源码,通过延迟加载非关键资源实现首屏优化将非关键资源(如广告、推荐内容等)的加载延迟到首屏渲染完成之后,以提高首屏展示速度。<!DOCTYPEhtml><html><head><title>延迟加载示例</title></head><body><h1>首屏内容</h1><!--非关键资源--><d......
  • SpringBoot项目通过注解快速解决,字典翻译,响应数据加密,数据脱敏等问题
    简介在几乎所有SpringBoot项目中都会面临字典翻译,接口数据加密,数据脱敏的问题。在每个接口中单独的解决会非常繁琐,因此接下来介绍一下怎么通过注解快速解决这些问题。实现步骤1.引入maven坐标<dependency><groupId>io.gitee.gltqe</groupId><artifactId>......
  • 视频直播app源码,利用缓存实现连续登录失败后的时间等待
    实现步骤:1、用户在视频直播app源码中发起登录请求2、后台验证是否失败次数过多,账户没有锁定的话就进入下面的步骤;否则直接返回3、验证用户的账号+密码3.1验证成功:删除缓存3.2验证失败:统计最近10分钟时间窗口内的失败次数,如果达到5次则设置锁定缓存,返回图解实......
  • MetaGPT day02: MetaGPT Role源码分析
    MetaGPT源码分析思维导图MetaGPT版本为v0.4.0,如下是frommetagpt.rolesimportRole,Role类执行Role.run时的思维导图:概述其中最重要的部分是_react,里面包含了一个循环,在循环中交替执行_think和_act,也就是让llm先思考再行动。_think中决定了llm下一个执行的动作是什么,这个动作......
  • Java21 + SpringBoot3集成WebSocket
    目录前言相关技术简介什么是WebSocketWebSocket的原理WebSocket与HTTP协议的关系WebSocket优点WebSocket应用场景实现方式添加maven依赖添加WebSocket配置类,定义ServerEndpointExporterBean定义WebSocketEndpoint前端创建WebSocket对象总结前言近日心血来潮想做一个开源项目,目......
  • TCP三次握手源码分析(服务端接收ACK&TCP连接建立完成)
    内核版本:Linux3.10内核源码地址:https://elixir.bootlin.com/linux/v3.10/source(包含各个版本内核源码,且网页可全局搜索函数)《TCP三次握手源码分析(客户端发送SYN)》《TCP三次握手源码分析(服务端接收SYN以及发送SYN+ACK)》《TCP三次握手源码分析(客户端接收SYN+ACK以及发送ACK......