Spring Validation详解:如何通过注解简化参数校验并分离业务逻辑
spring validation是什么

在平时的需求开发过程中,时常会有参数校验方面的需求。例如,对于一个接收用户注册请求的接口,需要对用户传入的用户名进行校验,确保其不能为空,并且用户名的长度不超过 20 个字符;同时,还要校验传入的手机号是否为合法的手机号格式等。当遇到这种情况时,很多人会在 controller 接口里编写一些判断代码。但是我们提供了一个修改用户信息的接口,此接口需对用户参数进行校验,这样我们的校验代码就会分布在两个地方,与真正的业务处理逻辑相耦合。并且未来若要新增一种校验逻辑,还需修改多个地方。
spring validation 能够以注解的形式来规定对象的校验规则,将校验与业务逻辑进行分离,从而使代码的编写更为便捷。
我们定义了一个名为 UserRegisterParam 的类,同时给 username 字段和 age 字段添加了 javax.validation 包中已有的一些验证注解。
@Data
class UserRegisterParam {
@NotBlank
@Size(min = 1, max = 20)
private String username;
@Min(1)
private int age;
}
接着在一个 Controller 的 RequestMapping 方法里,我们将这个类设定为方法的输入参数,同时给这个参数添加@Valid 注解。
@RequestMapping("/register")
公共的对象进行注册操作,注册的参数是经过验证的 UserRegisterParam 类型的 param 对象。
// 正常处理逻辑
}
如果这个请求的参数不满足 UserRegisterParam 类所定义的 validation 要求,那么 spring 将会抛出异常。例如出现用户名超长的情况,我们能够在统一的异常拦截器中把异常捕获并进行处理,然后将结果返回给用户,关于后面的使用部分将会进行介绍。
org.springframework.validation 出现了 BindException,其中包含 org.springframework.validation.BeanPropertyBindingResult,存在 1 个错误。
对象“userRegisterParam”在“username”字段上出现字段错误:拒绝值[11111111111111111111111];代码[Size.userRegisterParam.username,Size.username,Size.java.lang.String,Size];参数[org.springframework.context.support.DefaultMessageSourceResolvable:代码[userRegisterParam.username,username];参数[];默认消息[username],20,1];默认消息[个数必须在 1 和 20 之间]
在 spring-boot 中如何使用 spring validation;在 spring-mvc 中怎样使用 spring validation。
当 classpath 中包含了 validator 的实现包,例如
org.hibernate.validator:hibernate-validator 之后,spring 能够自动完成 validator 相关的配置,无需我们进行额外的配置工作。
我们在 springboot 项目里引入了 hibernate-validator 的依赖,接着 spring 会自动去注册一个默认的 Validator。
org.hibernate.validator
hibernate-validator
接着能够在 controller 里添加 @Validated 注解,以此对参数展开校验。
给参数类增加注解限制,常见的注解可以在
在 javax.validation.constraints 这个包中能够看到。
比如@Max、@Min、@PositiveOrZero 这些。@Positive 是用来限制数字的大小的;@NotBlank 是用于限制字符串不能为 null 或者只包含空字符串的;@NotEmpty 是用来限制字符串、集合、map、数组等不能为空的;@NotNull 是用来限制对象不能为空的。
公共类 UserRegisterParam 。
@NotNull
@Size,其最小值为 2,最大值为 5,并且消息为“用户名长度必须在 2 到 5 之间”
private String username;
public String getUsername() {
return username;
}
设置用户名这个操作,是将给定的字符串 username 进行设置。
this.username = username;
}
}
在controller参数上增加@Validated注解。
@RequestMapping("/register")
public Result>对 @Validated UserRegisterParam 类型的 requestParam 进行注册操作。 注册这个动作针对的是带有 @Validated 注解且类型为 UserRegisterParam 的 requestParam 。 需执行注册的对象是具有特定类型和注解的 requestParam 。 注册操作的对象是具备特定条件的 requestParam 。 对满足特定要求的 requestParam 进行注册处理。
return null;
}
转换验证异常为返回值
在默认情况下,spring 对于不符合 validation 验证的请求,其处理方式是返回一个 http400 错误码。然而,如果我们希望给请求的用户返回一些更为详细的信息,那么该如何实现呢?
可以利用 spring 的 ExceptionAdvice 功能来对异常进行统一的捕获与处理。校验失败时所返回的异常为 BindException。
我们下面的代码会返回一个 Result 对象,这个对象中的 code 和 message 能够依据自身的需求进行映射。倘若要对错误信息进行修改,就可以借助 BindException 的 getFieldErrors 方法来获取详细的字段以及错误原因。
@RestControllerAdvice
公共类 ApplicationExceptionAdvice 。
在处理异常时,针对 BindException 类进行相应的处理。
public Result>处理可抛出异常(HttpServletRequest 请求,BindException 异常)。
Result result = new Result();
result.setCode(1);
result 把 e 的消息设定为自己的消息。
return result;
}
}
自定义validation
javax.validation 中的注解能够满足较多的校验需求。然而,倘若存在一些特殊的校验情况,我们是可以进行自定义校验注解以及规则的。具体只需两步,其一为定义注解,其二为定义注解所对应的 ConstraintValidator。
我们以下面这个为例,即实现一个校验数字必须是奇数的校验注解,来展示实现过程。
首先定义一个注解,将其命名为 OddNum 。在这个注解中,@Constraint 的 validatedBy 用于指定对应的 ConstraintValidator ,也就是注解校验规则的实现。并且要默认加上 message()、groups()、payload()这几个方法。
针对方法(ElementType.METHOD)和字段(ElementType.FIELD)这两个元素类型进行标注(@Target)。
使用了 @Retention(RetentionPolicy.RUNTIME) 这个注解。 这个注解被用于指定类、方法、字段等元素的保留策略为运行时。 它决定了被注解的元素在编译后的字节码中是否会被保留以及在运行时是否可以被访问。 通常,在需要在运行时动态获取和使用注解信息的情况下,会使用这种保留策略。
使用了 @Constraint 注解,并且其验证器为 OddNumConstraintValidator.class
public @interface OddNum {
默认的 String message() 表示必须是奇数。
Class>[] groups() default { };
Class extends payload>[] payload() default { };
}
接着定义与之对应的 OddNumConstraintValidator 。这个类需要实现 ConstraintValidator ,并且通过泛型来引用对应的注解以及所支持的校验对象类型。在我们这里,奇数能够支持数字类型,也就是使用 Number 类。然后需要实现两个方法,一个是 initialize 方法,它能够从注解中获取用户配置的值,例如@Max 中的 value 值,并将其保存在字段中,以便在 isValid 方法中使用。因为我们的奇数校验判断不需要用户传入配置参数,所以这一步就被省略了。然后由 isValid 方法来负责校验,其中 value 是需要进行校验的对应值。我们在此进行了一个较为简单的数字奇数校验操作,若校验通过则返回 true,若校验不通过则返回 false。
公共类 OddNumConstraintValidator 实现了约束验证器。 它用于验证某个数值是否为奇数。 这个验证器在特定的约束条件下发挥作用。 它可以检查输入的数值是否满足奇数的定义。 这样就能确保在相关的应用场景中,只有奇数能够通过验证。 {
@Override
public void 初始化(OddNum 约束注解);
ConstraintValidator 调用 super.initialize(constraintAnnotation) 方法。
}
@Override
对于给定的数值 value 和约束验证器上下文 context,判断其是否有效。
返回值不为空且该值的长整型值除以 2 的余数为 1
}
}
定义完毕之后,便能够如同使用 javax.validation 包中的注解那般去使用了。
如何实现 spring validation(hibernate-validator)?
Jakarta Bean Validation 中定义了 javax.validation 这些规范,其中规范对应的实现最常用的是 hibernate validator。
在spring mvc中,有
LanguageProcessor 类的职责是把请求参数绑定到 RequestMapping 方法的参数对象上,并且会进行参数的校验工作。
在 validateIfApplicable 这个过程中会调用 binder.validate 。spring 能够找到已经加载的 validatator ,并利用它来进行 validate 。
在进行验证操作时,如果适用,需要对 WebDataBinder binder 和 MethodParameter parameter 进行验证。需要判断在特定情况下是否应该进行验证操作,然后根据判断结果对这两个参数进行相应的处理。如果满足验证条件,就启动验证流程;如果不满足,就不进行验证。总之,要根据具体情况来决定是否对这两个参数进行验证。
对于每一个 Annotation ann 而言,它都属于 parameter.getParameterAnnotations() 中的一个。
最终得到的验证提示存储在 validationHints 数组中。
如果 validationHints 不为空。
binder 对 validationHints 进行了验证。
break;
}
}
}
公共的方法用于进行验证,验证的参数是一个可变参数对象数组,其中包含了验证提示信息。
Object target = getTarget();
判断状态:目标不为空,即“没有要验证的目标”。因为断言状态要求目标不为空,若目标为空则会触发该断言。所以当目标为 null 时,会输出“没有要验证的目标”的错误信息。
获取绑定结果,结果为 BindingResult 类型,赋值给 bindingResult 变量。
调用每个验证器并使用相同的绑定结果。
获取到验证器列表后,对于其中的每一个验证器(Validator):
如果验证提示不为空且验证器是智能验证器,那么就进行相关操作。
SmartValidator 的 validator 对 target 进行验证,将验证结果存储在 bindingResult 中,并提供 validationHints 。
}
else if (validator != null) {
使用 validator 对 target 进行验证,并将结果存储在 bindingResult 中。
}
}
}
如果 classloader 中存在 hibernate-validator,那么就会把校验工作委托给 hibernate 的 ValidatorImpl 来进行。在 ValidatorImpl 中,首先从对应的类里读取相关的校验规则注解信息,将其构成 BeanMetaData。接着构建上下文信息 BaseBeanValidationContext。最后调用 validateInContext。
public final Set> validate(T object, Class>... groups) {
Contracts 确保对象不为空,使用了 MESSAGES.validatedObjectMustNotBeNull() 这个消息来进行验证。
sanityCheckGroups( groups );
使用了 @SuppressWarnings("unchecked") 这个注解。它用于抑制特定类型的编译器警告。通常在代码中,当我们进行一些可能会导致类型不安全的操作时,会使用这个注解来告诉编译器不要发出相关的警告。例如在泛型编程中,当我们进行类型转换时,可能会出现类型不安全的情况,这时就可以使用 @SuppressWarnings("unchecked") 来避免编译器发出警告。
Class rootBeanClass = (Class) object.getClass();
BeanMetaDatabeanMetaDataManager 获取了根 Bean 类的 Bean 元数据,这个获取到的 Bean 元数据被赋值给了 rootBeanMetaData 。
如果根 Bean 元数据不包含约束。
返回一个空的集合。
}
BaseBeanValidationContext同时还涉及到 rootBeanMetaData 和 object 。
ValidationOrder 是通过 determineGroupValidationOrder( groups )来确定的。
BeanValueContext, object>valueContext 是从 ValueContexts 中获取到的用于该 bean 的本地执行上下文。
获取参数名提供器的验证器作用域上下文。
object,
获取验证上下文的根 Bean 元数据。
PathImpl.createRootPath()
);
验证在上下文中进行,需返回验证结果。验证上下文为 validationContext,值上下文为 valueContext,验证顺序为 validationOrder。
}
最终会加载各个 Constraint 的校验实现逻辑来对目标 bean 进行校验。例如,我们刚定义的 OddNumConstraintValidator 会在此时发挥作用。
在 SimpleConstraintTree 的 validateConstraints 中进行加载方面的调用。validateConstraints先依据 validationContext 和 valueContext 去获取对应的 validator,在对我们的@OddNum 注解的参数进行处理时,能够获取到 OddNumConstraintValidator。
要验证约束条件,需使用验证上下文。在进行验证时,需依据相关的验证规则和条件来判断数据是否符合要求。如果数据不符合要求,需要给出相应的错误提示或处理措施。> validationContext,
ValueContext,> valueContext,
Collection违反了约束验证器上下文。
找到合适的约束验证器。
ConstraintValidator验证器 = 获取初始化后的约束验证器(验证上下文, 值上下文);
创建一个约束验证器上下文。
ConstraintValidatorContextImpl 是一个约束验证器上下文对象,它是由 validationContext 创建出来的,用于特定的上下文环境。这个上下文对象可以用于在验证过程中提供相关的信息和上下文。
获取属性路径,其中属性路径来自 valueContext
);
// validate
如果对 valueContext 和 constraintValidatorContext 进行 validateSingleConstraint 操作后得到的结果有值,那么就进行后续处理。
将 constraintValidatorContext 添加到 violatedConstraintValidatorContexts 中。
}
}
最后,validateSingleConstraint 方法会去调用 OddNumConstraintValidator 的 isValid 方法。
protected final Optional validateSingleConstraint(
ValueContext,> valueContext,
ConstraintValidatorContextImpl 为 constraintValidatorContext 。
ConstraintValidator validator) {
boolean isValid;
try {
@SuppressWarnings 这个注解用于抑制特定类型的编译器警告。它可以应用于方法、类或代码块等。通过使用 @SuppressWarnings 注解,可以告诉编译器忽略某些特定的警告信息,从而避免不必要的编译错误或警告提示。在 Java 编程中,经常会使用 @SuppressWarnings 注解来处理一些常见的编译器警告,例如未检查的类型转换、未使用的变量等。
V 经过验证的值等于从值上下文中获取到的当前经过验证的值并转换为 V 类型。
validator 对 validatedValue 和 constraintValidatorContext 进行验证,验证结果赋值给 isValid,即 isValid = validator.isValid( validatedValue, constraintValidatorContext )。
}
catch (RuntimeException e) {
如果 e 属于 ConstraintDeclarationException 这种情况。
throw e;
}
利用 LOG.getExceptionDuringIsValidCallException( e )以抛出异常。
}
if ( !isValid ) {
我们目前还没有添加这些违规行为,因为我们不知道它们是怎样的。
会对最终的布尔求值产生影响。
可以返回包含 constraintValidatorContext 的 Optional 对象。
}
return Optional.empty();
}