文章目录
Maven 依赖
项目为 springboot 项目,主要依赖有以下三个,其中spring-boot-starter-validation包含了 jakarta.validation-api(包含了 javax.validation)、hibernate-validator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Spring MVC Controller 的输入
在 Controller 验证前端传来的参数是我们 Web 开发最常做的事了,现在可以用 validation 取代那一大堆的 if 判断,我们可以用其验证所有请求相关的输入,比如:
- Path Variables
- Request Parameters
- RequestBody
- Header
- …
验证 Path Variables、 Request Parameters、Request Header
@RestController
@Validated
public class ValidatePathVariableController {
@GetMapping("/validatePathVariable/{id}")
public String validateRequestBody(@PathVariable @Length(min = 5, max = 10) String id) {
return id;
}
}
@RestController
@Validated
public class ValidateRequestParamController {
@GetMapping("/validateRequestParam")
public String validateRequestBody(@RequestParam @Length(min = 5, max = 10) String id) {
return id;
}
}
@RestController
@Validated
public class ValidateRequestHeaderController {
@GetMapping("/validateRequestHeader")
public String validateRequestBody(@RequestHeader @Length(min = 5, max = 10) String id) {
return id;
}
}
在以上两个例子中,必须将 @Validated
注解添加在类级别,以通知 Spring 要处理直接加载方法参数上的 Constraints 注解1,这区别于定义一个 POJO 作为被验证对象,即以 Constraints 注解标记字段,再以 POJO 作为方法参数。
验证失败将抛出异常ConstraintViolationException
,而不是MethodArgumentNotValidException
,Spring 没有对 ConstraintViolationException
进行异常处理,会直接 HTTP status 500 (Internal Server Error)。
验证 RequestBody
@RestController
public class ValidateRequestBodyController {
@PostMapping("/validateRequestBody")
public String validateRequestBody(
// 使用 @Valid 也可以
@Validated @RequestBody Input input) {
return input.toString();
}
}
在本例中,必须将 @Validated
或 @Valid
加在参数上,加在类级别、方法级别是无效的。
验证失败会抛出一个异常 MethodArgumentNotValidException
,默认 Spring 会将其转化成 HTTP status 为 400(Bad Request),且打印一条警告。可自定义异常处理器,比如@ExceptionHandler(MethodArgumentNotValidException.class)
来覆盖默认行为。
如果 Input 类中还有一个复杂类型的字段,比如自定义的类需要验证,则要为其添加
@Valid
注解。
非 Controller 组件的方法
验证除了可以应用于 Controller
以外,还可以应用于任意的被 Spring 管理的组件( Spring Component),与在 Controller
中使用有一点不同:
在验证 POJO 时,
Controller
可以直接以@Validated
或@Valid
注解相应的 POJO 参数,不需要类级别的@Validated
。但非Controller
组件需要类级别的@Validated
,且只能用@Valid
注解相应的 POJO 参数。
@Service
@Validated
public class ValidatingService {
public void validateInput(@Valid Input input){
System.out.println(input.toString());
// do something
}
public void validateId(@NotNull String id){
System.out.println(id);
// do something
}
}
自定义 Validator
在这个例子中,要完善 Input
类中对于 IpAddress 的验证,避免出现 111.111.111.333 这种的非法数据。
自定义一个验证需要的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Constraint(validatedBy = StartWithValidator.class)
public @interface StartWith {
String value();
String message() default "{me.zhao.validator.constraints.StartWith.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
# ValidationMessages.properties
me.zhao.validator.constraints.StartWith.message = \u5fc5\u987b\u4ee5{value}\u5f00\u5934: ${validatedValue}
自定义 Constraint 注解注意点
- 必须有
message()
、groups()
、payload()
三个方法,否则会报错 - 需要注解
@Constraint
来指定验证器,通过validatedBy
message
信息需要一个 ValidationMessages.properties 文件承载,命名方式如下图。可以用${validatedValue}
获取被验证的值,用{value}
获取注解的参数值。其他用法需要参考一下下图所示的文件内容。- ValidationMessages.properties 中的 message 要用中文的 Unicode
自定义一个 Validator
public class StartWithValidator implements ConstraintValidator<StartWith, String> {
private StartWith startWith;
@Override
public void initialize(StartWith constraintAnnotation) {
startWith = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
return value.startsWith(startWith.value());
}
}
定义一个 POJO 进行验证
@Data
public class Input2 {
@StartWith("P")
private String id;
}
以纯代码方式使用 Validator,不依赖 Spring 的 @Validate
注解
纯代码方式
我们可以手动创建一个 Validator
来完成验证,而不需要 Spring 提供任何支持。
@Service
public class ProgrammaticallyValidateService {
public String validateNoSpring(Input input) {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<Input>> constraintViolations = validator.validate(input);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
}
return input.toString();
}
}
当 constraintViolations
为空时,表示验证通过。
Spring 的非注解方式
除了上面那种手动创建一个 Validator
的方式,Spring 也预置了一个 Validator
,我们可以通过自动注入的方式使用它。
@Service
public class ProgrammaticallyValidateService {
private final Validator validator;
public ProgrammaticallyValidateService(Validator validator) {
this.validator = validator;
}
public String validateWithSpringValidator(Input input) {
Set<ConstraintViolation<Input>> constraintViolations = validator.validate(input);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
}
return input.toString();
}
}
验证组 group
一个 POJO 有时需要在多个地方使用,且每个情景下对 POJO 的要求不同,比如 insert
时要求 id
必须为空,update
时要求 id
不能为空,这时可以定义多个验证组。
非 Controller 中的使用
定义 group
public class InputGroup {
public interface Insert{}
public interface Update{}
}
被验证 POJO
id 参数被注解两次,且应用于不同的 group
@Data
public class InputWithGroup {
@Null(groups = {InputGroup.Insert.class})
@NotNull(groups = {InputGroup.Update.class})
private String id;
}
Service 验证
@Service
@Validated(InputGroup.Insert.class)
public class GroupValidateService {
@Validated(InputGroup.Insert.class)
public String validateInsert(@Valid InputWithGroup input) {
return input.toString();
}
@Validated(InputGroup.Update.class)
public String validateUpdate(@Valid InputWithGroup input) {
return input.toString();
}
}
- 必须在类级别添加
@Validate
,可同时指定组,比如@Validate(InputGroup.Insert.class)
- 方法级别的
@Validate(group)
可以覆盖类级别的 group - 由于
@Validate
不能用于非 Controller 之外的方法参数上,所有无法用参数级别的 group 覆盖类、方法级别的 - 如果在 POJO 中指定了
group
,但@Validate(这里没有写)
没有指定,是不起作用的
在 Controller 中使用 group
@RestController
public class GroupValidateController {
@PostMapping("validateInsert")
public String validateInsert(@Validated(InputGroup.Insert.class) @RequestBody InputWithGroup input) {
return input.toString();
}
@PostMapping("validateUpdate")
public String validateUpdate(@Validated(InputGroup.Update.class) @RequestBody InputWithGroup input) {
return input.toString();
}
}
- 可直接在参数级别的 `@Validate` 中指定 group
嵌套验证
嵌套验证是指一个对象 A 的某个字段 b 是另一个对象 B,在验证 A 对象其它字段的同时,也要验证 B 对象的字段,若 B 对象也有字段为对象,要一直验证下去。
嵌套验证的开启很简单,只需在需要嵌套验证的字段上添加 @Valid
注解。如下代码,A
类中的 b
字段、list
字段。
@Data
public class A {
@NotNull
@Length(min = 1)
private String name;
@Valid
@NotNull
private B b;
@Valid
@NotNull
@Size(min = 1)
private List<B> list;
}
@Data
public class B {
@NotNull
@Max(10)
private Integer number;
}
@Service
@Validated
public class ValidateNestService {
public String validateNest(@Valid A a) {
return a.toString();
}
}
校验模式:普通模式 -> 快速失败
普通模式:进行所有检验,然后统一返回所有错误。默认为此模式
快速失败:当遇到检验失败时,停止检验,直接返回
手动创建 Validator
如果是手动创建 Validator 进行验证的话,可采用以下方式将 failFast 设置为 true。
public String validateFailFast(Input input) {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<Input>> constraintViolations = validator.validate(input);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
}
return input.toString();
}
注入自己的 Validator
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}
以上为 Spring Validator 的自动注入,可以看出,在没有配置 Validator.class
的时候,会默认注入一个 LocalValidatorFactoryBean
作为 Validator。那我们可以打破这个条件,配置一个自己的 Validator。
@Configuration
public class ValidationConfig {
@Bean
public Validator validator() {
return Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();
}
}
修改 Spring 的 LocalValidatorFactoryBean 的属性
如图,通过 debug 能够找到一条路径 localValidatorFactoryBean -> targetValidator -> validatorScopedContext -> failFast,但 LocalValidatorFactoryBean
并没有方法直接获取 targetValidator
,还需要自己通过反射一层层的获取,然后修改值,比较麻烦,没有动手操作,这里只记录一个思路。
验证失效的原因之一 jar 冲突
可以用 IDEA 的 maven helper 插件找问题,输入 valid 查询是否有冲突的 jar。
参考
- All You Need To Know About Bean Validation With Spring Boot
- Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动作【享学Spring】
- Bean Validation完结篇:你必须关注的边边角角(约束级联、自定义约束、自定义校验器、国际化失败消息…)【享学Spring】
- 深入了解数据校验(Bean Validation):从深处去掌握@Valid的作用(级联校验)以及常用约束注解的解释说明【享学Java】
- Spring:自定义Validator的实现
- springboot使用hibernate validator校验
- Spring4 对Bean Validation规范的新支持(方法级别验证)
- SpringBoot校验(validation)
Constraints 注解指的是
java.validation
、hibernate.validator
中定义的注解,比如@NotNull
、@NotBlank
等等。 ↩︎