概述
Form表单传输到后端的数据,需要经过校验,前端JS校验虽然可以涵盖大部分的校验职责,如生日格式,邮箱格式校验等。但是为了避免用户绕过浏览器,使用http/curl等工具直接向后端请求一些违法数据,造成安全事故,故服务端的数据校验显得更为重要。
JSR303/JSR349/JSR380
JSR303是专家组成员向JCP提交的第一版Bean Validation,即针对Bean数据校验提出的一个规范,使用注解方式实现数据校验。后面有升级版本JSR349及JSR380。各个版本的规范对应关系如下:
- JSR 303:Bean Validation 1.0,伴随着JavaEE 6在2009年发布,Hibernate实现版本为4.3.1.Final
- JSR 349:Bean Validation 1.1,伴随着JavaEE 7在2013年发布,Hibernate实现版本为5.1.1.Final
- JSR 380:Bean Validation 2.0,伴随着JavaEE 8在2017年发布,Hibernate实现版本为6.0.1.Final
主流Bean Validation使用Hibernate实现,如果使用2.0规范,Hibernate必须选择6.0.1以上版本。
JSR规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints
包下,只提供规范不提供实现。
而Hibernate Validation是对这个规范的实践,提供相应的实现,并增加一些其他校验注解,如@Email,@Length,@Range等等,位于org.hibernate.validator.constraints
包下。
Spring对Hibernate Validation进行二次封装,显示校验validated bean时,可以使用Spring Validation或Hibernate Validation,而Spring Validation另一个特性,便是其在Spring MVC模块中添加自动校验,并将校验信息封装进特定的类中。
JSR349
每一个注解都包含message字段,用于校验失败时作为提示信息,特殊的校验注解,如Pattern(正则校验),还可以自己添加正则表达式。
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
建议使用新版本:
新版本GAV:
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.0</version>
</dependency>
关于Jakarta,可参考Jakarta项目介绍。
2018年发布Jakarta的第一个可用版本,最新版本是3.1.0
Jar目录结构如下:
注解
包括如下:
注解 | desc |
---|---|
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
校验被注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 | |
@Future | 限制必须是一个将来日期 |
@FutureOrPresent | 限制必须是一个将来或当前日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Negative | 限制必须为一个负数 |
@NegativeOrZero | 限制必须为一个负数或者0 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0),空格 和制表符等是blank,不是empty |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@NotNull | 限制必须不为null |
@Null | 限制只能为null |
@Past | 限制必须是一个过去的日期 |
@PastOrPresent | 限制必须是一个过去或者当前日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Positive | 限制必须为一个正数 |
@PositiveOrZero | 限制必须为一个正数或者0 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@SupportedValidationTarget | |
@ValidateOnExecution | |
@ConvertGroup | |
@ExtractedValue | |
@UnwrapByDefault | |
@Constraint | |
@GroupSequence | |
@OverridesAttribute | |
@ReportAsSingleViolation | |
@Valid |
Hibernate Validator
如果不使用validation-api
或jakarta.validation-api
,也可考虑使用hibernate-validator
。Hibernate Validator是Jakarta Bean Validation参考实现。
不要使用下面这个GAV:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
推荐使用新的GAV:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
包结构如下图所示:
实际上,hibernate-validator
内部还是依赖jakarta.validation-api
及其他各种三方依赖:
Hibernate Validator提供的未被标注为@Deprecated的校验注解:
注解 | 备注 |
---|---|
@CNPJ | br包下面, |
@CPF | 同上, |
@TituloEleitoral | 同上, |
@NIP | pl包下面, |
@PESEL | 同上, |
@REGON | 同上, |
@INN | ru包下面, |
@DurationMax | time包下面, |
@DurationMin | 同上, |
@CodePointLength | constraints包下面,下面全部都是 |
@ConstraintComposition | |
@CreditCardNumber | 限定数据满足信用卡卡号规则 |
@Currency | 限定数据满足币种规则 |
@EAN | |
@ISBN | 限定数据满足ISBN规则 |
@Length | |
@LuhnCheck | |
@Mod10Check | |
@Mod11Check | |
@Normalized | |
@ParameterScriptAssert | |
@Range | |
@ScriptAssert | |
@UniqueElements | |
@URL | 限定数据满足URL规则 |
Spring Validation
Spring Validation全面支持JSR-303、JSR-349的标准,并封装LocalValidatorFactoryBean作为validator的实现,兼容Spring的validation体系和Hibernate的validation体系,也可以被开发者直接调用,代替上述的从工厂方法中获取的Hibernate validator。
使用Spring Boot,会触发Web模块的自动配置,LocalValidatorFactoryBean已经成为Validator的默认实现,使用时只需要自动注入即可。Maven引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
参考下面分组校验的code,参数Foo前需要加上@Validated注解,表明需要Spring对其进行校验,而校验的信息会存放到其后的BindingResult中,必须相邻。如果有多个参数需要校验,形式如下:foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);
即一个校验类对应一个校验结果。
Spring validation不会在第一个错误发生后立即停止,而是继续试错,告诉所有的错误。
实例
使用校验注解
@Data
public class ErrMsg {
private String field;
private String objectName;
private String message;
}
全局异常配置类:
@ControllerAdvice
public class ExceptionAdvice extends DefaultExceptionAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public List<ErrMsg> exception(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
List<ErrMsg> errMsgs = new ArrayList<>();
allErrors.forEach(objectError -> {
ErrMsg errMsg = new ErrMsg();
FieldError fieldError = (FieldError) objectError;
errMsg.setField(fieldError.getField());
errMsg.setObjectName(fieldError.getObjectName());
errMsg.setMessage(fieldError.getDefaultMessage());
errMsgs.add(errMsg);
});
return errMsgs;
}
}
实体POJO:
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Data
public class SetMealConfigVO {
/**
* 解读信息
*/
@NotEmpty(message = "解读信息不能为空")
private String unscrambleType;
/**
* 套餐有效时长
*/
@NotNull(message = "套餐有效时长不能为空")
@Min(value = 1, message = "不能为负数")
private Integer useDays;
}
@PostMapping("/addOrEdit")
public Result<String> addOrEdit(@RequestBody @Valid SetMealConfigVO configVO, @LoginUser SysUser user) {
}
分组校验
如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。实际需求,如未成年人是不能喝酒,如何校验?
Class Foo {
@Min(value = 18, groups = {Adult.class})
private Integer age;
public interface Adult{}
public interface Minor{}
}
@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
for (FieldError item : bindingResult.getFieldErrors()) {
}
return "fail";
}
return "success";
}
自定义校验注解
作为示例,自定义校验注解@CannotHaveBlank,实现字符串不能包含空格
的校验限制:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
// 自定义注解中指定这个注解真正的验证者类
@Constraint(validatedBy = {CannotHaveBlankValidator.class})
public @interface CannotHaveBlank {
// 默认错误消息
String message() default "不能包含空格";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
// 指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CannotHaveBlank[] value();
}
}
接口ConstraintValidator:
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);// 初始化事件方法
boolean isValid(T value, ConstraintValidatorContext context);// 判断是否合法
}
实现ConstraintValidator接口完成定制校验逻辑的类:
// 所有的验证者都需要实现ConstraintValidator接口
public class CannotHaveBlankValidator implements ConstraintValidator<CannotHaveBlank, String> {
@Override
public void initialize(CannotHaveBlank constraintAnnotation) {
}
@Override
// ConstraintValidatorContext包含认证中所有的信息,
// 获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null && value.contains(" ")) {
// 获取默认提示信息
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
// 禁用默认提示信息
context.disableDefaultConstraintViolation();
// 设置提示语
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
return false;
}
return true;
}
}