前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原理解读:Spring MVC统一异常处理

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

作者头像
程序猿杜小头
发布2022-12-01 21:40:53
9981
发布2022-12-01 21:40:53
举报
文章被收录于专栏:程序猿杜小头程序猿杜小头

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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿杜小头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 异常从何而来
    • 1.1 获取Handler过程中抛出异常
      • 1.2 Handler执行过程中抛出异常
      • 2 异常如何处理
      • 3 总结
      • 4 参考文章
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档