spring异常分析及处理

spring异常分析及处理

spring mvc exceptions

参考官方文档:https://docs.spring.io/spring/docs/5.2.7.RELEASE/spring-framework-reference/web.html#mvc-exceptionhandlers

如果异常在请求映射期间发生或从请求处理程序(例如@Controller)抛出,则DispatcherServlet委托给HandlerExceptionResolver Beans 来解决该异常并提供替代处理,通常是错误响应。

下表列出了可用的HandlerExceptionResolver实现:

HandlerExceptionResolver描述
SimpleMappingExceptionResolver异常类名称和错误视图名称之间的映射。对于在浏览器应用程序中呈现错误页面很有用。
DefaultHandlerExceptionResolver解决Spring MVC引发的异常,并将它们映射到HTTP状态代码。另请参见替代ResponseEntityExceptionHandlerREST API异常
ResponseStatusExceptionResolver使用@ResponseStatus注释解决异常,并根据注释中的值将其映射到HTTP状态代码。
ExceptionHandlerExceptionResolver通过调用@Controller或@ControllerAdvice类中的@ExceptionHandler方法来解决异常。请参见@ExceptionHandler方法

支持的Exceptions

javax.servlet.http.HttpServletResponse

ExceptionHTTP Status Code
HttpRequestMethodNotSupportedException405 (SC_METHOD_NOT_ALLOWED)
HttpMediaTypeNotSupportedException415 (SC_UNSUPPORTED_MEDIA_TYPE)
HttpMediaTypeNotAcceptableException406 (SC_NOT_ACCEPTABLE)
MissingPathVariableException500 (SC_INTERNAL_SERVER_ERROR)
MissingServletRequestParameterException400 (SC_BAD_REQUEST)
ServletRequestBindingException400 (SC_BAD_REQUEST)
ConversionNotSupportedException500 (SC_INTERNAL_SERVER_ERROR)
TypeMismatchException400 (SC_BAD_REQUEST)
HttpMessageNotReadableException400 (SC_BAD_REQUEST)
HttpMessageNotWritableException500 (SC_INTERNAL_SERVER_ERROR)
MethodArgumentNotValidException400 (SC_BAD_REQUEST)
MissingServletRequestPartException400 (SC_BAD_REQUEST)
BindException400 (SC_BAD_REQUEST)
NoHandlerFoundException404 (SC_NOT_FOUND)
AsyncRequestTimeoutException503 (SC_SERVICE_UNAVAILABLE)

Chain of Resolvers

您可以通过HandlerExceptionResolver 在Spring配置中声明多个bean并order根据需要设置它们的属性来形成异常解析器链。order属性越高,异常解析器的定位就越晚。

HandlerExceptionResolver 接口定义了方法:

ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

HandlerExceptionResolver中定义指定它可以返回:

  • 指定错误视图的ModelAndView.
  • 如果在程序解析中处理了异常,则为空的ModelAndView (return new ModelAndView())。
  • 如果该异常仍未解决,则为null,以供后续的解析器尝试;如果该异常仍在末尾,则允许将其冒泡到Servlet容器。

MVC Config 默认自动声明了内置的 spring MVC 异常解析器,对于带注释@ResponseStatus的异常, 和@ExceptionHandler方法的支持。您可以自定义列表或取代它。

Container Error Page

如果任何HandlerExceptionResolver异常仍未得到解决,因此,造成传播或如果响应状态设置为一个错误状态( 4xx, 5xx), Servlet容器可以在HTML渲染一个默认的错误页面。自定义容器的默认错误页面,您可以在web . xml中声明一个错误页面映射。下面的例子显示了如何这样做:

<error-page>
    <location>/error</location>
</error-page>

鉴于前面的例子,当一个异常冒泡或响应错误状态时,Servlet容器将产生的错误发送到容器内配置的URL(例如,/error)。然后由DispatcherServlet处理,可能映射到一个@controller,可实现返回一个错误视图的名字与一个model或呈现JSON响应,如以下示例所示:

@RestController
public class ErrorController {

    @RequestMapping(path = "/error")
    public Map<String, Object> handle(HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));
        return map;
    }
}

注意:

Servlet API没有提供在Java中创建错误页面映射的方法。但是,您可以同时使用WebApplicationInitializerweb.xml

常见的异常全局处理

在Spring中常见的全局异常处理,主要有三种:

(1)注解ExceptionHandler

(2)继承HandlerExceptionResolver接口

(3)注解ControllerAdvice

具体可参考:https://www.cnblogs.com/yixinjishu/p/10863529.html

这里我们以HTTP Status Code 400 和 500 作为例子进行演示,我们创建一个Controller类,如下

@RestController
@RequestMapping("echo")
public class EchoController {

    /**
     * 请求路径 http://localhost:端口/echo/get
     *
     * @param name
     * @return
     */
    @RequestMapping("get")
    public String get(@RequestParam("name") String name) {
        return "response:" + name;
    }

    /**
     * 请求路径 http://localhost:端口/echo/cal
     *
     * @return
     */
    @RequestMapping("cal")
    public int calculate() {
        int val = 100 / 0;
        return val;
    }
}

我们分别请求get和cal 返回的错误分别对应400 和 500 如下:
在这里插入图片描述
在这里插入图片描述

注解@ExceptionHandler

注解ExceptionHandler作用对象为方法,最简单的使用方法就是放在controller文件中,详细的注解定义不再介绍。如果项目中有多个controller文件,通常可以在baseController中实现ExceptionHandler的异常处理,而各个contoller继承basecontroller从而达到统一异常处理的目的。因为比较常见,简单代码在上面添加如下代码:

@ExceptionHandler(Exception.class)
public String exception(Exception e) {
	return this.getClass().getSimpleName() + ":" + e.getMessage();
}

这时候在分别请求get和cal 返回的信息如下:

EchoController:Required String parameter 'name' is not present
EchoController:/ by zero

这时候发现添加ExceptionHandler之后的结果

优点:ExceptionHandler简单易懂,并且对于异常处理没有限定方法格式;

缺点:由于ExceptionHandler仅作用于方法,对于多个controller的情况,仅为了一个方法,所有需要异常处理的controller都继承这个类,不太好。

注解@ControllerAdvice

@ControllerAdvice注解,其实是其与ExceptionHandler的组合使用。在上文中可以看到,单独使用@ExceptionHandler时,其必须在一个Controller中,然而当其与ControllerAdvice组合使用时就完全没有了这个限制。换句话说,二者的组合达到的全局的异常捕获处理,我们创建一个类GlobalExceptionHandler,然后把上面EchoController的exception方法整个注释掉,这时候在请求get和cal 返回的信息和单独用注解ExceptionHandler一致。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String exception(Exception e) {
        return this.getClass().getSimpleName() + ":" + e.getMessage();
    }
}

通过上面结果可以看到,异常处理确实已经变更为GlobalExceptionHandler类。这种方法将所有的异常处理整合到一处,去除了Controller中的继承关系,并且达到了全局捕获的效果,推荐使用此类方式。

实现HandlerExceptionResolver接口

HandlerExceptionResolver本身是SpringMVC内部的接口,其内部只有resolveException一个方法,通过实现该接口我们可以达到全局异常处理的目的。

我们创建一个类:MyHandlerExceptionResolver,同时将GlobalExceptionHandler类上的注解@ControllerAdvice注释掉。

@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, 
                                         HttpServletResponse response,
                                         Object handler, Exception ex) {
        String message = this.getClass().getSimpleName() + ":" + ex.getMessage();
        PrintWriter pw = null;
        try {
            pw = response.getWriter();
            pw.write(message);
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != pw) {
                pw.close();
            }
        }
        return new ModelAndView();
    }
}

但这里有一个问题请求get时没有生效,显示错误信息如下
在这里插入图片描述

为什么500的异常处理已经生效了,但是400的异常处理却没有生效。这是怎么回事呢?不是说可以达到了全局捕获的效果吗?这里只好跟踪Spring的源码进行分析。

Spring中异常处理源码分析

在DispatcherServlet 类中的方法doDispatch 中调用了processDispatchResult方法,processDispatchResult调用了processHandlerException方法,如下图1:
在这里插入图片描述

​ 图1
在这里插入图片描述
​ 图2

看方法processHandlerException的代码this.handlerExceptionResolvers包含3个HandlerExceptionResolver【DefaultErrorAttributes,HandlerExceptionResolverComposite,MyHandlerExceptionResolver】。在循环到HandlerExceptionResolverComposite时跳出了循环,说明了异常被Spring自带的异常给处理掉了,所以就跳过了MyHandlerExceptionResolver类的处理,从而出现400的返回结果。而对于add请求,中间没有阻拦,所以就达到了预期效果。

三类异常的处理顺序

先把结论写上在进行分析:@ExceptionHandler优先于@ControllerAdvice优先于HandlerExceptionResolver。

我们将EchoController、GlobalExceptionHandler类上面的注释的内容全部放开。

下面去Spring源码去找结论,上面提到了HandlerExceptionResolverComposite 它里面有3个HandlerExceptionResolver分别是【ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver,DefaultHandlerExceptionResolver】,如下图3:
在这里插入图片描述
图3

ExceptionHandlerExceptionResolver中的doResolveHandlerMethodException方法中调用了getExceptionHandlerMethod方法如下图4:
在这里插入图片描述
​ 图4

通过跟进,发现有两个变量可能就是问题的关键:exceptionHandlerCache和exceptionHandlerAdviceCache。首先,exceptionHandlerCache上面的注释的翻译过来意思为:

// Local exception handler methods on the controller class itself.
// 控制器类本身上的本地异常处理程序方法。
// To be invoked through the proxy, even in case of an interface-based proxy.
// 即使是基于接口的代理,也要通过代理调用。
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);

在这里插入图片描述
​ 图5

在看代码上面的method正是@ExceptionHandler标注的EchoController方法exception相吻合。而在往下的代码

是针对@ControllerAdvice 注解进行处理的逻辑,然后在结合上面分析的HandlerExceptionResolver所处HandlerExceptionResolver的索引已知,参考图1。所以得出接口@ExceptionHandler优先于@ControllerAdvice优先于HandlerExceptionResolver。

for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
			ControllerAdviceBean advice = entry.getKey();
			if (advice.isApplicableToBeanType(handlerType)) {
				ExceptionHandlerMethodResolver resolver = entry.getValue();
				Method method = resolver.resolveMethod(exception);
				if (method != null) {
					return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
				}
			}
		}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值