转载

Spring 异常处理机制小结

前几天想把公司某个项目中的参数处理通过某一种统一的机制进行封装后并将返回结果统一处理,参照资料后发现可以通过

JSR之前定义的一些参数校验标准来对一些进行简单的设置

javax.validation.constraints包下面有如下注解:

注解 数据类型 说明
@Null 任意类型 必须为空
@NotNull 任意类型 必须不为空
@AssertFalse boolean,Boolean 注解布尔值为false
@AssertTrue 同@AssertFalse 注解布尔值为true
@Max(value=值) BigDecimal,BigInteger, byte,short, int, long,等任何Number 设置最大值
@Min(value=值) 同@Min 设置最小值
@DecimalMax(value=值) 同@Min 设置最大值
@Digits(integer=整数位数,fraction=小数位数) 同@Min 设置整数位和小数位的精度
@Future java.util.Date,java.util.Calendar;Joda Time类库的日期类型 比当前时间晚
@Past 同@Future 比当前时间早
@Size(min=最小值,max=最大值) 字符串、Collection、Map、数组等 长度
@Pattern(regexp=正则表达式) String 匹配正则表达式

除此之外,还有Hibernate Validation提供的注解:

注解 数据类型 说明
@Range(min=最小值, max=最大值) 同@Min 验证注解的元素值在最小值和最大值之间
@URL String 校验URL格式
@Email String 校验Email格式
@NotBlank String 不为null,不为””
@NotEmpty 任意类型 不为null,字符串长度不为0,集合大小不为0

​ 通过在一个 pojo 对象的 field 中加上这些注解并在 Controller 层对应的 pojo 对象前加上@Valid 注解就可以使用,并将校验不通过的异常信息包装在一个 BindingResult对象,整个处理十分方便,具体用法网上有很多,就不赘述了。

​ 上述对于这种校验需要包装一个完整的 pojo 对象,有时候在入参不多的情况下有点多余,一般就是对指定的参数校验,我就想在参数校验不通过的情况下对异常信息统一处理,于是马上想到可以通过 ExceptionHandler + ControllerAdvice注解的方式对进行统一处理,对于ConstraintViolationException这个异常进行捕获就可以了,可惜理想丰满现实骨感。在 web 容器加载后请求对应的mapping始终没有在@ExceptionHandler修饰的方法中捕获异常,这个配置方法和Spring提供的教程相差无几,为什么会没有处理…

Spring 统一的异常处理接口HandlerExceptionResolver

​ 在 Spring 的整个异常处理体系中,HandlerExceptionResolver接口是一个基础入口,其中的resolveException方法专门用于处理请求过程中发生的Exception,参数如下:

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

​ 在对源码进行调试之后,发现请求流程中如下:

  1. 对于所有的 http 请求,通过DispatcherServlet这个类进行分发,这个类是一个核心请求类,处理包括但不限于

    • 将请求参数请求解析并将解析后的参数放入 RequestMapping 修饰的方法的入参中
    • 调用定义好的拦截器对方法进行增强
    • 根据HttpServletRequest对象类型 将请求正确分发到对应 HandlerAdapter中
    • 将处理好的请求结果封装到一个 ModelAndView 对象中返回,如果是直接返回到一个结果页面还要进行视图的渲染
    • 对请求过程中异常的处理
  2. 在请求处理完成后,有如下代码(省略一些次要代码,下同)

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response)throws Exception {
    		HttpServletRequest processedRequest = request;
    		HandlerExecutionChain mappedHandler = null;
    		boolean multipartRequestParsed = false;
    
    		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
    		try {
    			ModelAndView mv = null;
    			Exception dispatchException = null;
    
    			try {
    
    				// Determine handler for the current request.
    				mappedHandler = getHandler(processedRequest);
    				if (mappedHandler == null || mappedHandler.getHandler() == null) {
    					noHandlerFound(processedRequest, response);
    					return;
    				}
    
    				// Determine handler adapter for the current request.
    				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
    				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    					return;
    				}
    				// Actually invoke the handler.
    				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    				mappedHandler.applyPostHandle(processedRequest, response, mv);
    			}
    			catch (Exception ex) {
    				dispatchException = ex;
    			}
    			catch (Throwable err) {
    				// As of 4.3, we're processing Errors thrown from handler methods as well,
    				// making them available for @ExceptionHandler methods and other scenarios.
    				dispatchException = new NestedServletException("Handler dispatch failed", err);
    			}
    			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    		}
    		catch (Exception ex) {
    			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    		}
    		catch (Throwable err) {
    			triggerAfterCompletion(processedRequest, response, mappedHandler,
    					new NestedServletException("Handler processing failed", err));
    		}
    	}
    // 在请求完成后会有对异常的检查和处理,由processDispatchResult方法的processHandlerException处理
    protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {
    
    		ModelAndView exMv = null;
      		// 遍历之前应用启动时所有在IOC容器中注册的异常处理器(都基于HandlerExceptionResolver接口)
    		for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
    			exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
    			if (exMv != null) {
    				break;
    			}
    		}
    		throw ex;
    	}
    

    看到这里,基本可以发现大致的问题,我的这个项目在启动时只有项目的异常配置ExceptionResolver,于是我又新建一个可以使用@ExceptionHandler注解的项目,观察它的ExceptionResolver类型,对比后发现:

    失效配置

    Spring 异常处理机制小结

    正常配置

    Spring 异常处理机制小结

继续跟踪一下发现在 ExceptionHandlerExceptionResolver 这个异常处理器

// ExceptionHandlerExceptionResolver内部用两个Map来存储所有ExceptionHandlerMethodResolver类,
// 他并不直接实现HandlerExceptionResolver接口,而是靠外部其它类来包装
// 在ExceptionHandlerMethodResolver中会去发现所有由@ExceptionHandler修饰的方法
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
			new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>(64); // 处理具体 Controller中的ExceptionHandler

private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
			new LinkedHashMap<ControllerAdviceBean, ExceptionHandlerMethodResolver>(); //处理ControllerAdvice中的ExceptionHandler

Spring 中异常处理方式

很显然,Spring 对于异常的处理有以下几种方式:

  1. 某一具体Controller方法中抛出的异常,由Controller中有@ExceptionHandler修饰的方法处理(如果异常类型可以匹配上的话)
  2. 通过ControllerAdvice + ExceptionHandler注解相结合的方法全局处理各个Controller中的异常
  3. 自定义异常解析器并实现HandlerExceptionResolver接口

而在我这个ExceptionHandler注解失效项目中,显然采用第三种方法处理异常,并且我采用了@EnableWebMvc注解作为全局的 web 配置,默认这个配置会读取WebMvcConfigurationSupport这个类中的配置;但不幸的是这个配置会被继承WebMvcConfigurerAdapter或实现WebMvcConfigurer接口的配置类所覆盖,我们项目中正好继承了WebMvcConfigurerAdapter类并且只添加了一种自定义的异常解析器作为全局处理

> // Java Doc 中对WebMvcConfigurationSupport异常解析器的部分描述
> Registers a {@link HandlerExceptionResolverComposite} with this chain of
> exception resolvers:
>
> {@link ExceptionHandlerExceptionResolver} for handling exceptions
> through @{@link ExceptionHandler} methods.
> {@link ResponseStatusExceptionResolver} for exceptions annotated
> with @{@link ResponseStatus}.
> {@link DefaultHandlerExceptionResolver} for resolving known Spring
> exception types
>

看到这里,有人会问,当初继承WebMvcConfigurerAdapter这个类自定义配置是为了满足业务需求,尤其是异常这块希望有一个全局处理机制,但后来发现需要对某一特定异常进行处理的时候却发现ExceptionHandler注解失效,难道不能两种兼得吗?答案当然是可以,仔细查阅 Spring WebMvcConfigurer源码中的 Java Doc 发现,自定义的异常解析器有两种添加方法:

// 以覆盖WebMvcConfigurationSupport的默认解析器方法重新定义异常解析器
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers){
}
// 以继承WebMvcConfigurationSupport的默认解析器方法扩展异常解析器
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers){
}

显然,第二种就能满足我们的需求了,效果如下:

Spring 异常处理机制小结

原文  http://www.zsfblues.com/2018/02/04/Spring-异常处理机制小结/
正文到此结束
Loading...