springboot情操陶冶-web配置(五)

微信扫一扫,分享到朋友圈

springboot情操陶冶-web配置(五)

本文讲讲mvc的异常处理机制,方便查阅以及编写合理的异常响应方式

入口例子

很简单,根据之前的文章,我们只需要复写 WebMvcConfigurer
接口的异常添加方法即可,如下

1.创建简单的异常处理类,本例针对绑定异常

package com.example.demo.web.validation;

import com.example.demo.web.model.ResEntity;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author nanco
 * -------------
 * resolve bindexception
 * -------------
 * @create 18/9/9
 */
public class SimpleExceptionResolver extends AbstractHandlerExceptionResolver {

    private static final Logger EXCEPTION_LOG = LoggerFactory.getLogger(SimpleExceptionResolver.class);

    private final Map<String, List> errorResultMap = new HashMap(2);

    private final String ERROR_KEY = "error_result";

    private Gson gson = new Gson();

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // only process BindException,unless return null to allow the next handler understanding the exception
        if (BindException.class.isInstance(ex)) {
            ResEntity resEntity = new ResEntity();
            try {
                BindException bindException = BindException.class.cast(ex);
                List allErrors = bindException.getAllErrors();

                List resMessages = new ArrayList(allErrors.size());
                allErrors.stream().forEach(error -> {
                    resMessages.add(error.getDefaultMessage());
                });

                errorResultMap.put(ERROR_KEY, resMessages);

                resEntity.setData(errorResultMap);

                response.getOutputStream().write(gson.toJson(resEntity).getBytes());
            } catch (IOException e) {
                EXCEPTION_LOG.error("process BindException fail.", e);
            }

            return new ModelAndView();
        }
        return null;
    }
}

2.实现 WebMvcConfigurer
接口后复写其中的 extendHandlerExceptionResolvers()
方法

package com.example.demo.web.config;

import com.example.demo.web.validation.SimpleExceptionResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * @author nanco
 * -------------
 * color the mvc config
 * -------------
 * @create 2018/9/5
 **/
@Configuration
public class BootWebMvcConfigurer implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter> converters) {

    }

    @Override
    public void extendHandlerExceptionResolvers(List resolvers) {
        // response first
        resolvers.add(0, new SimpleExceptionResolver());
    }
}

上述简单的代码便会对系统抛出的 BindException
异常进行针对性的处理,从而返回合乎格式的响应体。当然这只是一小部分,笔者可以稍微从源码的角度来分析下spring的异常机制

源码层

查阅过 DispatcherServlet
源码的都知道,当出现异常的时候,则会尝试调用 HandlerExceptionResolver
解析器去根据异常进行视图渲染或者直接返回对应的错误信息。笔者按步骤来进行简单分析,从 WebMvcConfigurationSupport
入手

1.异常解析器注册

@Bean
    public HandlerExceptionResolver handlerExceptionResolver() {
        List exceptionResolvers = new ArrayList();
        // 优先加载用户自定义的异常解析器,也可通过WebMvcConfigurer来复写
        configureHandlerExceptionResolvers(exceptionResolvers);
        // 当用户没有复写上述方法后,采取默认的异常解析器
        if (exceptionResolvers.isEmpty()) {
            addDefaultHandlerExceptionResolvers(exceptionResolvers);
        }
        // 扩增异常解析器,可见上文中的例子
        extendHandlerExceptionResolvers(exceptionResolvers);
        HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
        composite.setOrder(0);
        composite.setExceptionResolvers(exceptionResolvers);
        return composite;
    }

2.直接看下spring内置的默认异常解析器吧,参考 addDefaultHandlerExceptionResolvers()
方法

protected final void addDefaultHandlerExceptionResolvers(List exceptionResolvers) {
        // 1.异常的方法处理,跟@RequestMapping注解的方法调用类似
        ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
        exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager());
        exceptionHandlerResolver.setMessageConverters(getMessageConverters());
        exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
        exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
        if (jackson2Present) {
            exceptionHandlerResolver.setResponseBodyAdvice(
                    Collections.singletonList(new JsonViewResponseBodyAdvice()));
        }
        if (this.applicationContext != null) {
            exceptionHandlerResolver.setApplicationContext(this.applicationContext);
        }
        exceptionHandlerResolver.afterPropertiesSet();
        exceptionResolvers.add(exceptionHandlerResolver);
        // 2.携带@ResponseStatus注解的解析器
        ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
        responseStatusResolver.setMessageSource(this.applicationContext);
        exceptionResolvers.add(responseStatusResolver);
        // 3.默认的异常解析器,针对spring的内置异常作下简单的response
        exceptionResolvers.add(new DefaultHandlerExceptionResolver());
    }

笔者主要关注 ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
解析器,那就分块来简单的讲解把

ExceptionHandlerExceptionResolver

初始化状态的代码就不罗列了,读者直接阅读源码就知道,笔者此处作下初始化的总结

  1. 寻找所有的携带 @ControllerAdvice
    注解的bean,包装成 ExceptionHandlerMethodResolver
    方法解析器,由此来从中挑选出携带 @ExceptionHandler
    注解的方法集合

  2. 对第一条中所得的方法集合,读取其中 @ExceptionHandler
    注解的值(Throwable实现类);无则读取对应方法实现了Throwable异常接口的参数集合。即得出 exceptionTypes
    集合

  3. 对上述的 exceptionTypes
    集合依次与对应的method形成映射,即方便针对指定的异常可以调用相应的方法来返回结果

  4. 对上述满足条件的 ControllerAdvice
    ,结合 ExceptionHandlerMethodResolver
    装入 exceptionHandlerAdviceCache
    属性map中

  5. 封装参数解析器集合与返回值解析器集合,和处理 @RequestMapping
    的操作一样

具体的解析过程,笔者此处点一下,方便与上文对照着看,直接看关键的 getExceptionHandlerMethod()
方法

protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
            @Nullable 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);
            }
            // For advice applicability check below (involving base packages, assignable types
            // and annotation presence), use target class instead of interface-based proxy.
            if (Proxy.isProxyClass(handlerType)) {
                handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
            }
        }
        // 进入@ControlleAdvice的语法环境了,判断抛异常的所在类,ControllerAdvice是否支持
        for (Map.Entry 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;
    }

最终就是根据Exception的类型找寻符合条件的method,然后按照 @RequestMapping
注解的处理方式得到相应的视图对象供视图解析器去渲染

ResponseStatusExceptionResolver

针对携带 @ResponseStatus
注解的异常类来返回响应体的,简单的看下代码吧

@Override
    @Nullable
    protected ModelAndView doResolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

        try {
            // 直接返回的是ResponseStatusException类型的异常则直接处理
            if (ex instanceof ResponseStatusException) {
                return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
            }
            // 读取异常类上携带的@ResponseStatus注解,有则返回结果
            ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
            if (status != null) {
                return resolveResponseStatus(status, request, response, handler, ex);
            }
            // 递归调用下
            if (ex.getCause() instanceof Exception) {
                ex = (Exception) ex.getCause();
                return doResolveException(request, response, handler, ex);
            }
        }
        catch (Exception resolveEx) {
            logger.warn("ResponseStatus handling resulted in exception", resolveEx);
        }
        // 无符合条件的,直接返回null,调用下一个异常解析器
        return null;
    }

最终调用的也就是 HttpServletResponse#sendError(int statusCode,String reason)
方法直接返回结果

DispatcherServlet异常处理逻辑

此处还是贴下重要的代码片段,加深印象,直接查阅 processHandlerException()
方法

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
            @Nullable Object handler, Exception ex) throws Exception {

....
if (this.handlerExceptionResolvers != null) {
            // 对异常解析器集合进行遍历
            for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
                exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
                // ModelAndView对象不为null则直接跳出,否则采取下一个异常解析器
                if (exMv != null) {
                    break;
                }
            }
        }

}
....

温馨提示:

  1. 根据上述代码的逻辑可见,用户在自定义相应的异常解析器时,需要注意如果满足解析指定的异常,则最后返回不为null的视图对象( return new ModelAndView()
    ),以免其跑至下一个异常解析器,影响服务执行结果。
  2. 遍历的异常解析器顺序此处提一下,其采取的是简单的 ArrayList
    集合来保持顺序,所以用户如果想自己的异常解析器保持较高的优先级,则可以采取List接口的 add(int index, T value)
    方法添加或者直接实现 HandlerExceptionResolver
    并设置 order
    属性来保持即可

结语

了解异常解析器的加载机制以及运行逻辑,方便我们写出合乎spring逻辑的代码,以此保证代码的整洁性。

微信扫一扫,分享到朋友圈

springboot情操陶冶-web配置(五)

微点评 | 快递普遍涨价,菜鸟深化骨干网,专家赵小敏有话说!

上一篇

golang 中的闭包和defer

下一篇

你也可能喜欢

springboot情操陶冶-web配置(五)

长按储存图像,分享给朋友