SpringMVC之异常处理机制

SpringBoot异常处理机制

默认异常处理机制

springboot默认提供了一个处理/error的handler,全局异常处理。

对于机器客户端来说,产生JSON(具体的错误)、状态码和异常信息;

对于浏览器来说,产生一个白页同时附带上html错误信息;

为了自定义,还可以利用视图来解析和错误。

浏览器端响应页面

机器端响应JSON

对比一下,二者错误信息十分类似。

SpringBoot提供利用自定义页面处理异常

可以自定义页面在错误发生的时候响应到该页面上来

放在public/templates目录下的error目录下的页面来进行渲染响应,而且也说明了如果是以5开头的错误,都可以进入到5xx这个页面进行渲染响应,同理4xx也是可以的。

默认异常处理机制源码

看一下springboot中提供的ErrorMvcAutoConfiguration异常自动配置原理

首先放入了两个组件:DefaultErrorAttributes和BasicErrorController

BasicErrorController

看一下对应的类的信息,默认在server.error.path没有配置的时候,BasicErrorController默认处理的是/error请求。

然后接着看下面的handlermethod

存在着两种处理方式:一个是html,另外一个是json格式的数据。

对于html页面来说,默认响应的页面是error页面。

除此之外,还会放置一个默认的error视图bean和一个处理error的视图解析器,这个视图解析器是按照bean的名称来进行解析的。

也就是说,如果发送了/error请求,会从容器中根据/error作为组件ID来找(利用BeanNameViewResolver来找到的),然后进行渲染。

所以视图长什么样子,就渲染成什么样子。默认放置的view是StaticView,看看类中的渲染方式:

这里是默认的渲染页面风格

上面的只是响应页面,但是也可以响应json。

这里是根据内容协商原理来进行判断的。

也就是说如果希望的是响应JSON,那么将会调用BasicErrorController的error方法来处理当前请求,然后根据响应json还是页面来进行确定是否使用默认的视图解析器来进行响应。

DefaultErrorViewResolver

默认错误视图解析器

从这里可以知道官网中说明的支持4xx和5xx的大概逻辑了。

那么看一下具体的实现逻辑:

根据状态码来决定view。可以精确匹配,也可以模糊匹配。

如状态码是405,那么对应的页面是/error/405.html,也可以是/errot/4xx.html

从这里可以看到/error下面,然后加上错误错误码对应的详细信息来匹配到对应的viewname作为ModelAndView中的view对象。

DefaultErrorAttributes

从这里可以看到默认的错误属性,然后联想到响应出去的json和html中的信息都应该保存在哪个地方呢?就是这个地方了。

响应的错误信息会保存到reqeust作用域对象中来,然后在响应的时候会根据具体信息来进行删除。

这里只是简单的分析,但是具体是如何联动起来的,再来进行分析。

异常处理步骤

从例子开始,先来写一个案例来进行说明:

@RestController
public class ExceptionController {
    
    @RequestMapping("/teste")
    public String testException(){
        int i = 1 / 0;
        return "success";
    }
    
}

访问的时候,下面这行代码肯定会出现问题:

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

然后看看里面是如何来进行处理的:

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);		
				.............
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
				...........
				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

目标方法执行期间,出现了任何异常都会被之后,然后肯定会来到processDispatchResult方法中来进行处理异常。

这里也说明了什么?说明了controller中出现了异常信息,那么会有异常处理器来进行处理请求。

开始来处理Handler中出现的异常。

上来就是先把request作用域中设置的响应数据格式给清空掉,从新匹配新的数据格式。

开始来找异常处理器来处理请求,如果找到异常处理器能够处理请求,那么返回ModelAndView;如果没有找到,那么将再次抛出异常。

看看遍历异常处理器如何处理的

遍历异常处理器处理

DefaultErrorAttributes

实现了HandlerExceptionResolver接口

这个类的出来方式就是只做存储,返回null

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) {
    storeErrorAttributes(request, ex);
    return null;
}

将异常信息存储起来。

ExceptionHandlerExceptionResolver

用来处理方法上加了@ExceptionHandlerException注解信息的。

类似如下:

@ControllerAdvice
public class ExceptionControllerAdvice {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionControllerAdvice.class);

    public ExceptionControllerAdvice() {
    }

    @ExceptionHandler({Throwable.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public CommonHttpEntity<Object> buildErrorMessage(Throwable t) {
        if (t instanceof ServiceException) {
            ServiceException serviceException = (ServiceException) t;
            return CommonHttpEntity.buildFailResponse(serviceException);
        } else {
            logger.error("未捕获异常", t);
            return CommonHttpEntity.buildFailResponse(ServiceExeceptionEnum.F999.buildServiceException());
        }
    }
}

那么就会进入到下面的响应中来

这么做的目的在于不管是否发生了错误,那么都将会来响应JSON数据。

这个是我们最终需要采用的数据格式。

ResponseStatusExceptionResolver

如果在方法上标注了@ResponseStatus那么这里将会来获取得到信息。

这里处理方式和ExceptionHandlerExceptionResolver是相同的处理逻辑。

DefaultHandlerExceptionResolver

和上面是相同的处理逻辑

没有任何异常处理器处理

将会抛出异常,进入到拦截器的最终拦截方法中来,做没有意义的事情。

但是此时此刻,springmvc将会再发一个/error的请求,那么将会由BasicErrorController中来进行请求。

又因为对于客户端来说(浏览器)发送的Accep字段中接收text的权重比较高,所以是响应页面的。

默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html

模板引擎最终响应这个页面 error/500.html

SpringBoot处理处理最佳方案

总结一下springboot中可以使用的几种方式:

  • 1、自定义错误页

    • error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  • 2、 @ControllerAdvice+@ExceptionHandler处理全局异常

    • 底层是 ExceptionHandlerExceptionResolver 支持的
  • 3、@ResponseStatus+自定义异常

    • 底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
  • 4、Spring底层的异常

    • 如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

推荐使用第二种方式

@ControllerAdvice
public class ExceptionControllerAdvice {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionControllerAdvice.class);

    public ExceptionControllerAdvice() {
    }

    @ExceptionHandler({Throwable.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public CommonHttpEntity<Object> buildErrorMessage(Throwable t) {
        if (t instanceof ServiceException) {
            ServiceException serviceException = (ServiceException) t;
            return CommonHttpEntity.buildFailResponse(serviceException);
        } else {
            logger.error("未捕获异常", t);
            return CommonHttpEntity.buildFailResponse(ServiceExeceptionEnum.F999.buildServiceException());
        }
    }
}
posted @ 2022-10-06 18:43  雩娄的木子  阅读(379)  评论(0编辑  收藏  举报