本文讲讲 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<String>> 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<ObjectError> allErrors = bindException.getAllErrors();
- List<String> 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<HandlerExceptionResolver> resolvers) {
- // response first
- resolvers.add(0, new SimpleExceptionResolver());
- }
- }
上述简单的代码便会对系统抛出的 BindException 异常进行针对性的处理, 从而返回合乎格式的响应体. 当然这只是一小部分, 笔者可以稍微从源码的角度来分析下 spring 的异常机制
源码层
查阅过 DispatcherServlet 源码的都知道, 当出现异常的时候, 则会尝试调用 HandlerExceptionResolver 解析器去根据异常进行视图渲染或者直接返回对应的错误信息. 笔者按步骤来进行简单分析, 从 WebMvcConfigurationSupport 入手
1. 异常解析器注册
- @Bean
- public HandlerExceptionResolver handlerExceptionResolver() {
- List<HandlerExceptionResolver> 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<HandlerExceptionResolver> 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
初始化状态的代码就不罗列了, 读者直接阅读源码就知道, 笔者此处作下初始化的总结
寻找所有的携带 @ControllerAdvice 注解的 bean, 包装成 ExceptionHandlerMethodResolver 方法解析器, 由此来从中挑选出携带 @ExceptionHandler 注解的方法集合
对第一条中所得的方法集合, 读取其中 @ExceptionHandler 注解的值 (Throwable 实现类); 无则读取对应方法实现了 Throwable 异常接口的参数集合. 即得出 exceptionTypes 集合
对上述的 exceptionTypes 集合依次与对应的 method 形成映射, 即方便针对指定的异常可以调用相应的方法来返回结果
对上述满足条件的 ControllerAdvice , 结合 ExceptionHandlerMethodResolver 装入 exceptionHandlerAdviceCache 属性 map 中
封装参数解析器集合与返回值解析器集合, 和处理 @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<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;
- }
最终就是根据 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;
- }
- }
- }
- }
- ....
温馨提示:
根据上述代码的逻辑可见, 用户在自定义相应的异常解析器时, 需要注意如果满足解析指定的异常, 则最后返回不为 null 的视图对象 (return new ModelAndView()), 以免其跑至下一个异常解析器, 影响服务执行结果.
遍历的异常解析器顺序此处提一下, 其采取的是简单的 ArrayList 集合来保持顺序, 所以用户如果想自己的异常解析器保持较高的优先级, 则可以采取 List 接口的 add(int index, T value) 方法添加或者直接实现 HandlerExceptionResolver 并设置 order 属性来保持即可
结语
了解异常解析器的加载机制以及运行逻辑, 方便我们写出合乎 spring 逻辑的代码, 以此保证代码的整洁性.
来源: https://www.cnblogs.com/question-sky/p/9729958.html