概述
校验例子
大家平时编码中经常涉及参数的校验,对于一个用户注册的方法来说会校验用户名密码信息:
public class UserController {public ResponseEntity<String> registerUser(String username, String password) {if (username == null || username.isEmpty()) {return ResponseEntity.badRequest().body("用户名不能为空");}if (password == null || password.isEmpty()) {return ResponseEntity.badRequest().body("密码不能为空");}if (password.length() < 6) {return ResponseEntity.badRequest().body("密码长度至少为6位");}// 处理用户注册逻辑return ResponseEntity.ok("用户注册成功");}
}
上述例子中需要手动编写参数校验逻辑的过程。虽然对于这个简单的示例而言,手动编写校验逻辑可能是可行的,但是对于复杂的验证规则和多个参数的情况,手动编写校验逻辑会变得冗长、难以维护和复用。
引入现代的校验框架如Spring Validation
可以帮助解决这些问题,提供更高效、统一和可维护的参数校验方案。
Bean Validation规范
-
JSR303/JSR-349/JSR-380: JSR303(
Bean Validation
)是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。JSR-349(Bean Validation 1.1
)是其的升级版本,添加了一些新特性。JSR-380(Bean Validation 2.0
,JSR380标准 )对其规范进一步扩展和增强。 -
hibernate validation
:hibernate validation
是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等 -
Spring validation
:spring validation
对hibernate validation
进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中
Bean Validation的主页:http://beanvalidation.org
Bean Validation的参考实现:https://github.com/hibernate/hibernate-validator
相关版本兼容性
Bean Validation | Hibernate Validation | JDK | Spring Boot |
1.1 | 5.4 + | 6+ | 1.5.x |
2.0 | 6.0 + | 8+ | 2.0.x |
3.0 | 7.0 + | 9+ | 2.0.x |
3.0后Bean Validation
改名为Jakarta Bean Validation 3.0
了。
如果你的项目版本是jdk1.8的,不要使用hibernate-validator 7.0
的版本,它里面的依赖的jakarta.validation-api:3.0
是需要jdk1.9的部分支持的。
Spring Validation注解
Spring Validation建立在Java Bean Validation(JSR 380)的基础上,为开发人员提供了一组注解和工具,用于定义和执行数据验证规则。它允许开发人员在应用程序中定义验证规则,并使用这些规则来验证输入数据、请求参数、领域对象等。
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
常用注解标签如下:
标签 | 说明 |
---|---|
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
hibernate-validator 校验Java Bean
-
pom引入hibernate-validator
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.2.0.Final</version>
</dependency>
-
创建一个Java Bean,我们校验一下用户名跟年龄
public class User {@NotBlank(message = "用户名不能为空")private String username;@Min(value = 18, message = "年龄不能小于18岁")private int age;// 构造函数、Getter 和 Setter 方法
}
-
执行校验
public class ValidatorTest {public static void main(String[] args) {// 创建校验器ValidatorFactory factory = Validation.buildDefaultValidatorFactory();Validator validator = factory.getValidator();// 创建用户对象User user = new User();user.setUsername("");user.setAge(16);// 执行校验Set<ConstraintViolation<User>> violations = validator.validate(user);// 处理校验结果if (!violations.isEmpty()) {for (ConstraintViolation<User> violation : violations) {System.out.println(violation.getMessage());}} else {System.out.println("校验通过");}}
}
用Spring Validation提高生产力
Spring Validation引入
添加pom依赖
Spring Validation
校验包被独立成了一个starter
组件,引入如下依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
如果是spring-boot-starter-web
不用引入了,spring-boot-starter-web
集成了spring-boot-starter-validation
,默认可以不加spring-boot-starter-validation
,它同时也集成了hibernate-validator
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
hibernate Validator校验器
这里我们定义hibernate校验器用于校验参数
@Configuration@EnableAutoConfigurationpublic class HibernateValidatorConfiguration {@Beanpublic MethodValidationPostProcessor methodValidationPostProcessor() {MethodValidationPostProcessor processor = new MethodValidationPostProcessor();processor.setValidator(validator());processor.setProxyTargetClass(true);return processor;}@Beanpublic Validator validator() {return Validation.byProvider(HibernateValidator.class).configure().addProperty("hibernate.validator.fail_fast", "true").buildValidatorFactory().getValidator();}}
全局异常处理
每个Controller
方法中都如果都写一遍对校验结果信息的处理,使用起来还是很繁琐。可以通过全局异常处理的方式统一处理校验异常。
@ControllerAdvice@Slf4jpublic class GlobalExceptionHandler {@ExceptionHandler(value = ConstraintViolationException.class)@ResponseBodypublic ApiResult defaultInsuranceExceptionHandler(ConstraintViolationException e) {log.error("校验错误:{}", e.getMessage());return ApiResult.error("error", e.getMessage());}@ExceptionHandler(value = MissingServletRequestParameterException.class)@ResponseBodypublic ApiResult handleMissingServletRequestParameter(MissingServletRequestParameterException ex) {String error = ex.getParameterName() + " 参数为空";return ApiResult.error("error", error);}}
Controller接口Bean校验
首先,假设我们有一个用户注册的请求对象 UserRegistrationRequest
,其中包含用户名和密码字段。
@Data
public class UserRegistrationRequest {@NotBlank(message = "用户名不能为空")private String username;@NotBlank(message = "密码不能为空")@Size(min = 6, message = "密码长度至少为6位")private String password;
}
我们使用了两个注解进行参数校验:
-
@NotBlank
:该注解用于验证字段不能为空或空格,并可以通过message
属性指定验证失败时的错误消息。 -
@Size
:该注解用于验证字段的长度,我们指定了密码的最小长度为6,并通过message
属性定义了验证失败时的错误消息。
接下来我们定义一个Controller接口
@RestController
public class UserController {@PostMapping("/register")public ResponseEntity<String> registerUser(@Valid @RequestBody UserRegistrationRequest request) {// 处理用户注册逻辑return ResponseEntity.ok("用户注册成功");}
}
@RequestParam 参数校验
首先需要将MethodValidationPostProcessor设置成cglib代理
processor.setProxyTargetClass(true);
定义一个接口,我们需要对ID进行校验,1<=id<=400
@Validated
public interface ValidClient {@GetMapping("/valid")String queryById(@RequestParam("id") @Min(1) @Max(400) Integer id);}
controller实现
@RestController
public class ValidController implements ValidClient {@Overridepublic String queryById(Integer id) {return "id:" + id;}
}
分组校验
还是看上面那个Controller接口校验的例子,如果我们注册的时候需要校验用户名和密码,重置密码的时候只校验密码该怎么校验呢?这个时候就用到了分组校验了。
-
首先,我们需要定义一个新的分组,用于更新场景中的验证,例如
UpdateGroup
。
public interface UpdateGroup {
}
-
接下来,我们需要在
UserRegistrationRequest
类的username
字段上使用@Validated
注解,并通过groups
属性指定要应用的验证分组。
public class UserRegistrationRequest {@NotBlank(message = "用户名不能为空", groups = {RegistrationGroup.class})private String username;@NotBlank(message = "密码不能为空")@Size(min = 6, message = "密码长度至少为6位")private String password;// 其他字段和方法
}
在上述示例中,我们将@Validated
注解应用到username
字段,并通过groups
属性指定了RegistrationGroup.class
,这意味着在注册场景中会进行校验。
-
我们可以使用
@Validated
注解来指定不要应用任何验证分组。
@RestController
public class UserController {@PostMapping("/register")public ResponseEntity<String> registerUser(@Validated(RegistrationGroup.class) @RequestBody UserRegistrationRequest request) {// 处理用户注册逻辑return ResponseEntity.ok("用户注册成功");}@PostMapping("/update")public ResponseEntity<String> updateUser(@Validated(UpdateGroup.class) @RequestBody UserRegistrationRequest request) {// 处理用户更新逻辑return ResponseEntity.ok("用户更新成功");}
}
在上述示例中,我们在updateUser
方法中使用@Validated(UpdateGroup.class)
来指定在更新场景中只执行UpdateGroup
分组的验证规则。由于username
字段上没有指定groups
属性,所以在更新场景中将不会对username
字段进行校验。
自定义注解
Spring 的 validation 为我们提供了许多特性,几乎可以满足日常开发中绝大多数参数校验场景了。但是,一个好的框架一定是方便扩展的。有了扩展能力,就能应对更多复杂的业务场景,下面我们自定义一个日期格式校验的注解
定义注解接口
@Documented@Constraint(validatedBy = DateFormatValidator.class)@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)public @interface DateFormat {//默认错误消息String message() default "时间格式错误";//分组Class<?>[] groups() default {};//默认日期格式String formatter() default "yyyy-MM-dd";//负载Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documented@interface List {DateFormat[] value();}}
定义校验器
public class DateFormatValidator implements ConstraintValidator<DateFormat, String> {protected String dateFormatter;@Overridepublic void initialize(DateFormat constraintAnnotation) {this.dateFormatter = constraintAnnotation.formatter();}@Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {if (StringUtils.isNotBlank(value)) {try {DateUtils.parseDate(value, dateFormatter);} catch (Exception e) {return false;}return true;}return false;}
}
自定义校验注解使用起来和官方注解没有区别,在需要的字段上添加相应注解即可。
public class RequestParam {@DateFormat(message = "日期输入错误")private String beginDate;// 其他字段和方法
}