参数校验
在程序中,参数校验是有个重复的工作,在不同的接口可能需要编写相同的校验逻辑,而 JSR 定义了 JavaBean 参数校验的元数据模型以及API。
JSR 简介
JSR
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。
任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
JSR-303
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 …
Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
JSR 303 用于对 Java Bean 中的字段的值进行验证。
Spring MVC 3.x 之中也大力支持 JSR-303,可以在控制器中对表单提交的数据方便地验证。
BeanValidation
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull
, @Max
, @ZipCode
, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
JSR303 在项目中的实践
依赖引入
在 SpringBoot 中,只需要引入对应的 starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在非 Spring Boot 项目中,可以直接添加以下依赖
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${api-version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator-version}</version>
</dependency>
在 Spring Boot 2.3 之后 web 模块默认不再依赖 Validation starter,需要单独引入
Spring Bean 校验
如果使用 Spring MVC 应用程序或者 Restful API,可以下面三个地方进行校验:
- Request body
- Path variables.
- Query parameters.
使用内置的校验API
@Data
public class UserRegisterVo {
@NotEmpty(message = "用户名必须填写")
@Length(min = 6, max = 18, message = "用户名必须是6-18个字符")
private String username;
@NotEmpty(message = "用户名必须填写")
@Length(min = 6, max = 18, message = "密码必须是6-18个字符")
private String password;
@NotEmpty(message = "电话号码必须填写")
@Pattern(regexp = "^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$", message = "手机格式不正确")
private String phone;
@NotEmpty(message = "验证码不能为空")
private String code;
}
上面我们为我们的注册表单 Bean 创建的一些简单的校验规则
在请求体中校验
@Controller
public class LoginController {
@PostMapping("/register")
public String register(@Valid UserRegisterVo userRegisterVo,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
// 参数校验、错误结果封装
if (bindingResult.hasErrors()) {
Map<String, String> errorMap = bindingResult.getFieldErrors()
.stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (oldKey, newKey) -> oldKey));
redirectAttributes.addFlashAttribute("errorMap", errorMap);
return "redirect:http://auth.platform.com/register.html";
}
// ....
return "redirect:http://auth.platform.com/login.html";
}
}
上面程序中,有几个需要注意的点:
@Valid
表明将会对UserRegisterVo
模型数据进行校验- 错误的校验信息将会封装到
BindingResult
对象
高级使用-自定义校验
这里我们依照JDK提供的内置校验注解,照葫芦画瓢,自定义实现一个参数校验注解。
创建校验注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {SelectListValuesConstraintValidator.class}
)
public @interface SelectListValues {
/**
* The message element value is used to create the error message.
*/
String message() default "参数不匹配";
/**
* a groups element that specifies the processing groups with which the constraint declaration is associated
*/
Class<?>[] groups() default {};
/**
* specifies the payload with which the the constraint declaration is associated
*/
Class<? extends Payload>[] payload() default {};
/**
* 值在values中的才允许通过
*/
int[] values() default {};
}
上面程序中,需要注意几个点:
@Target
/@Retention
/@Documented
/这几个注解是注解通用的,不赘述@Constraint
指定处理的参数校验器,这里是自己实现的SelectListValuesConstraintValidator
,下面聊message
,校验注解必须要写,表明发生错误时的错误提示groups
,组,先不用管,参照内置的校验注解即可payload
,负载,先不用管,参照内置的校验注解即可values
,自定义内容,下面用到
创建校验器
public class SelectListValuesConstraintValidator implements ConstraintValidator<SelectListValues, Integer> {
Set<Integer> values = new HashSet<>();
@Override
public void initialize(SelectListValues constraintAnnotation) {
// 获取注解中传入的values信息
int[] inputs = constraintAnnotation.values();
// 遍历values信息,放到set中
for (int value : inputs) {
values.add(value);
}
}
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
// 当用户传进来的值符合给定的值才允许通过(return true)
return values.contains(integer);
}
}
上面程序需要注意这几个点:
ConstraintValidator<SelectListValues, Integer>
,第一个类型时对应绑定的注解,第二个类型对应的时修饰的参数类型;initialize
方法,可以从这里拿到用户使用注解的一些信息,比如参数值;isValid
方法,可以获得用户实际传递的值,可以在此处进行校验,如果通过返回true
,否则返回false
。
上面的校验器主要实现了,获取用户在注解中传入的values
内容,判断传进来的值只有在其中校验通过。
常用内置校验注解
表 1. Bean Validation 中内置的 constraint
Constraint | 详细信息 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
表 2. Hibernate Validator 附加的 constraint
Constraint | 详细信息 |
---|---|
@Email | 被注释的元素必须是电子邮箱地址 |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |