原理解读:Spring MVC统一异常处理

2022-12-01 21:40:53 浏览数 (1)

Running with Spring Boot v2.5.4, Java 11.0.12

当前,Spring统一异常处理机制是Java开发人员普遍使用的一种技术,在业务校验失败的时候,直接抛出业务异常即可,这明显简化了业务异常的治理流程与复杂度。值得一提的是,统一异常处理机制并不是Spring Boot提供的,而是Spring MVC,前者只是为Spring MVC自动配置了刚好够用的若干组件而已,具体配置了哪些组件,感兴趣的读者可以到spring-boot-autoconfigure模块中找到答案。

1 异常从何而来

DispatcherServlet是Spring MVC的门户,所有Http请求都会通过DispatcherServlet进行路由分发,即使Http请求的处理流程抛出了异常。doDispatch()方法是其核心逻辑,主要内容如下:

代码语言:javascript复制
public class DispatcherServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerExecutionChain mappedHandler = null;
        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
            try {
                // Determine handler for the current request.
                mappedHandler = getHandler(request);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }
                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                // Invoke all HandlerInterceptor preHandle() in HandlerExecutionChain.
                if (!mappedHandler.applyPreHandle(request, response)) {
                    return;
                }
                // Actually invoke the handler.
                mv = ha.handle(request, response, mappedHandler.getHandler());
                // Invoke all HandlerInterceptor postHandle() in HandlerExecutionChain.
                mappedHandler.applyPostHandle(request, response, mv);
            } catch (Exception ex) {
                dispatchException = ex;
            }
            // Handle the result of handler invocation, which is either a ModelAndView or an Exception to be resolved to a ModelAndView.
            processDispatchResult(request, response, mappedHandler, mv, dispatchException);
        } catch (Exception ex) {
            // Invoke all HandlerInterceptor afterCompletion() in HandlerExecutionChain.
            triggerAfterCompletion(request, response, mappedHandler, ex);
        }
    }
}

阅读上述源码可以看出如果出现了异常,会先将该异常实例赋予dispatchException这一局部变量,然后由processDispatchResult()方法负责异常处理。很明显,在doDispatch()方法内有两处容易抛出异常,第一处在为Http请求寻找相匹配的Handler过程中,Handler是什么东东?一般就是那些由@Controller@RestController标注的自定义Controller,这些Controller会由HandlerMethod包装起来;另一处就是在执行Handler的过程中。

1.1 获取Handler过程中抛出异常

获取Handler离不开HandlerMapping,由于@RequestMapping注解的广泛应用,使得RequestMappingHandlerMapping成为了一等宠臣,其继承关系如下图所示:

继承关系图清晰交代了HandlerMapping的子类AbstractHandlerMethodMapping实现了InitializingBean接口这一事实。众所周知:Spring IoC容器在构建Bean的过程中,如果当前Bean实现了InitializingBean接口,那么就会通过后者的afterPropertiesSet()方法来进行初始化操作,具体初始化逻辑如下:

代码语言:javascript复制
public class RequestMappingHandlerMapping implements RequestMappingInfoHandlerMapping {
    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
    }
}
public class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean{
    @Override
    public void afterPropertiesSet() {
        initHandlerMethods();
    }
    protected void initHandlerMethods() {
        // obtainApplicationContext().getBeanNamesForType(Object.class))
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                processCandidateBean(beanName);
            }
        }
    }
    protected void processCandidateBean(String beanName) {
        Class<?> beanType = beanType = obtainApplicationContext().getType(beanName);
        // AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)
        //  || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }
    protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass());
        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            //  private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
            //      RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
            //      RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
            //      return createRequestMappingInfo(requestMapping, condition);
            //  }
            Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup) method -> getMappingForMethod(method, userType));
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        this.mappingRegistry.register(mapping, handler, method);
    }
}
public class AbstractHandlerMethodMapping.MappingRegistry {
    private final Map<RequestMappingInfo, MappingRegistration> registry = new HashMap<>();
    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    public void register(RequestMappingInfo mapping, Object handler, Method method) {
        this.readWriteLock.writeLock().lock();
        try {
            HandlerMethod handlerMethod = createHandlerMethod(handler, method);
            Set<String> directPaths = getDirectPaths(mapping);
            for (String path : directPaths) {
                this.pathLookup.add(path, mapping);
            }
            this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths));
        } finally {
            this.readWriteLock.writeLock().unlock();
        }
    }
}

上述初始化逻辑主要为:

  1. 从ApplicationContext中获取所有Bean,遍历每一个Bean;
  2. 判断当前Bean是否含有@Controller注解(注意:@RestController依然由@Controller标注),若无则遍历下一个Bean,若有则意味着这是一个Handler;
  3. 从当前Handler中探测出所有由@RequestMapping标注的方法,然后构建出一个以Method实例为keyRequestMappingInfo实例为value的Map(注意:@GetMapping、@PostMapping等也由@RequestMapping标注);
  4. 遍历该Map,填充MappingRegistry中Map<RequestMappingInfo, MappingRegistration>类型的成员变量registry。registry中所填充的内容示例如下:
代码语言:javascript复制
{
    "registry": [
        {
            "key": {
                "RequestMappingInfo": {
                    "patternsCondition": "/crimson_typhoon/v1/fire",
                    "methodsCondition": "POST"
                }
            },
            "value": {
                "MappingRegistration": {
                    "HandlerMethod": {
                        "bean": "customExceptionHandler",
                        "beanType": "com.example.crimson_typhoon.controller.CrimsonTyphoonController",
                        "method": "com.example.crimson_typhoon.controller.CrimsonTyphoonController.v1Fire(com.example.crimson_typhoon.dto.UserDto,java.lang.Boolean)"
                    }
                }
            }
        }
    ]
}

贴了这么一大段源码,只是想说明一个事实:DispatcherServlet可以快速根据Http请求解析出Handler,因为Http请求与Handler的映射关系被预先缓存在MappingRegistry中了。

下面步入正题:在获取Handler过程中究竟是否会抛出异常?又是哪些异常呢?

根据上图,我们直接去看AbstractHandlerMethodMapping中lookupHandlerMethod()方法的逻辑,如下:

代码语言:javascript复制
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<AbstractHandlerMethodMapping.Match> matches = new ArrayList<>();
    List<RequestMappingInfo> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
    if (directPathMatches != null) {
        for (T mapping : mappings) {
            T match = getMatchingMapping(mapping, request);
            if (match != null) {
                matches.add(new AbstractHandlerMethodMapping.Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
            }
        }
    }
    if (!matches.isEmpty()) {
        // 详细决策逻辑跳过
        return 最匹配的HandlerMethod;
    } else {
        // 没找到匹配的HandlerMethod
        return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
    }
}

顺藤摸瓜,继续:

代码语言:javascript复制
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
    RequestMappingInfoHandlerMapping.PartialMatchHelper helper = new RequestMappingInfoHandlerMapping.PartialMatchHelper(infos, request);
    if (helper.hasMethodsMismatch()) {
        throw new HttpRequestMethodNotSupportedException();
    }
    if (helper.hasConsumesMismatch()) {
        throw new HttpMediaTypeNotSupportedException();
    }
    if (helper.hasProducesMismatch()) {
        throw new HttpMediaTypeNotAcceptableException();
    }
    if (helper.hasParamsMismatch()) {
        throw new UnsatisfiedServletRequestParameterException();
    }
    return null;
}

最终,抛出哪些异常还是让我们定位到了,比如大名鼎鼎的HttpRequestMethodNotSupportedException就是在这里被抛出的。


事实上,如果最终没有为Http请求寻找到相匹配的Handler,也将抛出异常,它就是NoHandlerFoundException,前提是要在application.properties配置文件中添加spring.mvc.throw-exception-if-no-handler-found=true这一项配置!

代码语言:javascript复制
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (this.throwExceptionIfNoHandlerFound) {
        throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), new ServletServerHttpRequest(request).getHeaders());
    } else {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

1.2 Handler执行过程中抛出异常

Handler执行过程中抛出的异常比较宽泛,一般可以归纳为两种:一种是执行Handler后抛出的异常,比如:业务逻辑层中未知的运行时异常和开发人员自定义的异常;另一种是还未开始执行Handler,而是在为其方法参数进行数据绑定时抛出的异常,比如:BindingException及其子类MethodArgumentNotValidException。大家可能对MethodArgumentNotValidException尤为熟悉,常见的异常抛出场景如下所示:

代码语言:javascript复制
@RestController
@RequestMapping(path = "/crimson_typhoon")
public class CrimsonTyphoonController {
    @PostMapping(path = "/v1/fire")
    public Map<String, Object> v1Fire(@RequestBody @Valid UserDto userDto, @RequestParam("dryRun") Boolean dryRun) {
        return ImmutableMap.of("status", "success", "code", 200, "data", ImmutableList.of(userDto));
    }
}

public class UserDto {
    @NotBlank
    private String name;
    @NotNull
    private int age;
}

如果调用方传递的请求体参数不符合Bean Validation的约束规则,那么就会抛出MethodArgumentNotValidException异常。

2 异常如何处理

无论是在获取Handler过程中、在为Handler的方法参数进行数据绑定过程中亦或在Handler执行过程中出现了异常,总是会先将该异常实例赋予dispatchException这一局部变量,然后由processDispatchResult()方法负责异常处理。下面来看看DispatcherServlet中processDispatchResult()方法是究竟如何处理异常的,源码逻辑很直白,最终是将异常委派给HandlerExceptionResolver处理的,如下:

代码语言:javascript复制
public class DispatcherServlet {
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                       HandlerExecutionChain mappedHandler,
                                       ModelAndView mv, Exception exception) {
        boolean errorView = false;
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            } else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }
        if (mv != null && !mv.wasCleared()) {
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
    }
    protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelAndView exMv = null;
        if (this.handlerExceptionResolvers != null) {
            for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
                exMv = resolver.resolveException(request, response, handler, ex);
                if (exMv != null) {
                    break;
                }
            }
        }
        if (exMv != null) {
            if (exMv.isEmpty()) {
                request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
                return null;
            }
            if (!exMv.hasView()) {
                String defaultViewName = getDefaultViewName(request);
                if (defaultViewName != null) {
                    exMv.setViewName(defaultViewName);
                }
            }
            WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
            return exMv;
        }
        throw ex;
    }
}

主角登场!HandlerExceptionResolver是一个函数式接口,即有且只有一个resolveException()方法:

代码语言:javascript复制
public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}

HandlerExceptionResolver与HandlerMappingHandlerAdapter等类似,一般不需要开发人员自行定义,Spring MVC默认会提供一些不同风格的HandlerExceptionResolver,这些HandlerExceptionResolver会通过initHandlerExceptionResolvers()方法被提前填充到DispatcherServlet中handlerExceptionResolvers这一成员变量中,具体地:

代码语言:javascript复制
private void initHandlerExceptionResolvers(ApplicationContext context) {
    if (this.detectAllHandlerExceptionResolvers) {
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    }
}

HandlerExceptionResolverComposite是DispatcherServlet中handlerExceptionResolvers这一成员变量所持有的最重要的异常解析器,Composite后缀表明这是一个复合类,自然会通过其成员变量持有若干HandlerExceptionResolver类型的苦力小弟,而在这众多苦力小弟中最为重要的非ExceptionHandlerExceptionResolver异常解析器莫属!查阅其源码后发现它也实现了InitializingBean接口:

代码语言:javascript复制
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
        implements ApplicationContextAware, InitializingBean {
 private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
   new ConcurrentHashMap<>(64);
 private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
   new LinkedHashMap<>();  
  
    @Override
    public void afterPropertiesSet() {
        initExceptionHandlerAdviceCache();
    }
    private void initExceptionHandlerAdviceCache() {
        // ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class)
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
            if (resolver.hasExceptionMappings()) {
                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
            }
        }
    }
}
public class ExceptionHandlerMethodResolver {
    public static final ReflectionUtils.MethodFilter EXCEPTION_HANDLER_METHODS = method ->
            AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
    private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
    
    public ExceptionHandlerMethodResolver(Class<?> handlerType) {
        for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
            for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
                this.mappedMethods.put(exceptionType, method);
            }
        }
    }
    private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
        List<Class<? extends Throwable>> result = new ArrayList<>();
        ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
        result.addAll(Arrays.asList(ann.value()));
        if (result.isEmpty()) {
            for (Class<?> paramType : method.getParameterTypes()) {
                if (Throwable.class.isAssignableFrom(paramType)) {
                    result.add((Class<? extends Throwable>) paramType);
                }
            }
        }
        return result;
    }
}

上述关于ExceptionHandlerExceptionResolver的初始化逻辑很清晰:首先,从IoC容器中获取所有由@ControllerAdvice注解接口标注的Bean,这个Bean一般就是我们平时自定义的全局异常统一处理器;然后,逐一遍历这些全局异常处理器Bean,将其作为ExceptionHandlerMethodResolver构造方法的参数,后者会解析出含有@ExceptionHandler注解的异常处理方法,按照以Class<? extends Throwable>实例为key、以Method实例为value的映射规则填充其成员变量mappedMethods;最后,ExceptionHandlerExceptionResolver再按照以ControllerAdviceBean实例为key、以ExceptionHandlerMethodResolver实例为value的映射规则填充其成员变量exceptionHandlerAdviceCache。本文为了更直观地展示这种映射关系,笔者这里通过JSON来表达:

代码语言:javascript复制
{
    "exceptionHandlerAdviceCache": {
        "key": {
            "ControllerAdviceBean": {
                "beanName": "customExceptionHandler",
                "beanType": "com.example.crimson_typhoon.config.exception.CustomExceptionHandler"
            }
        },
        "value": {
            "ExceptionHandlerMethodResolver": {
                "mappedMethods": [
                    {
                        "key": "class java.lang.Exception",
                        "value": "public org.springframework.http.ResponseEntity com.example.crimson_typhoon.config.exception.CustomExceptionHandler.handleUnknownException(Exception)"
                    },
                    {
                        "key": "class java.lang.NullPointerException",
                        "value": "public org.springframework.http.ResponseEntity com.example.crimson_typhoon.config.exception.CustomExceptionHandler.handleNullPointerException(NullPointerException)"
                    }
                ]
            }
        }
    }
} 

ExceptionHandlerExceptionResolver的初始化用意与RequestMappingHandlerMapping一致,也是为了提前缓存,这样后期可以快速地根据异常获取相匹配的@ExceptionHandler异常处理方法

通过分析ExceptionHandlerExceptionResolver的初始化逻辑,大家应该明白了为什么它是最为重要的一个异常解析器,因为它与由@ControllerAdvice标注的统一异常处理器息息相关。此外,大家不要把ExceptionHandlerExceptionResolver和ExceptionHandlerMethodResolver搞混淆了,从后者名称来看,它只是一个面向@ExceptionHandler注解的方法解析器,压根不会解析异常哈。


下面回过头来看看HandlerExceptionResolverComposite中的逻辑,核心内容如下:

代码语言:javascript复制
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
    private List<HandlerExceptionResolver> resolvers;
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (this.resolvers != null) {
            for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
                ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
                if (mav != null) {
                    return mav;
                }
            }
        }
        return null;
    }
}

上述源码说明:HandlerExceptionResolverComposite会让其持有的异常解析器逐一解析异常,如果谁能返回一个非空的ModelAndView实例对象,那么谁就是赢家;绝大多数情况下,都是ExceptionHandlerExceptionResolver获得最后的胜利。ExceptionHandlerExceptionResolver中的异常解析逻辑在doResolveHandlerMethodException()方法中:

代码语言:javascript复制
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
        implements InitializingBean {
    @Override
    protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
        ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
        if (exceptionHandlerMethod == null) {
            return null;
        }
        if (this.argumentResolvers != null) {
            exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        if (this.returnValueHandlers != null) {
            exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }

        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();

        ArrayList<Throwable> exceptions = new ArrayList<>();
        Throwable exToExpose = exception;
        while (exToExpose != null) {
            exceptions.add(exToExpose);
            Throwable cause = exToExpose.getCause();
            exToExpose = (cause != exToExpose ? cause : null);
        }
        Object[] arguments = new Object[exceptions.size()   1];
        exceptions.toArray(arguments);
        arguments[arguments.length - 1] = handlerMethod;
        exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);

        if (mavContainer.isRequestHandled()) {
            return new ModelAndView();
        } else {
            ModelMap model = mavContainer.getModel();
            HttpStatus status = mavContainer.getStatus();
            ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
            mav.setViewName(mavContainer.getViewName());
            if (!mavContainer.isViewReference()) {
                mav.setView((View) mavContainer.getView());
            }
            if (model instanceof RedirectAttributes) {
                Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
                RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
            }
            return mav;
        }
    }
}

从上述ExceptionHandlerExceptionResolver的源码中,最终看到了执行@ExceptionHandler异常处理方法的身影,与执行Handler中目标方法的原理一致,都是通过反射调用的,不再赘述。这里必须要重点看一下getExceptionHandlerMethod()方法的逻辑,如下:

代码语言:javascript复制
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
        implements InitializingBean {
    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
        Class<?> handlerType = null;
        if (handlerMethod != null) {
            handlerType = handlerMethod.getBeanType();
            ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
            if (resolver == null) {
                resolver = new ExceptionHandlerMethodResolver(handlerType);
                this.exceptionHandlerCache.put(handlerType, resolver);
            }
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
            }
            if (Proxy.isProxyClass(handlerType)) {
                handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
            }
        }
        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);
                }
            }
        }
        return null;
    }
}

在刚才介绍ExceptionHandlerExceptionResolver的初始化逻辑时已经提到了:其成员变量缓存了ControllerAdviceBean与ExceptionHandlerMethodResolver的映射关系。可是这个成员变量却在最后时刻才被遍历,这是为什么呢?原来,ExceptionHandlerExceptionResolver并不会首先从统一异常处理器中寻找@ExceptionHandler异常处理方法,而是先从当前Handler中查找,找到之后缓存在其另一个成员变量exceptionHandlerCache中。


最后,再介绍一个容易被忽略的知识点。回忆一下,当我们访问服务中不存在的API时,往往会响应一种奇怪的格式;之所以奇怪,是因为咱们平时都会定制化API的响应格式,而此时的响应格式与咱们定制化的格式不匹配,这是咋回事呢?如下所示:

代码语言:javascript复制
{
    "timestamp": "2021-12-06T13:51:34.063 00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/crimson_typhoon/v4/fire"
}

这是因为在根据Http请求获取Handler时,常规的Handler是不可能匹配到了,只能由ResourceHttpRequestHandler这一个HttpRequestHandler来兜底,它在通过handleRequest()方法处理该Http请求时发现自己也搞不定,于是就只能将其转发给Servlet容器中默认的Error Page处理了。只需通过response.sendError(HttpServletResponse.SC_NOT_FOUND)Response中的errorState这一成员变量的值置为1,那么Servlet容器就会乖乖地进行服务端转发操作。Error Page会由Spring Boot注册到Servlet容器中,它就是BasicErrorController,具体内容如下:

代码语言:javascript复制
package org.springframework.boot.autoconfigure.web.servlet.error;

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    private final ErrorProperties errorProperties;

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }
}

如果你有强迫症,就是忍不了响应格式不统一的现象,那你可以像下面这样做:

代码语言:javascript复制
@Configuration
public class CustomErrorHandlerConfig {
    @Resource
    private ServerProperties serverProperties;
    @Bean
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        return new BasicErrorController(errorAttributes, serverProperties.getError()) {
            @Override
            public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
                HttpStatus status = getStatus(request);
                Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
                Map<String, Object> finalBody = ImmutableMap.of("status", body.get("error"), "code", status.value(), "data", List.of());
                return ResponseEntity.ok(finalBody);
            }
        };
    }
}

然后响应内容就变了,具体响应格式参照各自项目规范修改即可:

代码语言:javascript复制
{
    "status": "Not Found",
    "code": 404,
    "data": []
}

3 总结

聊到统一异常治理,自然要对治理对象的分类有一个清晰的认知。异常也就两类:未知异常已知异常。未知异常多半是由隐藏的BUG造成的,笔者认为统一异常处理层一定要有针对未知异常的处理逻辑,直白点说就是在由@RestControllerAdvice标注的统一异常处理类中要有一个由@ExceptionHandler(value = Exception.class)标注的方法,但千万不要通过getMessage()将异常信息反馈给调用方,因为异常是未知的,可能会将很长串的异常堆栈信息暴漏出来,这样既不友好也不安全,建议反馈简短的信息即可,比如:Internal Server Error,但要在日志中完整地记录异常堆栈信息,方便后期排查。已知异常的范围比较宽泛,针对已知异常,向调用方暴漏的错误信息一定要简洁清晰,这也是完全可以做到的,尤其是开发人员主动抛出的自定义异常,这类异常在统一异常处理层中可以放心大胆地通过getMessage()方式将异常信息反馈给调用方或前台用户,因为开发人员在抛出异常的时候会填充简短精炼的提示信息。

关于最佳实践思路,建议大家自定义的统一异常处理器能够继承ResponseEntityExceptionHandler,大家可以去看看它的源码就知道为什么这么建议了!

4 参考文章

  1. https://docs.spring.io/spring-framework/docs/5.3.9/reference/html/web.html#mvc-exceptionhandlers
  2. https://docs.spring.io/spring-boot/docs/2.5.4/reference/html/features.html#features.developing-web-applications.spring-mvc.error-handling

0 人点赞