数据校验是任何一个应用程序都会用到的功能,无论是显示层还是持久层. 通常,相同的校验逻辑会分散在各个层中, 这样,不仅浪费了时间还会导致错误的发生(译注: 重复代码). 为了避免重复, 开发人员经常会把这些校验逻辑直接写在领域模型里面, 但是这样又把领域模型代码和校验代码混杂在了一起, 而这些校验逻辑更应该是描述领域模型的元数据.
JSR 303 - Bean Validation - 为实体验证定义了元数据模型和API. 默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展. Bean Validation API 并不局限于应用程序的某一层或者哪种编程模型, 例如,如图所示, Bean Validation 可以被用在任何一层, 或者是像类似Swing的富客户端程序中.
Hibernate Validator is the reference implementation of
this JSR. The implementation itself as well as the Bean Validation API and TCK are all provided and distributed under the Apache Software License 2.0
JSR提供的校验注解:
@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(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator提供的校验注解:
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内
spring-boot-starter-web包自动依赖hibernate-validator,不用再重复引入,直接开搞
org.springframework.boot
spring-boot-starter-web
org.hibernate
hibernate-validator
5.3.1.Final
声明一个bean注册到spring容器,这个bean是一个容器后处理器,会把校验的逻辑通过AOP织入有@Validated注解的class,具体可以看这个类的源码
这一步在springboot其实也不用做,ValidationAutoConfiguration这个配置类自动帮我们做了
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(){
return new MethodValidationPostProcessor();
}
验证不通过会产生异常,因为我们项目提供rest接口,所以通过全局捕获异常,然后转换为json给前台
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 用来处理bean validation异常
* @param ex
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public WebResult resolveConstraintViolationException(ConstraintViolationException ex){
WebResult errorWebResult = new WebResult(WebResult.FAILED);
Set> constraintViolations = ex.getConstraintViolations();
if(!CollectionUtils.isEmpty(constraintViolations)){
StringBuilder msgBuilder = new StringBuilder();
for(ConstraintViolation constraintViolation :constraintViolations){
msgBuilder.append(constraintViolation.getMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if(errorMessage.length()>1){
errorMessage = errorMessage.substring(0,errorMessage.length()-1);
}
errorWebResult.setInfo(errorMessage);
return errorWebResult;
}
errorWebResult.setInfo(ex.getMessage());
return errorWebResult;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public WebResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){
WebResult errorWebResult = new WebResult(WebResult.FAILED);
List objectErrors = ex.getBindingResult().getAllErrors();
if(!CollectionUtils.isEmpty(objectErrors)) {
StringBuilder msgBuilder = new StringBuilder();
for (ObjectError objectError : objectErrors) {
msgBuilder.append(objectError.getDefaultMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if (errorMessage.length() > 1) {
errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
}
errorWebResult.setInfo(errorMessage);
return errorWebResult;
}
errorWebResult.setInfo(ex.getMessage());
return errorWebResult;
}
}
这两个异常分别对应校验的两种使用方式
可以自定义异常格式
package com.start.base.common.validator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class CommonValidator implements InitializingBean {private Validator validator;
//实现校验方法并返回结果
public ValidationResult validate(Object bean) {
ValidationResult validationResult = new ValidationResult();
Set> constraintViolations = validator.validate(bean);
if (constraintViolations.size() > 0) {
//有错误
validationResult.setHasError(true);
MaperrMsgMap = new HashMap<>();
constraintViolations.forEach(constraintViolation -> {
String errMsg = constraintViolation.getMessage();
String propertyName = constraintViolation.getPropertyPath().toString();
errMsgMap.put(propertyName,errMsg);
});
validationResult.setErrMsgMap(errMsgMap);
}
return validationResult;
}@Override
public void afterPropertiesSet() throws Exception {
//使hibernate validator通过工厂初始化进行实例化
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
this.validator = factory.getValidator();
}
}
package com.start.base.common.validator;
import java.util.HashMap;
import java.util.Map;import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
//import org.springframework.util.StringUtils;
public class ValidationResult {
//判断校验是否有错
private boolean hasError = false;//存放错误信息的map
private MaperrMsgMap = new HashMap<>(); //格式化字符串信息获得错误结果的msg方法
public String getErrMsg(){
return JSONObject.toJSONString(errMsgMap);
}public void setHasError(boolean b) {
// TODO Auto-generated method stub
this.hasError=b;
}public Object getErrMsgMap() {
// TODO Auto-generated method stub
return errMsgMap;
}public void setErrMsgMap(Map
errMsgMap) {
this.errMsgMap = errMsgMap;
}
}
@AssertFalse 校验false
@AssertTrue 校验true
@DecimalMax(value=,inclusive=) 小于等于value,
inclusive=true,是小于等于
@DecimalMin(value=,inclusive=) 与上类似
@Max(value=) 小于等于value
@Min(value=) 大于等于value
@NotNull 检查Null
@Past 检查日期
@Pattern(regex=,flag=) 正则
@Size(min=, max=) 字符串,集合,map限制大小
@Valid 对po实体类进行校验
这篇文章介绍的注解更全一点
@Controller
@Validated
public class ValidationController {
@GetMapping("/validate1")
@ResponseBody
public String validate1(
@Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
@Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age,
@Future @RequestParam("birth")@DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth){
return "validate1";
}
}
注意类名需要加注解@Validated
校验失败会抛出ConstraintViolationException异常
然后我们在全局异常捕获类捕获这个异常,返回给前台对应的错误json
给model类增加校验注解
public class User {
@Size(min = 1,max = 10,message = "姓名长度必须为1到10")
private String name;
@NotEmpty
private String firstName;
@Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100")
private Integer age;
@Future
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date birth;
...getter setter
}
在controller对应User实体前增加@Valid注解
@PostMapping("/validate2")
@ResponseBody
public User validate2(@Valid @RequestBody User user){
return user;
}
message支持表达式和EL表达式 ,比如message = "姓名长度限制为{min}到{max} ${1+2}")
想把错误描述统一写到properties的话,在classpath下面新建ValidationMessages_zh_CN.properties文件(注意value需要转换为unicode编码),然后用{}格式的占位符
除了默认提供的校验注解外,我们可以定义自己的校验注解
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { HandsomeBoyValidator.class})
public @interface HandsomeBoy {
String message() default "123";
String name();
Class>[] groups() default { };
Class extends Payload>[] payload() default {};
}
注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = { HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类
一开始写了这个自定义注解和验证类,发现没有生效,最后发现是@Constraint这个注解里的类没有配置,还跟了很多源码,蛋疼,总的来讲,这个配置还是挺方便的
外国人写的一篇博客,介绍自定义验证配置,挺全的
https://www.baeldung.com/spring-mvc-custom-validator
public class HandsomeBoyValidator implements ConstraintValidator {
private String name;
/**
* 用于初始化注解上的值到这个validator
* @param constraintAnnotation
*/
@Override
public void initialize(HandsomeBoy constraintAnnotation) {
name =constraintAnnotation.name();
}
/**
* 具体的校验逻辑
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(User value, ConstraintValidatorContext context) {
return name ==null || name.equals(value.getName());
}
}
这边的功能是user类里面的name字段必须和配置的一样,否则输出一个事实
@PostMapping("/validate3")
@ResponseBody
public User validate3(@Valid @HandsomeBoy(name = "scj",message = "666") @RequestBody User user){
return user;
}
如果验证不通过,会输出盛超杰第二帅,全局异常处理器不要忘记配置
public enum CaseMode {
UPPER,
LOWER;
}
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
String message() default "";Class>[] groups() default {};
Class extends Payload>[] payload() default {};
CaseMode value();
}
public class CheckCaseValidator implements ConstraintValidator{
private CaseMode caseMode;
public void initialize(CheckCase checkCase) {
this.caseMode = checkCase.value();
}public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}if (caseMode == CaseMode.UPPER) {
return s.equals(s.toUpperCase());
} else {
return s.equals(s.toLowerCase());
}
}
}
package com.start.base.common.validator;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckDateValidator.class)
@Documented
public @interface CheckDate {
String message() default "";Class>[] groups() default {};
Class extends Payload>[] payload() default {};
String date() default "";
}
package com.start.base.common.validator;
import java.text.ParseException;
import java.text.SimpleDateFormat;import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;public class CheckDateValidator implements ConstraintValidator
{
private String date;
public void initialize(CheckDate checkDate) {
this.date= checkDate.date();
}public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return false;
}SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
sdf.parse(s);
this.date = sdf.parse(s).toString();
return true;
} catch (ParseException e) {
//e.printStackTrace();
this.date = null;
}
return false;}
}
https://github.com/hsn999/start-cloud/tree/master/base-common
Hibernate validator官方文档
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface