数据校验validation
概述
form表单传输到后端的数据,需要经过校验,前端的JS校验虽然可以涵盖大部分的校验职责,如生日格式,邮箱格式校验等。但是为了避免用户绕过浏览器,使用http/curl等工具直接向后端请求一些违法数据,造成安全事故,故服务端的数据校验显得更为重要。
JSR303/JSR349/JSR380
JSR303是专家组成员向JCP提交的第一版Bean Validation,即针对bean数据校验提出的一个规范,使用注解方式实现数据校验。后面有升级版本JSR349及JSR380。各个版本的规范对应关系如下:
JSR303伴随着JAVAEE 6在2009年发布,Hibernate实现版本4.3.1.Final,Apache BVal实现版本0.5
JSR349伴随着JAVAEE 7在2013年发布,Hibernate实现版本5.1.1.Final,Apache BVal实现版本1.1.2
JSR380伴随着JAVAEE 8在2017年发布,完全兼容低版本的JAVA SE,Hibernate实现版本6.0.1.Final,Apache BVal实现版本2.0.3(不太确定)
主流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另一个特性,便是其在springmvc模块中添加自动校验,并将校验信息封装进特定的类中。
JSR349
每一个注解都包含message字段,用于校验失败时作为提示信息,特殊的校验注解,如Pattern(正则校验),还可以自己添加正则表达式。
https://github.com/beanvalidation/beanvalidation-api
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
和API组织结构几乎一模一样(没有一一去对比),建议使用新版本
Maven Repository: Search/Browse/Explore
https://mvnrepository.com/
使用搜索Maven Central Repository
搜索jakarta.validation-api
,得到如下:
https://search.maven.org/artifact/jakarta.validation/jakarta.validation-api
可知其官网、GitHub
搜索可知,2018年才发布第一个可用版本,最新版本是3.0.0
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>
故而在此之前,一般选择使用
Hibernate Validator Engine
Hibernate’s Jakarta Bean Validation reference implementation.
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.1.Final</version>
</dependency>
不要使用下面这个GAV:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.1.Final</version>
</dependency>
实例
@Data
public class ErrorMsg {
private String field;
private String objectName;
private String message;
}
全局异常配置类:
@ControllerAdvice
public class ExceptionAdvice extends DefaultExceptionAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public List<ErrorMsg> exception(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
List<ErrorMsg> errorMsgs = new ArrayList<>();
allErrors.forEach(objectError -> {
ErrorMsg errorMsg = new ErrorMsg();
FieldError fieldError = (FieldError) objectError;
errorMsg.setField(fieldError.getField());
errorMsg.setObjectName(fieldError.getObjectName());
errorMsg.setMessage(fieldError.getDefaultMessage());
errorMsgs.add(errorMsg);
});
return errorMsgs;
}
}
实体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) {
}
2.0.2和3.0.0版本提供的注解一模一样:
注解 | 备注 |
---|---|
@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) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@NotNull | 限制必须不为null |
@Null | 限制只能为null |
@Past | 限制必须是一个过去的日期 |
@PastOrPresent | 限制必须是一个过去或者当前日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Positive | 限制必须为一个正数 |
@PositiveOrZero | 限制必须为一个正数或者0 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
Hibernate Validator
Hibernate Validator提供的校验注解,未包括标注为@Deprecated的:
注解 | 备注 |
---|---|
@CNPJ | br包下面, |
@CPF | br包下面, |
@TituloEleitoral | br包下面, |
@NIP | pl包下面, |
@PESEL | pl包下面, |
@REGON | pl包下面, |
@INN | ru包下面, |
@DurationMax | time包下面, |
@DurationMin | time包下面, |
@CodePointLength | |
@ConstraintComposition | |
@CreditCardNumber | |
@Currency | |
@EAN | |
@ISBN | |
@Length | |
@LuhnCheck | |
@Mod10Check | |
@Mod11Check | |
@Normalized | |
@ParameterScriptAssert | |
@Range | |
@ScriptAssert | |
@UniqueElements | |
@URL |
Spring validation
Spring validation全面支持JSR-303、JSR-349的标准,并封装LocalValidatorFactoryBean作为validator的实现,兼容spring的validation体系和hibernate的validation体系,也可以被开发者直接调用,代替上述的从工厂方法中获取的hibernate validator。使用springboot,会触发web模块的自动配置,LocalValidatorFactoryBean已经成为Validator的默认实现,使用时只需要自动注入即可。
参考下面分组校验的code,参数Foo前需要加上@Validated注解,表明需要Spring对其进行校验,而校验的信息会存放到其后的BindingResult中,必须相邻。如果有多个参数需要校验,形式如下:foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);
即一个校验类对应一个校验结果。
Spring validation不会在第一个错误发生后立即停止,而是继续试错,告诉所有的错误。
分组校验
如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。实际需求,如未成年人是不能喝酒,如何校验?
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;
}
}
参考
@valid与@validated
https://blog.csdn.net/gaojp008/article/details/80583301
https://blog.csdn.net/qq_37209293/article/details/85613319
SpringMVC中实现Bean Validation(JSR 303 JSR 349 JSR 380)
https://blog.csdn.net/u013815546/article/details/77248003