SpringBoot中,错误页和异常处理定制实现原理

本文介绍了SpringBoot中如何利用Servlet规范的ErrorPage机制处理服务器错误,以及与SpringMVC异常处理的区别。重点讲解了`ErrorMvcAutoConfiguration`在配置ErrorPage中的角色,以及如何通过自定义异常解析器来控制异常显示。
摘要由CSDN通过智能技术生成
/**
 * <pre>
 *
 *     前置概念:
 *
 *          Servlet规范中定义的一种机制ErrorPage,用于处理发生在Servlet容器中的错误,当Servlet容器无法处理请求时
 *          (比如404页面不存在、500服务器错误等), 会根据配置的ErrorPage来展示相应的错误页面
 *
 *          在Tomcat处理请求的时候,出现异常就会找给Tomcat上下中注册的ErrorPage对象,其中ErrorPage中存在code,异常类型和转发的路径
 *          并且,会先根据响应错误码来找设置了一致code的ErrorPage,否则按照异常类型来找ErrorPage对象
 *          找了ErrorPage对象之后,会在request作用域中存入一大堆的数据,最终又会调用Servlet的API对ErrorPage中的location的URL进行转发
 *          RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation()).include(request.getRequest(), response.getResponse());;
 *
 *    在SpringBoot中,{@link org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration}配置了TomcatServletWebServerFactory
 *    该类就是用于创建Tomcat对象的,对Tomcat的配置都在该工厂中配置,所以,对于ErrorPage,就是在{@link org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#configureContext}完成的
 *    SpringBoot会将自身配置的ErrorPage对象,一一转换为Tomcat提供的ErrorPage对象,然后注册到Tomcat上下文中
 *
 *    而如何在SpringBoot中注册ErrorPage呢? 在SpringBoot的Web服务器的自动配置中,注入的一个{org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor}的Bean
 *    这个BeanPostProcessor在Bean的初始化前,会从Spring容器中获取ErrorPageRegistrar类型的Bean,然后通过回调ErrorPageRegistrar.registerErrorPages来注册ErrorPage
 *    而SpringBoot又提供了一个{@link org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer},注册了一个默认的ErrorPage
 *    // 接收ServerProperties的error属性中的path来配置默认的该ErrorPage转发的路径,默认为/error @Value("${error.path:/error}")
 *    ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
 *    errorPageRegistry.addErrorPages(errorPage);
 *
 *    在SpringMVC中,就只能通过web.xml中配置ErrorPage,因为web.xml是tomcat需要的配置文件,对于tomcat的配置都可以在web.xml中配置
 *
 *    如果不需要用到ErrorPage机制,那么我们可以利用SpringMVC提供的异常解析器机制,使用自定义的HandlerExceptionResolver或者SpringMVC提供的异常解析器
 *    返回ModelAndView,设置自己固定的错误页面或者动态设置页面都可以, 这样,在出现异常的情况下,SpringMVC就可以处理,不需要将异常抛到Tomcat
 *
 *
 * 在Springboot-springmvc异常处理的核心原理{@link DispatcherServlet#processHandlerException}
 *
 *
 * 最后,在SpringBoot默认提供的ErrorPage有专门的处理类{@link org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration}
 * 该类的作用:
 *      1. BasicErrorController,处理/error请求的类
 *      2. DefaultErrorAttributes该类是一个HandlerExceptionResolver,进行异常解析
 *      3. ErrorPageCustomizer: 注册ErrorPage,规定路径为/error(默认,可以修改)
 *      4. PreserveErrorControllerTargetClassPostProcessor: 该类会对所有实现了ErrorController接口的BeanDefinition中设置preserveTargetClass,使用CBLIB代理
 *      5. DefaultErrorViewResolver: 默认的错误视图解析器,可以解析error/目录下的4xx,5xx.html视图
 *      6: StaticView: /error返回的View视图,因为/error可以返回页面,json,多种情况都要兼容
 *      7: BeanNameViewResolver: 解析View(StaticView)对象的视图解析器
 * </pre>
 */
public class DispatcherServlet {
    //  请求处理的核心逻辑
    public void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 正常执行返回的结果
        ModelAndView mv = null;
        // 执行过程中是否出现异常
        Exception dispatchException = null;

        try {
            // 获取处理请求的Handler类
            mappedHandler = getHandler(processedRequest);
            // 执行Handler的方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        } catch (Exception ex) {
            // 执行过程出现异常
            dispatchException = ex;
        } catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // 处理最终的结果
        this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }

    /**
     * 处理最终结果
     *
     * @param request       请求对象
     * @param response      响应对象
     * @param mappedHandler 处理请求的handler类
     * @param mv            正确执行返回的模型和视图对象,如果出现异常则没有
     * @param exception     执行过程中的异常对象
     */
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, Exception exception) {
        // 是否存在异常的view
        boolean errorView = false;
        // 如果存在异常对象,表示出现异常了
        if (exception != null) {
            // 获取到处理接口的Handler
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            // 处理异常情况,使用异常解析器,看是否能处理该异常情况,并且返回一个模型和视图对象
            mv = this.processHandlerException(request, response, handler, exception);
            // 异常解析器如果成功返回ModelAndView,表示给定了指定的视图
            errorView = (mv != null);
        }
        // 存在ModelAndView对象
        if (mv != null && !mv.wasCleared()) {
            // 就可以调用View对象的render方法
            this.render(mv, request, response);
            // 如果存在异常的ModelAndView对象
            if (errorView) {
                // 删除保存到请求域中的异常数据,处理异常的时候保存的
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
    }

    // 视图解析渲染
    public void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 最终渲染的视图对象
        View view;
        // 获取视图名称
        String viewName = mv.getViewName();
        // 如果存在视图名,就需要根据视图解析器根据名称解析成View对象
        if (viewName != null) {
            // 根据所有的视图解析器对该名称进行解析,直到解析到View对象为止
            view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
            // 如果所有视图解析器都无法解析该视图名,抛出异常,因为没有视图对象View可以渲染
            if (view == null) {
                throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'");
            }
        } else {
            // 没有视图名,那么就可能直接在ModelAndView中设置的是View对象
            view = mv.getView();
            if (view == null) {
                throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'");
            }
        }
        // 如果设置了状态
        if (mv.getStatus() != null) {
            // 保存该状态,并且设置到响应对象中
            request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
            response.setStatus(mv.getStatus().value());
        }
        // 调用View对象的渲染流程,该转发转发,重定向重定向,自定义就自定义
        view.render(mv.getModelInternal(), request, response);
    }

    // 处理异常的核心逻辑
    public ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 最终异常解析器处理完成返回的模型视图对象
        ModelAndView exMv = null;
        if (this.handlerExceptionResolvers != null) {
            // 遍历所有的异常解析器对象
            for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
                // 执行解析异常逻辑,返回ModelAndView对象
                exMv = resolver.resolveException(request, response, handler, ex);
                // 直到有异常解析器返回ModelAndView结束
                if (exMv != null) {
                    break;
                }
            }
        }
        // 如果异常解析器解析完成,并且解析到了ModelAndView
        if (exMv != null) {
            // 返回的是否是一个空视图,并且Model中也没有数据,直接结束,也不需要干嘛了
            if (exMv.isEmpty()) {
                request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
                return null;
            }
            // 如果不存在视图View对象,要执行一些默认的操作,比如按照默认的策略找视图名
            if (!exMv.hasView()) {
                // 获取默认的视图名,默认情况url就是视图名
                String defaultViewName = getDefaultViewName(request);
                // 如果url获取到视图名
                if (defaultViewName != null) {
                    // 保存需要解析该视图对象
                    exMv.setViewName(defaultViewName);
                }
            }
            // 将异常信息保存到请求域中
            WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
            // 只要拿到ModelAndView,那么SpringMVC就会当正常逻辑来进行视图解析渲染
            return exMv;
        }
        // 如果没有视图解析能解析该异常并返回ModelAndVie对象,则继续抛出异常
        throw ex;
    }
}

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值