写在前面

在日常的开发工作中,为了程序的健壮性,大部分方法都需要进行入参数据校验。最直接的当然是在相应方法内对数据进行手动校验,但是这样代码里就会有很多冗余繁琐的if-else。

比如如下的保存用户信息的方法:

上面的方法因为请求对象里的参数很多,所以就有了好多if/else。当然这么写没问题,但是:

  1. 扩展性差:如果后续 UserRequest 里又新增了参数,那还得在方法实现里增加校验代码,参数校验和业务代码混在一起。

  2. 可读性差:当参数校验过多时,代码会十分冗长,违背阿里巴巴代码规约。

  3. 不易复用:其他比如更新用户信息的方法,可能也会有类似的参数校验。如果也手动校验的话,会存在很多重复代码。

那么该怎么优雅的进行参数校验呢?

spring validation

Java在早在2009年就提出了 Bean Validation(JSR)规范,其中定义了一系列的校验注解,比如 @NotEmpty、@NotNull等,支持通过注解的方式对字段进行校验,避免在业务逻辑中耦合冗余的校验逻辑。

不过,以上注解本身不做校验,只是能给开发者做个提醒。如果需要达到参数校验的目的,还需要其他配置。

Controller方法参数校验

相关Demo 可以 点击这里

效果示例

Spring 提供了相应的 Bean Validation 实现:Java Bean Validation,并在 Spring MVC 中添加了自动校验,默认就会对 @Valid/@Validated 修饰的方法参数使用 Validator 来做校验逻辑。

举个栗子:

引入依赖:

validation-api是一套标准(JSR-303),叫做Bean Validation,Hibernate Validator是Bean Validation的参考实现,提供了JSR-303 规范中所有内置constraint的实现,除此之外Hibernate Validator还附加了一些constraint。

  • 在方法在入参对应元素上配置校验注解:

  • 在 Controller 相应方法中,使用 @Valid/@Validated 注解开启数据校验功能:

如果数据校验通过,就会继续执行方法里的业务逻辑;否则,就会抛出一个 MethodArgumentNotValidException 异常。默认情况下,Spring 会将该异常及其信息以错误码 400 进行下发,返回结果示例如下:

但是返回的异常结果不是需要的格式,所以再来个全局异常捕获器拦截该异常,就可以得到一个完美的异常结果:

设置了如上捕获器后,如果数据校验不通过,返回的结果为:

借助Spring和约束注解,就非常简单明了、优雅地完成了方法参数校验。

而且,假如以后入参对象里新增了参数,只需要顺便添加一个注解,而不用去改业务代码了!

@Valid 和 @Validated

  • @Valid注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。

  • @Validated注解,是 Spring Validation 所定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。

两者的区别在于 @Validated 有 value 属性,支持分组校验,即根据不同的分组采用不同的校验机制,@Valid 可以添加在成员变量上,支持嵌套校验。所以建议的使用方式就是:启动校验(即 Controller 层)时使用 @Validated 注解,嵌套校验时使用 @Valid 注解,这样就能同时使用分组校验和嵌套校验功能。

注意:单参数校验需要在类上添加 @Validated 注解

分组校验

但是,对于同个参数,不同的场景可能需要不同的校验,这时候就可以用分组校验能力。

比如创建 User 时,userId为空;但是更新 User 时,userId值则不能为空。示例如下:

自定义校验注解

还有,如果现有的基础校验注解没法满足校验需求,那就可以使用自定义注解。由两部分组成:

  • 由 @Constraint 注解的注解。

  • 实现了 javax.validation.ConstraintValidator 的 validator。

两者通过 @Constraint 关联到一起。

假设有个性别枚举,需要校验用户的性别是否属于此范围内,示例如下:

第一步,自定义约束注解 InEnum,可以参考 NotNull 的定义:

第二步,自定义约束校验器 InEnumValidator。如果校验通过,返回 true;反之返回 false:

第三步,参数上增加 @InEnum 注解校验:

设置了如上校验后,如果数据校验不通过,返回的结果为:

校验原理

  1. MethodValidationPostProcessor是 Spring 提供的来实现基于方法的JSR校验的核心处理器,能让约束作用在方法入参、返回值上。关于校验方面的逻辑在切面MethodValidationInterceptor。

  2. MethodValidationInterceptor:用于处理方法的数据校验

  3. LocalValidatorFactoryBean:最终是使用它来执行验证功能的,它也是Spring MVC默认的验证器。默认情况下, LocalValidatorFactoryBean 会配置一个 SpringConstraintValidatorFactory 实例。如果有指定的 ConstraintValidatorFactory,就会使用指定的,因此在遇到自定义约束注解的时候,就会自动实例化 @Constraint指定的关联 Validator,从而完成数据校验过程。

  1. SpringValidatorAdapter:是javax.validation.Validator到Spring的Validator的适配,通过它就可以对接到Bean Validation来完成校验了。

Service方法参数校验

效果示例

更多情况下是需要对 Service 层的接口进行参数校验的,那么该怎么配置呢?

在校验方法入参的约束时,若是 @Override 父类/接口的方法,那么这个入参约束只能写在父类/接口上面。(至于为什么只能写在接口处,其实是和 Bean Validation 的实现有关,可参考此类:OverridingMethodMustNotAlterParameterConstraints)

  1. 如果入参是平铺的参数

首先需要在父类/接口的方法入参里增加注解约束,然后用 @Validated 修饰实现类。

如果数据校验通过,就会继续执行方法里的业务逻辑;否则,就会抛出一个 ConstraintViolationException 异常。

  1. 如果入参是对象:在实际开发中,其实大多数情况下方法入参是个对象,而不是单单平铺的参数。

首先需要在方法入参类里增加 @NotNull 等注解约束,然后在父类/接口的方法入参里增加 @Valid(便于嵌套校验),最后用 @Validated 修饰实现类。

如果需要格式化错误结果,可以再来个异常处理切面,就可以得到一个完美的异常结果。

较简洁的方式:FastValidatorUtils

原理见《FastValidator 原理与最佳实践》

具体示例如下:

自定义注解@RequestValid和对应切面RequestValidAspect。注解在具体的方法上,对于被注解的方法,在 AOP 中会扫描所有入参,对参数进行校验。

Validation的基本校验注解

注解描述@Null验证对象是否为null@NotNull验证对象是否不为null,无法检查长度为0的字符串@NotBlank检查约束字符串是否为null,且被Trim后的长度是否大于0,只适用于字符串,会去掉前后空格@NotEmpty检查约束元素是否为NULL或者是EMPTY@AssertTrue验证Boolean对象是否为true@AssertFalse验证Boolean对象是否为false@Size(min=, max=)验证对象(Array、Collection、Map、String)长度是否在给定的范围之内@Length(min=, max=)验证注解的元素值长度是否在min和max区间内@Past验证Date和Calendar对象是否在当前时间之前@Future验证Date和Calendar对象是否在当前时间之后@Pattern验证String对象是否符合正则表达式的规则@Min验证Number和String对象是否大等于指定的值@Max验证Number和String对象是否小等于指定的值@DecimalMax被标注的值必须不大于约束中指定的最大值,参数为通过BigDecimal定义的最大值的字符串表示,小数存在精度@DecimalMin被标注的值必须不小于约束中指定的最小值,参数为通过BigDecimal定义的最小值的字符串表示,小数存在精度@Digits验证Number和String的构成是否合法@Digits(integer=,fraction=)验证字符串是否符合指定格式的数字,integer指定整数精度,fraction指定小数精度@Range(min=, max=)验证注解的元素值在最小值和最大值之间@Range(min=10000,max=50000,message=“range.bean.wage”)验证注解的元素值在最小值10000和最大值50000之间,自定义错误信息为"range.bean.wage"@Valid写在方法参数前,递归地对该对象进行校验,如果关联对象是集合或数组,则对其中的元素进行递归校验;如果是一个map,则对其中的值部分进行校验(是否进行递归验证)@CreditCardNumber信用卡验证@Email验证是否是邮件地址,如果为null,不进行验证,算通过验证@ScriptAssert(lang= ,script=, alias=)自定义脚本验证@URL(protocol=,host=, port=,regexp=, flags=)验证URL的格式是否正确,可以指定协议