Springboot使用@Valid注解,对其参数错误异常的统一处理

       在我们使用springboot作为微服务框架进行敏捷开发的时候,为了保证传递数据的安全性,需要对传递的数据进行校验,但是在以往的开发中,开发人员花费大量的时间在繁琐的if else 等判断语句来对参数进行校验,这种方式不但降低了我们的开发速度,而且写出来的代码中带有很多冗余代码,使得编写的代码不够优雅,为了将参数的验证逻辑和代码的业务逻辑进行解耦,Java给我们提供了@Valid注解,用来帮助我们进行参数的校验,实现了将业务逻辑和参数校验逻辑在一定程度上的解耦,增加了代码的简洁性和可读性。springboot中自带了spring validation参数校验框架,其使用上和@valid差不多,在这里就不累述了,本文主要讲解@valid的使用对其参数校验失败后的错误一样的统一处理。

       首先,简介对微服务开发中异常的统一处理,spring中的@RestControllerAdvice注解可以获取带有@controller注解类的异常,通过@ExceptionHandler(MyException.class)注解来共同完成对异常进行处理。示例如下:

/**
 * 通用异常拦截处理类(通过切面的方式默认拦截所有的controller异常)
 */
@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {

    /**
     * 对运行时异常进行统一异常管理方法
     * @param e
     * @return
     */
    @ExceptionHandler(FlyException.class) // FlyException类继承于RuntimeException
    public ResponseEntity> handlerException(FlyException e) {
        Map result = new HashMap<>(1);
        result.put("message", e.getMessage());
        return ResponseEntity.status(e.getCode()).body(result);
    }

通过注解@RestControllerAdvice和注解@ExceptionHandler的联合使用来实现对异常的统一处理,然后在前端以友好的方式显示。

使用@Valid注解的示例如下:

  @PostMapping
    public ResponseEntity save(@Valid BrandCreateRequestDto dto, BindingResult bindingResult) {
        // 判断是否含有校验不匹配的参数错误
        if (bindingResult.hasErrors()) {
            // 获取所有字段参数不匹配的参数集合
            List fieldErrorList = bindingResult.getFieldErrors();
            Map result = new HashMap<>(fieldErrorList.size());
            fieldErrorList.forEach(error -> {
                // 将错误参数名称和参数错误原因存于map集合中
                result.put(error.getField(), error.getDefaultMessage());
            });
            return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(result);
        }

        brandService.save(dto);
        return ResponseEntity.status(HttpStatus.CREATED.value()).build();
    }

@Valid注解确实将我们原来的参数校验的问题进行了简化,但是,如果我们有多个handler需要处理,那我们岂不是每次都要写这样的冗余代码。通过查看@valid的实现机制(这里就不描述了),当参数校验失败的时候,会抛出MethodArgumentNotValidException异常(当用{@code @Valid}注释的参数在验证失败时,将引发该异常):

/**
 * Exception to be thrown when validation on an argument annotated with {@code @Valid} fails.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 */
@SuppressWarnings("serial")
public class MethodArgumentNotValidException extends Exception {

	private final MethodParameter parameter;

	private final BindingResult bindingResult;


	/**
	 * Constructor for {@link MethodArgumentNotValidException}.
	 * @param parameter the parameter that failed validation
	 * @param bindingResult the results of the validation
	 */
	public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
		this.parameter = parameter;
		this.bindingResult = bindingResult;
	}

按照我们的预想,我们只需要在原来定义的统一异常处理类中,捕获MethodArgumentNotValidException异常,然后对其错误信息进行分析和处理即可实现通用,代码如下:

/**
 * 对方法参数校验异常处理方法
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity> handlerNotValidException(MethodArgumentNotValidException exception) {
    log.debug("begin resolve argument exception");
    BindingResult result = exception.getBindingResult();
    Map maps;

    if (result.hasErrors()) {
        List fieldErrors = result.getFieldErrors();
        maps = new HashMap<>(fieldErrors.size());
        fieldErrors.forEach(error -> {
            maps.put(error.getField(), error.getDefaultMessage());
        });
    } else {
        maps = Collections.EMPTY_MAP;
    }

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps);
}

但是经过测试,会发现对该异常进行捕获然后处理是没有效果的,这可能是我,也是大家遇到的问题之一,经过对@Valid的执行过程的源码进行分析,数据传递到spring中的执行过程大致为:前端通过http协议将数据传递到spring,spring通过HttpMessageConverter类将流数据转换成Map类型,然后通过ModelAttributeMethodProcessor类对参数进行绑定到方法对象中,并对带有@Valid或@Validated注解的参数进行参数校验,对参数进行处理和校验的方法为ModelAttributeMethodProcessor.resolveArgument(...),部分源代码如下所示:

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

...
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        ...
        Object attribute = null;
		BindingResult bindingResult = null;

		if (mavContainer.containsAttribute(name)) {
			attribute = mavContainer.getModel().get(name);
		}
		else {
			// Create attribute instance
			try {
				attribute = createAttribute(name, parameter, binderFactory, webRequest);
			}
			catch (BindException ex) {
				if (isBindExceptionRequired(parameter)) {
					// No BindingResult parameter -> fail with BindException
					throw ex;
				}
				// Otherwise, expose null/empty value and associated BindingResult
				if (parameter.getParameterType() == Optional.class) {
					attribute = Optional.empty();
				}
				bindingResult = ex.getBindingResult();
			}
		}

        //进行参数绑定和校验
		if (bindingResult == null) {
			// 对属性对象的绑定和数据校验;
			// 使用构造器绑定属性失败时跳过.
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
           
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					bindRequestParameters(binder, webRequest);
				}
                // 对绑定参数进行校验,校验失败,将其结果信息赋予bindingResult对象
				validateIfApplicable(binder, parameter);
                // 如果获取参数绑定的结果中包含错误的信息则抛出异常
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
			// Value type adaptation, also covering java.util.Optional
			if (!parameter.getParameterType().isInstance(attribute)) {
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
			}
			bindingResult = binder.getBindingResult();
		}

		// Add resolved attribute and BindingResult at the end of the model
		Map bindingResultModel = bindingResult.getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

		return attribute;
}

通过查看源码,当BindingResult中存在错误信息时,会抛出BindException异常,查看BindException源代码如下:

/**
 * Thrown when binding errors are considered fatal. Implements the
 * {@link BindingResult} interface (and its super-interface {@link Errors})
 * to allow for the direct analysis of binding errors.
 *
 * 

As of Spring 2.0, this is a special-purpose class. Normally, * application code will work with the {@link BindingResult} interface, * or with a {@link DataBinder} that in turn exposes a BindingResult via * {@link org.springframework.validation.DataBinder#getBindingResult()}. * * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop * @see BindingResult * @see DataBinder#getBindingResult() * @see DataBinder#close() */ @SuppressWarnings("serial") public class BindException extends Exception implements BindingResult { private final BindingResult bindingResult; /** * Create a new BindException instance for a BindingResult. * @param bindingResult the BindingResult instance to wrap */ public BindException(BindingResult bindingResult) { Assert.notNull(bindingResult, "BindingResult must not be null"); this.bindingResult = bindingResult; }

 

我们发现BindException实现了BindingResult接口(BindResult是绑定结果的通用接口, BindResult继承于Errors接口),所以该异常类拥有BindingResult所有的相关信息,因此我们可以通过捕获该异常类,对其错误结果进行分析和处理。代码如下:

/**
 * 对方法参数校验异常处理方法
 */
@ExceptionHandler(BindException.class)
public ResponseEntity> handlerNotValidException(BindException exception) {
    log.debug("begin resolve argument exception");
    BindingResult result = exception.getBindingResult();
    Map maps;

    if (result.hasErrors()) {
        List fieldErrors = result.getFieldErrors();
        maps = new HashMap<>(fieldErrors.size());
        fieldErrors.forEach(error -> {
            maps.put(error.getField(), error.getDefaultMessage());
        });
    } else {
        maps = Collections.EMPTY_MAP;
    }

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps);
}

这样,我们对是content-type类型为form(表单)类型的请求的参数校验的异常处理就解决了,对于MethodArgumentNotValidException异常不起作用的原因主要是因为跟请求发起的数据格式(content-type)有关系,对于不同的传输数据的格式spring采用不同的HttpMessageConverter(http参数转换器)来进行处理.以下是对HttpMessageConverter进行简介:

HTTP 请求和响应的传输是字节流,意味着浏览器和服务器通过字节流进行通信。但是,使用 Spring,controller 类中的方法返回纯 String 类型或其他 Java 内建对象。如何将对象转换成字节流进行传输?

在报文到达SpringMVC和从SpringMVC出去,都存在一个字节流到java对象的转换问题。在SpringMVC中,它是由HttpMessageConverter来处理的。

当请求报文来到java中,它会被封装成为一个ServletInputStream的输入流,供我们读取报文。响应报文则是通过一个ServletOutputStream的输出流,来输出响应报文。http请求与相应的处理过程如下:

针对不同的数据格式,springmvc会采用不同的消息转换器进行处理,以下是springmvc的内置消息转换器:

由此我们可以看出,当使用json作为传输格式时,springmvc会采用MappingJacksonHttpMessageConverter消息转换器, 而且底层在对参数进行校验错误时,抛出的是MethodArgumentNotValidException异常,因此我们需要对BindException和MethodArgumentNotValidException进行统一异常管理,最终代码演示如下所示:

 /**
     * 对方法参数校验异常处理方法(仅对于表单提交有效,对于以json格式提交将会失效)
     * 如果是表单类型的提交,则spring会采用表单数据的处理类进行处理(进行参数校验错误时会抛出BindException异常)
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity> handlerBindException(BindException exception) {
        return handlerNotValidException(exception);
    }

    /**
     * 对方法参数校验异常处理方法(前端提交的方式为json格式出现异常时会被该异常类处理)
     * json格式提交时,spring会采用json数据的数据转换器进行处理(进行参数校验时错误是抛出MethodArgumentNotValidException异常)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity> handlerArgumentNotValidException(MethodArgumentNotValidException exception) {
        return handlerNotValidException(exception);
    }

    public ResponseEntity> handlerNotValidException(Exception e) {
        log.debug("begin resolve argument exception");
        BindingResult result;
        if (e instanceof BindException) {
            BindException exception = (BindException) e;
            result = exception.getBindingResult();
        } else {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            result = exception.getBindingResult();
        }

        Map maps;
        if (result.hasErrors()) {
            List fieldErrors = result.getFieldErrors();
            maps = new HashMap<>(fieldErrors.size());
            fieldErrors.forEach(error -> {
                maps.put(error.getField(), error.getDefaultMessage());
            });
        } else {
            maps = Collections.EMPTY_MAP;
        }

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps);

    }

这样就完美解决了我们对参数校验异常的统一处理。在这里我仅仅是针对参数校验的异常进行了统一处理,也就是返回给前端的响应码是400(参数格式错误),对于自定义异常或者其他的异常都可以采用这种方式来对异常进行统一处理,如果大家遇到什么问题,可以在下方留言,大家一起解决,共同进步。

你可能感兴趣的:(springboot笔记)