Springboot Controller优雅参数校验源码分析

前言

  • 【校验参数在项目中是很常见的,在java中,几乎每个有入参的方法,在执行下一步操作之前,都要验证参数的合法性,比如是入参否为空,数据格式是否正确等等,往常的写法就是一大推的if-else,既不美观也不优雅,这个时候JCP组织站出来了,并且制定了一个标准来规范校验的操作,这个标准就是Java Validation API(JSR 303)。】 —摘自博客园

  • 【Bean Validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08)】— 摘自知乎

使用步骤

pom中添加依赖

以继承 spring-boot-starter-parent 为例

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

另需加入依赖:

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
或者
		<dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>

因为hibernate-validator中也依赖了validation-api ( 高版本validation-api中包含了完善的核心注解,提供了与hibernate-validator相同功能的注解)
在这里插入图片描述

在低版本的spring-boot里可能需要两个都要引入,主要是低版本中的各有各的特色注解(比如在validation-api中在2.0版本才加入NotBlank这个注解),但是高版本中validation-api中的注解已经很完善;

hibernate-validator中的一些注解已经不建议使用了;
在这里插入图片描述

接收请求的参数添加注解

加入依赖后,我们就可以在我们的实体上加上对应的校验注解,例如:

package com.xx.log.common.pojo.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;


@Data
public class TestDTO {
    
    

    @NotBlank(message = "info 不能为空")
    @NotNull(message = "info 不能为null")
    private String info;


    @NotBlank(message = "name 不能为空")
    @NotNull(message = "name 不能为null")
    private String name;
}

开启校验

开启校验是在我们controller 方法的参数前用 @Validated 或者 @Valid 注解

    @GetMapping("/test")
    public TestVO test(@Validated TestDTO testDTO) {
    
    
        log.info("testDTO:{}", testDTO);
        return TestVO.builder().code(0).build();
    }
    
或者

    @PostMapping("/test")
    public TestVO test(@RequestBody @Validated TestDTO testDTO) {
    
    
        log.info("testDTO:{}", testDTO);
        return TestVO.builder().code(0).build();
    }

源码分析

源码入口

参数校验是在调用controller方法前参数准备阶段里面,具体代码入口如下

org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest
↓
org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues
↓
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument
↓
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument
↓
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable

我们看看 validateIfApplicable 这个方法

	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    
    
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation ann : annotations) {
    
    
//@A
			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
//@B			
			if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
    
    
				Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
				Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {
    
    hints});
				binder.validate(validationHints);
				break;
			}
		}
	}

从这个地方可以看出 @Validated和 @Valid 、以@Valid 开头的自定义注解都可以开启校验

binder代码还很深,包括它的创建和使用
binder创建是由 ServletRequestDataBinderFactory 类创建的ExtendedServletRequestDataBinder实例

public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {
    
    
	/**
	 * Create a new instance.
	 * @param binderMethods one or more {@code @InitBinder} methods
	 * @param initializer provides global data binder initialization
	 */
	public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
			@Nullable WebBindingInitializer initializer) {
    
    
		super(binderMethods, initializer);
	}

	/**
	 * Returns an instance of {@link ExtendedServletRequestDataBinder}.
	 */
	@Override
	protected ServletRequestDataBinder createBinderInstance(
			@Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {
    
    
		return new ExtendedServletRequestDataBinder(target, objectName);
	}
}

ExtendedServletRequestDataBinder的继承关系
在这里插入图片描述
接上面源码的调用栈的validateIfApplicable方法继续往下走

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
↓
org.springframework.validation.DataBinder#validate(java.lang.Object...)org.springframework.boot.autoconfigure.validation.ValidatorAdapter#validate(java.lang.Object, org.springframework.validation.Errors)org.springframework.validation.beanvalidation.SpringValidatorAdapter#validate(java.lang.Object, org.springframework.validation.Errors)

最后一个方法是

@Override
public void validate(Object target, Errors errors) {
    
    
	if (this.targetValidator != null) {
    
    
		processConstraintViolations(this.targetValidator.validate(target), errors);
	}
}
  • this.targetValidator 这个属性实际的对象是hibernate提供的校验类 org.hibernate.validator.internal.engine.ValidatorImpl
  • processConstraintViolations 作用是将hibernate校验返回的结果统计成spring能够识别的结果,也就是将校验结果再封装一遍

注意点

  • @NotBlank 不能用来校验数值类型,可以用来校验字符串
  • 数值类型可以用以下注解
    • @NotNull
    • @Max
    • @Min
    • @DecimalMin
    • @DecimalMax

转换校验失败异常提示

如果参数校验不通过,框架会抛出 MethodArgumentNotValidException,在调用方看来不是很友好,我们可以定义通用的该异常的处理类进行统一处理返回:

package com.xx.log.config;

import com.xx.log.common.pojo.vo.TestVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class ParameterCalibration {
    
    

    @ExceptionHandler({
    
    MethodArgumentNotValidException.class, BindException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public TestVO handleMethodArgumentNotValidException(Exception exception) {
    
    
        StringBuilder errorInfo = new StringBuilder();
        BindingResult bindingResult = null;
        if (exception instanceof MethodArgumentNotValidException) {
    
    
            bindingResult = ((MethodArgumentNotValidException) exception).getBindingResult();
        }
        if (exception instanceof BindException) {
    
    
            bindingResult = ((BindException) exception).getBindingResult();
        }
        for (int i = 0; i < bindingResult.getFieldErrors().size(); i++) {
    
    
            if (i > 0) {
    
    
                errorInfo.append(",");
            }
            FieldError fieldError = bindingResult.getFieldErrors().get(i);
            errorInfo.append(fieldError.getField()).append(" :").append(fieldError.getDefaultMessage());
        }
        log.error(errorInfo.toString());
        //这里返回自己的Result的结果类。
        return TestVO.builder().msg(errorInfo.toString()).code(-1).build();
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public TestVO handleDefaultException(Exception exception) {
    
    
        log.error(exception.toString());
        //这里返回自己的Result的结果类。
        return TestVO.builder().msg("服务器错误").code(-1).build();
    }
}

异常统一处理原理可参考我 另一篇文章

自定义校验注解

自定义注解 【cnblogs 文章推荐】

over~~~

猜你喜欢

转载自blog.csdn.net/Aqu415/article/details/119705027