진시 추천 | [스프링의 원리와 실전을 간단하게 설명] "하단 소스코드 분석"은 소스코드의 관점에서 스프링의 예외처리 ExceptionHandler 구현 원리를 심도 있게 분석한다.

이 글은 "골든스톤 프로젝트" 에 참여하고 있습니다.

ExceptionHandler의 역할

ExceptionHandler는 애플리케이션의 예외를 처리하기 위해 Spring 프레임워크에서 제공하는 주석입니다. 애플리케이션에서 예외가 발생하면 ExceptionHandler가 우선적으로 예외를 가로채 처리한 다음 처리 결과를 프런트 엔드로 반환합니다. 이 주석은 클래스 수준 및 메서드 수준에서 다양한 수준의 예외를 포착하는 데 사용할 수 있습니다.

Spring에서 ExceptionHandler를 사용하는 것은 매우 간단합니다. 예외를 포착해야 하는 메서드에 @ExceptionHandler를 주석으로 추가한 다음 예외를 수신하고 예외 정보를 반환하고 프런트 엔드 사용자에게 예외 정보를 표시할 메서드를 정의하기만 하면 됩니다.

ExceptionHandler 사용

설명: 문제가 있을 수 있는 Controller의 경우 @ExceptionHandler 주석 메서드를 추가합니다. 다음은 기본 ExceptionHandler 예제입니다 .

@RestController
public class ExceptionController {
	
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("An error occurred: " + ex.getMessage());
    }
    @RequestMapping("/test")
    public String test() throws Exception {
        throw new Exception("Test exception!");
    }
}
复制代码

위의 예에서 우리는 **@RestController** 주석이 달린 컨트롤러인 ExceptionController라는 클래스를 정의했습니다. 여기에는 예외를 생성할 수 있는 요청 처리기와 예외를

@RequestMapping 주석은 위의 ExceptionHandler에서 처리할 예외를 발생시키는 "/test"라는 API를 구성합니다. "/test"가 요청되면 Controller 메서드는 예외를 발생시키고 @ExceptionHandler 메서드를 트리거합니다.

위의 @ExceptionHandler 메소드에서는 ResponseEntity를 통해 클라이언트에게 예외 정보를 제공하고 있으며 HTTP 상태 코드는 500으로 설정되어 있습니다. 이를 통해 클라이언트는 오류가 발생했음을 알 수 있고 나중에 디버깅할 수 있도록 예외 정보를 기록할 수 있습니다.

总之,使用ExceptionHandler能够更好的掌控应用的异常信息,使得应用在发生异常的时候更加可控,并且更加容易进行调试

ExceptionHandler的注意事项

  • Controller类下多个**@ExceptionHandler**上的异常类型不能出现一样的,否则运行时抛异常。

  • @ExceptionHandler下方法返回值类型支持多种,常见的ModelAndView,@ResponseBody注解标注,ResponseEntity等类型都OK.

源码分析介绍

原理说明-doDispatch

代码片段位于:org.springframework.web.servlet.DispatcherServlet#doDispatch

执行**@RequestMapping方法抛出异常后,Spring框架 try-catch的方法捕获异常, 正常逻辑发不发生异常都会走processDispatchResult**流程 ,区别在于异常的参数是否为null .

	HandlerExecutionChain mappedHandler = null;
	Exception dispatchException = null;
	ModelAndView mv = null;
    try{
        //根据请求查找handlerMapping找到controller
        mappedHandler=getHandler(request); 
        //找到处理器适配器HandlerAdapter
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 
        if(!mappedHandler.applyPreHandle(request,response)){ 
            //拦截器preHandle
            return ;
        }      
        //调用处理器适配器执行@RequestMapping方法
        mv=ha.handle(request,response); 
        //拦截器postHandle
        mappedHandler.applyPostHandle(request,response,mv);  
    }catch(Exception ex){
        dispatchException=ex;
    }
    //将异常信息传入了
    processDispatchResult(request,response,mappedHandler,mv,dispatchException) 
复制代码

原理说明-processDispatchResult

代码片段位于:org.springframework.web.servlet.DispatcherServlet#processDispatchResult

如果 @RequestMapping 方法抛出异常,拦截器的postHandle方法不执行,进入processDispatchResult,判断入参dispatchException,不为null , 代表发生异常,调用processHandlerException处理。

原理说明-processHandlerException

代码片段位于:org.springframework.web.servlet.DispatcherServlet#processHandlerException 여기에 이미지 설명 삽입

this当前对象指dispatchServlet,handlerExceptionResolvers可以看到三个HandlerExceptionResolver,这三个是Spring框架帮我们注册的,遍历有序集合handlerExceptionResolvers,调用接口的resolveException方法。

여기에 이미지 설명 삽입

注册的第一个HandlerExceptionResolver.ExceptionHandlerExceptionResolver, 继承关系如下面所示。

여기에 이미지 설명 삽입

原理说明-AbstractHandlerExceptionResolver

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#resolveException

여기에 이미지 설명 삽입 这里AbstractHandlerExceptionResolver的shouldApplyTo都返回true, logException用来记录日志、prepareResponse方法,用来设置response的Cache-Control。 여기에 이미지 설명 삽입 异常处理方法就位于doResolveException 여기에 이미지 설명 삽입 여기에 이미지 설명 삽입

注意:AbstractHandlerExceptionResolver和AbstractHandlerMethodExceptionResolver名字看起来非常相似,但是作用不同,一个是面向整个类的,一个是面向方法级别的。

原理说明-AbstractHandlerMethodExceptionResolver

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#shouldApplyTo

接口方法实现AbstractHandlerExceptionResolver的resolveException,先判断shouldApplyTo,AbstractHandlerExceptionResolver 和子类AbstractHandlerMethodExceptionResolver都实现了shouldApplyTo方法,子类的shouldApplyTo都调用父类AbstractHandlerExceptionResolver的shouldApplyTo.

父类AbstractHandlerExceptionResolver的shouldApplyTo方法.

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#shouldApplyTo

Spring初始化的时候并没有额外配置 , 所以mappedHandlers和mappedHandlerClasses都为null, 可以在这块扩展进行筛选 ,AbstractHandlerExceptionResolver提供了setMappedHandlerClasses 、setMappedHandlers用于扩展。

doResolveException

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#doResolveException 여기에 이미지 설명 삽입 Spring请求方法执行一样的处理方式,设置argumentResolvers、returnValueHandlers,之后进行调用异常处理方法。 여기에 이미지 설명 삽입

获取@ExceptionHandler

@ExceptionHandler的方法入参支持:Exception ;SessionAttribute 、 RequestAttribute注解、 HttpServletRequest 、HttpServletResponse、HttpSession。 여기에 이미지 설명 삽입 @ExceptionHandler方法返回值常见的可以是: ModelAndView 、@ResponseBody注解、ResponseEntity。

getExceptionHandlerMethod方法

getExceptionHandlerMethod说明: 获取对应的@ExceptionHandler方法,封装成ServletInvocableHandlerMethod返回。

exceptionHandlerCache是针对Controller层面的@ExceptionHandler的处理方式,而exceptionHandlerAdviceCache是针对@ControllerAdvice的处理方式. 这两个属性都位于ExceptionHandlerExceptionResolver中。

여기에 이미지 설명 삽입

ExceptionHandlerMethodResolver,缓存A之前没存储过Controller的class ,所以新建一个ExceptionHandlerMethodResolver 加入缓存中,ExceptionHandlerMethodResolver 的初始化工作一定做了某些工作。 여기에 이미지 설명 삽입

resolveMethod方法

根据异常对象让 ExceptionHandlerMethodResolver 解析得到 method , 匹配到异常处理方法就直接封装成对象 ServletInvocableHandlerMethod ; 就不会再去走@ControllerAdvice里的异常处理器了,这里说明了。

여기에 이미지 설명 삽입

resolveMethodByExceptionType根据当前抛出异常寻找 匹配的方法,并且做了缓存,以后遇到同样的异常可以直接走缓存取出 여기에 이미지 설명 삽입 resolveMethodByExceptionType方法,尝试从缓存A:exceptionLookupCache中根据异常class类型获取Method ,初始时候肯定缓存为空 ,就去遍历ExceptionHandlerMethodResolver的mappedMethods(上面提及了key为异常类型,value为method,exceptionType为当前@RequestMapping方法抛出的异常,判断当前异常类型是不是@ExceptionHandler中value声明的子类或本身,满足条件就代表匹配上了; 여기에 이미지 설명 삽입

可能存在多个匹配的方法,使用ExceptionDepthComparator排序,排序规则是按照继承顺序来(继承关系越靠近数值越小,当前类最小为0,顶级父类Throwable为int最大值),排序之后选取继承关系最靠近的那个,并且ExceptionHandlerMethodResolver的exceptionLookupCache中,key为当前抛出的异常,value为解析出来的匹配method.

全局级别异常处理器实现HandlerExceptionResolver接口

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {

    @Override

    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",ex.getMessage());
        return new ModelAndView("error",mmp);
    }
}
复制代码
  • 使用方式: 只需要将该Bean加入到Spring容器,可以通过Xml配置,也可以通过注解方式加入容器;
    方法返回值不为null才有意义,如果方法返回值为null,可能异常就没有被捕获.

  • 缺点分析:比如这种方式全局异常处理返回JSP、velocity等视图比较方便,返回json或者xml等格式的响应就需要自己实现了.如下是我实现的发生全局异常返回JSON的简单例子.

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生全局异常!");
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",ex.getMessage());
        response.addHeader("Content-Type","application/json;charset=UTF-8");
        try {
            new ObjectMapper().writeValue(response.getWriter(),ex.getMessage());
            response.getWriter().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}
复制代码

全局级别异常处理器@ControllerAdvice+@ExceptionHandler使用方法

用法说明:这种情况下 @ExceptionHandler与第一种方式用法相同,返回值支持ModelAndView,@ResponseBody等多种形式。

@ControllerAdvice
public class GlobalController {
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView fix1(Exception e){
        System.out.println("全局的异常处理器");
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",e);
        return new ModelAndView("error",mmp);
    }
}
复制代码
  • 方式一:提到ExceptionHandlerExceptionResolver不仅维护@Controller级别的@ExceptionHandler,同时还维护的@ControllerAdvice级别的@ExceptionHandler代码片段位于: 여기에 이미지 설명 삽입 isApplicableToBeanType方法是用来做条件判断的,@ControllerAdvice注解有很多属性用来设置条件, basePackageClasses、assignableTypes、annotations等,比如我限定了annotations为注解X, 那标注了@X 的ControllerA就可以走这个异常处理器,ControllerB就不能走这个异常处理器。

现在问题的关键就只剩下了exceptionHandlerAdviceCache是什么时候扫描@ControllerAdvice的,下面的逻辑和@ExceptionHandler的逻辑一样了,exceptionHandlerAdviceCache初始化逻辑:

代码片段位于:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#afterPropertiesSet,afterPropertiesSet是Spring bean创建过程中一个重要环节。 여기에 이미지 설명 삽입


代码片段位于:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache

여기에 이미지 설명 삽입

ControllerAdviceBean.findAnnotatedBeans方法查找了SpringMvc父子容器中标注 @ControllerAdvice 的bean, new ExceptionHandlerMethodResolver初始化时候解析了当前的@ControllerAdvice的bean的@ExceptionHandler,加入到ExceptionHandlerExceptionResolver的exceptionHandlerAdviceCache中,key为ControllerAdviceBean,value为ExceptionHandlerMethodResolver . 到这里exceptionHandlerAdviceCache就初始化完毕。

Spring父子容器中所有@ControllerAdivce的bean的方法

代码片段位于:org.springframework.web.method.ControllerAdviceBean#findAnnotatedBeans

遍历了SpringMVC父子容器中所有的bean,标注ControllerAdvice注解的bean加入集合返回。

比较说明

@Controller+@ExceptionHandler、HandlerExceptionResolver接口形式、@ControllerAdvice+@ExceptionHandler优缺点说明:

调用优先级

  • @Controller+@ExceptionHandler优先级最高
  • @ControllerAdvice+@ExceptionHandler 略低
  • HandlerExceptionResolver最低。

三种方式并存的情况 优先级越高的越先选择,而且被一个捕获处理了就不去执行其他的

三种方式都支持多种返回类型

  • @Controller+@ExceptionHandler, @ControllerAdvice+@ExceptionHandler는 Spring에서 지원하는 @ResponseBody와 ResponseEntity를 사용할 수 있다.

  • HandlerExceptionResolver 메소드는 리턴 값 타입이 ModelAndView만 될 수 있음을 선언하며, JSON, xml 등을 리턴해야 하는 경우 직접 구현해야 합니다.

캐시 활용

  • @Controller+@ExceptionHandler의 캐시 정보는 ExceptionHandlerExceptionResolver의 exceptionHandlerCache에, @ControllerAdvice+@ExceptionHandler의 캐시 정보는 ExceptionHandlerExceptionResolver의 exceptionHandlerAdviceCache에 있다.

  • HandlerExceptionResolver 인터페이스는 캐싱을 수행하지 않으며 예외가 보고될 때 자체 HandlerExceptionResolver 구현 클래스만 사용하므로 다소 성능이 소모됩니다.

추천

출처juejin.im/post/7219908995338715191