Spring @Valid 注解 校验实体属性

  • 1 @Valid 介绍
    • 1.1 前言
    • 1.2 Bean Validation
    • 1.3 关于validation包
    • 1.3 关于Spring Hibernate Validator支持的约束集信息(constraints)
  • 2 简单的使用
    • 2.1配置项
      • 2.1.1 maven -- jar包引入
      • 2.1.2 SpringMVC配置
    • 2.2 实现方式
      • 2.2.1 @Valid + BindingResult + javax.validation.constraints.*
      • 2.2.2 validator.validate()
      • 2.2.3 代码示例
      • 2.2.4 对比不同实现的一些总结。
  • 3 补充 @Validated 注解 + javax.validation.constraints.*

1 @Valid 介绍

1.1 前言

在我们日常开发中,数据校验的实现是一件比较痛苦的事情,繁琐且无趣,对于一般的业务而言,极少出现一些过于复杂的校验,常常都是非空,长度,最大最小值,正则,数据关联,定值等等。我在开发过程中为了减少我写这些代码的coding成本,由此认识了javax.validation包下的 @Valid 注解 和 Java 的 Bean Validation 规范。

1.2 Bean Validation

首先了解下JSR(Java Specification Requests)

JSR:Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

Java 在2009年的 JAVAEE 6 中发布了 JSR303以及javax下的validation包内容。

这项工作的主要目标是为java应用程序开发人员提供 基于java对象的 约束(constraints)声明 和 对约束的验证工具(validator),以及约束元数据存储库和查询API。
但是该内容并没有具体的实现, Hibernate-Validator框架 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

注意:此处的Hibernate 不是 Hibernate ORM

后续关于Bean Validation 的规范提案 和 各个版本的实现 陆续得以迭代,各个版本的信息如下:

Bean Validation Version release year JSR Version Hibernate impl version apache BVal impl version
Bean Validation 1.0 2009年 JSR 303 Hibernate-Validator-4.3.1.Final org.apache.bval 0.5
Bean Validation 1.1 2013年 JSR 349 Hibernate-Validator-5.1.1.Final org.apache.bval 1.1.2
Bean Validation 2.0 2017年 JSR 380 Hibernate-Validator-6.0.1.Final org.apache.bval 2.0.3(最新版本)

本文主要基于 Hibernate-Validator 框架的实现为主。

1.3 关于validation包

Java-Api文档
其中 javax.validation.constraints包下是 bean validation 支持的约束,以注解的形式使用。

1.3 关于Spring Hibernate Validator支持的约束集信息(constraints)

截取自 IBM JSR 303

表 1. Bean Validation 中内置的 constraint

表 2. Hibernate Validator 附加的 constraint

2 简单的使用

2.1配置项

2.1.1 maven – jar包引入

如果使用springMVC ,需要在 pom文件中引入对应的jar包。

<!--        hibernate.validator jar包--><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.0.17.Final</version></dependency>

这里不需要引入 javax.validation包,hibernate-validator框架中已经有引入了。

如果使用springboot,笔者使用的 2.1.7.RELEASE 版本,其中 spring-boot-starter-web 包中已经引入了 hibernate-validator 6.0.17.RELEASE了,不需要我们手动添加。

如果使用的非maven项目,需要下载对应的jar引入到项目中。

2.1.2 SpringMVC配置
<!--配置中需要注意的地方:
LocalValidatorFactoryBean :
validationMessageSource通过set方法注入,然后转换为MessageInterpolator对象,如果不配置validationMessageSource信息源,那么会使用默认的信息源HibernateValidations_xx_xx.properties,位于org.hibernate.validator包下,具体使用哪个文件由Locale.getDefaultLocale()决定。-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"><property name="providerClass" value="org.hibernate.validator.HibernateValidator" /><property name="validationMessageSource" ref="hibernate_validator_messages"/>
</bean>

2.2 实现方式

这部分介绍关心实现的小伙伴也可以略过,直接看实现。

2.2.1 @Valid + BindingResult + javax.validation.constraints.*

BindingResult 是 spring 对于 Hibernate-Validator的进一步封装,主要处理 约束违反信息 的处理。也就是当 判断 校验不通过时 所获取的 默认的或者自定义的 错误信息。
使用 @Valid 结合 BindingResult实例,Spring将对被注解的实体进行约束校验,原理是AOP。

2.2.2 validator.validate()

springboot 自动将 ValidatorFactory 加载到了IOC容器中,不需要进行配置(注意:SpringMVC 需要进行配置)。我们可以使用自动注入的方式获取 ValidatorFactory接口的实现的实例,进而获取 Validator接口的实现的实例,调用.validate(T var1, Class<?>… var2)方法对 java bean 进行约束检验。基于此,还可以用作AOP切面的开发。
validator接口源码

package javax.validation;import java.util.Set;
import javax.validation.executable.ExecutableValidator;
import javax.validation.metadata.BeanDescriptor;public interface Validator {<T> Set<ConstraintViolation<T>> validate(T var1, Class<?>... var2);<T> Set<ConstraintViolation<T>> validateProperty(T var1, String var2, Class<?>... var3);<T> Set<ConstraintViolation<T>> validateValue(Class<T> var1, String var2, Object var3, Class<?>... var4);BeanDescriptor getConstraintsForClass(Class<?> var1);<T> T unwrap(Class<T> var1);ExecutableValidator forExecutables();
}

ValidatorFactory 工厂接口源码

package javax.validation;public interface ValidatorFactory extends AutoCloseable {Validator getValidator();ValidatorContext usingContext();MessageInterpolator getMessageInterpolator();TraversableResolver getTraversableResolver();ConstraintValidatorFactory getConstraintValidatorFactory();ParameterNameProvider getParameterNameProvider();ClockProvider getClockProvider();<T> T unwrap(Class<T> var1);void close();
}
2.2.3 代码示例

下面我们用一个简单的示例来完成上诉提到的两种实现。
首先我们创建一个 简单的 po 类,也就是我们的业务实体类。
假设一个后台管理系统的职员信息管理模块,需要新增员工信息。
新建一个Employee类 用于 封装 职员的基础信息

package com.onesport.kern.beanValidationTest.dao;
//看包名就知道这个是hibernate附加的constraints
import org.hibernate.validator.constraints.Length;
//javax.validation.constraints包下所支持的一些约束类型
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import java.util.Date;/*** @author Kern* @Title: Employee* @ProjectName kern-demo* @Description: TODO* @date 2019/9/1718:53*/
public class Employee {@NotNull(message = "姓名必填!")@Length(max = 20, message = "姓名过长!")private String name;@NotNull(message = "工牌必填!")@Pattern(regexp = "^[0-9]\\d{10}",message = "请输入10位数字工牌!")//长度10,0-9private String badgeCode;@Pattern(regexp = "^[1-2]",message = "性别参数错误!")@NotNull(message = "性别必填!")private String gender;@Past(message = "无效的出生日期!")private Date birthDate;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getBadgeCode() {return badgeCode;}public void setBadgeCode(String badgeCode) {this.badgeCode = badgeCode;}public String getGender() {return gender;}public void setGender(String gender) {this.gender = gender;}public Date getBirthDate() {return birthDate;}public void setBirthDate(Date birthDate) {this.birthDate = birthDate;}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", badgeCode='" + badgeCode + '\'' +", gender=" + gender +", birthDate=" + birthDate +'}';}
}

无论是@Valid注解 + BindingResult 这种形式 或者是 直接调用Validator.validate() 方法,我们都可以直接使用这个 po 对象。
接下来我们简便点来,直接写出来两种实现方式的controller层方法。这里忽略了service层,如有其他的业务可以在service层写。代码如下:

package com.onesport.kern.beanValidationTest.control;import com.onesport.kern.beanValidationTest.dao.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.validation.*;
import java.util.List;
import java.util.Set;/*** @author Kern* @Title: EmployeeControl* @ProjectName kern-demo* @Description: TODO* @date 2019/9/1722:26*/
@RequestMapping(path = "/employee")
@RestController
public class EmployeeControl {//换行符private static String lineSeparator = System.lineSeparator();/*** 注解实现* 使用@Valid 注解 实体, 并传入参数bindResult以获取校验结果信息* @param employee* @param bindingResult* @return*/@PostMapping("/bindingResult")public Object addEmployee(@RequestBody @Valid Employee employee, BindingResult bindingResult){if (bindingResult.hasErrors()){//校验结果以集合的形式返回,当然也可以获取单个。具体可以查看bindResult的API文档List<FieldError> fieldErrorList = bindingResult.getFieldErrors();//StringBuilder组装异常信息StringBuilder builder = new StringBuilder();//遍历拼装fieldErrorList.forEach(error -> {builder.append(error.getDefaultMessage() + lineSeparator);});builder.insert(0,"use @Valid n BingdingResult :" +lineSeparator);return builder.toString();}//TODO there can invoke service layer method to do sometingreturn "添加职员信息成功:" + employee.toString();}//Spring boot 已帮我们把 validation 的关键对象的实例装载如 IOC 容器中@Autowiredprivate ValidatorFactory autowiredValidatorFactory;@Autowiredprivate Validator autowiredValidator;/*** 调用validator实现* @param employee* @return*/@PostMapping("/validator")public Object addEmployee(@RequestBody Employee employee){System.out.println("这里将导入 由 Springboot 的 IOC 容器中获取的 校验器工厂和 校验器类");System.out.println("validator工厂类:"+ autowiredValidatorFactory.toString());System.out.println("validator类:"+ autowiredValidator.toString());/*** 下述的工厂类和校验器类也可以使用上述由IOC容器中获取的对象实例代替*///实例化一个 validator工厂ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();//获取validator实例Validator validator = validatorFactory.getValidator();//调用调用,得到校验结果信息 SetSet<ConstraintViolation<Employee>> constraintViolationSet = validator.validate(employee);//StringBuilder组装异常信息StringBuilder builder = new StringBuilder();//遍历拼装constraintViolationSet.forEach(violationInfo -> {builder.append(violationInfo.getMessage() + lineSeparator);});if (builder.toString().length() > 0){builder.insert(0,"use validator :" +lineSeparator);return builder.toString();}//TODO there can invoke service layer method to do sometingreturn "添加职员信息成功:" + employee.toString();}
}

上下两个方法,分别对应两种实现。我们测试一下结果。这里直接使用yapi接口管理工具来测试了。
首先调用一下第一个实现 @Valid + BingdingResult
参数:

结果:

再调用一下第二个实现 直接调用 validator
参数: 这次我们只把姓名和工牌写错

结果:
控制台console:

返回信息:

2.2.4 对比不同实现的一些总结。

我们回过头来看一下上述两种实现,第一种实现 BindingResult是 Spring 提供的再封装工具,底层是调用 spring aop ,在我们业务开发中实现也相对简单,method body 也可以很方便的进行封装成一个通用的工具方法供调用,以免重复写这些代码。但是总是要带这个bindResult 参数还是比较烦。
而第二种实现方法就相对较为灵活,同样也是可编程的,在对一些Excel批量导入的数据校验等等的场景会较为灵活。

3 补充 @Validated 注解 + javax.validation.constraints.*

这部分一直没有补充,趁五一前补充一下,源码看了个大概,没有细研究,但大体上应该没错。
自spring 3.1 版本起提供了 @Validated 注解用于方法参数的表单校验, 同样也是基于 JSR-303 标准,是spring mvc对表单校验支撑框架的进一步封装。

前景提要,需要依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><version>你的springboot版本</version></dependency>

下面的介绍内容建议结合源码一起看。

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {/**指定验证组, 可以看下 javax.validation.groups.Default 接口, 就是校验的时候注解指定组,然后两两对应,支持继承关系*/Class<?>[] value() default {};
}

这个注解可以注释到类,方法,参数上,意思也很简单,如果注解到类上,则类的所有方法的所有参数都进行表单校验,以此类推方法级别和参数级别的。

该注解结合基于BeanPostProcessor的 MethodValidationPostProcessor , 在bean构建后搜索相关的注解和 validation注解,如果匹配到参数需要进行校验,插入切面和配置的校验器在请求时拦截并校验。

@Configuration
@ConditionalOnClass({ExecutableValidator.class})
@ConditionalOnResource(resources = {"classpath:META-INF/services/javax.validation.spi.ValidationProvider"}
)
@Import({PrimaryDefaultValidatorPostProcessor.class})
public class ValidationAutoConfiguration {public ValidationAutoConfiguration() {}@Bean@Role(2)@ConditionalOnMissingBean({Validator.class})public static LocalValidatorFactoryBean defaultValidator() {LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();factoryBean.setMessageInterpolator(interpolatorFactory.getObject());return factoryBean;}@Bean@ConditionalOnMissingBeanpublic static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator) {MethodValidationPostProcessor processor = new MethodValidationPostProcessor();boolean proxyTargetClass = (Boolean)environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);processor.setProxyTargetClass(proxyTargetClass);processor.setValidator(validator);return processor;}
}

关于校验器
如果你不对校验器进行配置,springboot自动注入的是LocalValidatorFactoryBean, 即一个支持国际化语言翻译的校验器工厂类,题外话(简单说你可以使用例如 i18n 等国际化技术来 翻译 你 校验注解上的 消息 , 例如 @NotNull(message="Validation.UserId.NotNull") ,然后根据语言环境转换这个message。)

上面

关于切面拦截器
我们做好配置和编码后程序织入的切面如何处理呢?一段代码可以很简单的说明


// ==================================== part1  自动注入 ========================================public class ValidationAutoConfiguration {//自动注入注入了 MethodValidationPostProcessor  注意参入了一个懒加载的校验器,这个是可以配置的@Beanpublic static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator) {}
}
// ==================================== part2  Bean初始化 ========================================
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {@Nullableprivate Validator validator;/*** Validator 也是可以配置的*/public void setValidator(Validator validator) {// 忽略实现}/*** 工厂同理*/public void setValidatorFactory(ValidatorFactory validatorFactory) {// 忽略实现}/**这个类是 InitializingBean 的实现,初始化完成后会构造一个默认的切面advice, 最终构造的默认就是 MethodValidationInterceptor**/@Overridepublic void afterPropertiesSet() {Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));}protected Advice createMethodValidationAdvice(@Nullable Validator validator) {return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());}
}// ==================================== part3  拦截器实现 ========================================public class MethodValidationInterceptor implements MethodInterceptor {// 直接看invoke方法,去掉了多余的代码,直接看流程@Override@SuppressWarnings("unchecked")public Object invoke(MethodInvocation invocation) throws Throwable {Set<ConstraintViolation<Object>> result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);Object returnValue = invocation.proceed();result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);if (!result.isEmpty()) {/***  如果 ConstraintViolation 集合不为空,就抛出一个 ConstraintViolationException 异常*/throw new ConstraintViolationException(result);}return returnValue;}

结果就很清晰了,使用 @Valid + BindingResult 的方式,我们在Controller中判断BindingResult ,然后自己处理异常,这样也挺好,但是代码整体不够优美,当然我们可以自己实现切面。

而 @Validated 是Spring 默认实现的一个切面,他提供了类级别,方法级别,参数级别的表单校验识别, 只要你使用了注解, 方法就会被织入切面。 最终的结果是当表单校验发生错误时,抛出一个 ConstraintViolationException。 一提到异常处理,我们接下来就可以使用全局异常捕获这种方式来进行处理了。如下

@RestControllerAdvice
@Component
public class GlobalExceptionHandler {// 当直接校验具体参数时会出现javax.validation.ConstraintViolationException@ExceptionHandler(value = ConstraintViolationException.class)public HttpResult<?> handleException(ConstraintViolationExceptione e) {log.error("表单校验异常信息 msg={}, requestId={}", e.getMessage(), RoutedInnerRequestHelper.getRequestId(), e);// 拼接参数String errorMsg = ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(", "));return HttpResult.fail(ResultCodeEnum.EXCEPTION.getCode(), errorMsg);}}

做好上面的配置后, 我们要做的事情和 使用 @Valid + BindingResult 的事情差不多


// 请求参数加 javax.validation.constraints包下的注解注解
@Data
public class Employee {@NotNull(message = "姓名必填!")@Length(max = 20, message = "姓名过长!")private String name;@NotNull(message = "工牌必填!")@Pattern(regexp = "^[0-9]\\d{10}",message = "请输入10位数字工牌!")//长度10,0-9private String badgeCode;@Pattern(regexp = "^[1-2]",message = "性别参数错误!")@NotNull(message = "性别必填!")private String gender;@Past(message = "无效的出生日期!")private Date birthDate;
}// controller 加注解    这里加了好几个注解方便说明,一般按需加注就可以了
@RequestMapping(path = "/employee")
@RestController
@Validated //注解到类上, 则所有方法都会扫描 javax.validation.constraints包下的注解
public class EmployeeControl {//换行符private static String lineSeparator = System.lineSeparator();@PostMapping("/bindingResult")@Validated //注解到方法上, 就这个方法的所有参数都进行校验扫描public Object addEmployee(@RequestBody @Validated Employee employee){  //注解到参数, 只有这个参数校验 //TODO there can invoke service layer method to do sometingreturn "添加职员信息成功:" + employee.toString();}
}

这样就完成了表单校验部分的编码。 是不是比 @Valid 用起来更优雅。

最后一个, 如果有小伙伴需要了解 @Validted + 国际化语言的实现方案的,可以留言, 我再出一篇博文

@Valid 注解详解 Java Bean Validation的前世今生相关推荐

  1. @Valid注解详解

    @Valid 用于验证注解是否符合要求,直接加在变量user之前,在变量中添加验证信息的要求,当不符合要求时就会在方法中返回message 的错误提示信息. @RestController@Reque ...

  2. Java Bean Validation 详解

    前言 最近维护一个老项目,项目使用最原始的Servlet,项目中充斥着各种类似判空的简单校验,为了减少重复代码,因此需要手动引入 Java 的 Bean Validation. Java Bean V ...

  3. Hibernate Validation校验注解详解

    在前后端传递数据的时候,往往后端需要校验传递数据的格式,比如用户名的格式,密码是否为空.我们可以在service层编写代码判断,但是当我们在多处需要校验传递来的数据的时候,就会出现大量重复的代码,一旦 ...

  4. java注解 源码_详解Java注解教程及自定义注解

    详解Java注解教程及自定义注解 更新时间:2016-02-26 11:47:06   作者:佚名   我要评论(0) Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个 ...

  5. java 自动装载_java_详解Java的Spring框架下bean的自动装载方式,Spring容器可以自动装配相互协 - phpStudy...

    详解Java的Spring框架下bean的自动装载方式 Spring容器可以自动装配相互协作bean之间的关系,这有助于减少对XML配置,而无需编写一个大的基于Spring应用程序的较多的和元素. 自 ...

  6. java的注解方式_详解Java注解的实现与使用方法

    详解Java注解的实现与使用方法 Java注解是java5版本发布的,其作用就是节省配置文件,增强代码可读性.在如今各种框架及开发中非常常见,特此说明一下. 如何创建一个注解 每一个自定义的注解都由四 ...

  7. java method 注解_JAVA 注解详解及简单实例

    JAVA 注解详解及简单实例 何为注解 注解(Annotation)又称为元数据,在JDK1.5后引入,它的作用是: 生成文档  这是注解的原始用途,可以通过注解生成JavaDoc文档 跟踪代码的依赖 ...

  8. Java基础13:反射与注解详解

    Java基础13:反射与注解详解 什么是反射? 反射(Reflection)是Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性. Orac ...

  9. SpringMVC学习:控制层(Controller)基于注解详解

    文章目录 一.URL映射Controller的方法返回值 二.SpringMVC各类注解详解 (一) @Controller (二) @RequestMapping 1.基本用法 2. path属性或 ...

最新文章

  1. BZOJ 2431: [HAOI2009]逆序对数列【DP】
  2. 构建区块链数字货币交易平台服务器的选择
  3. python保存快捷键_超详细的Sublime Text配置python教程
  4. 18000字的SQL优化大全,收藏直接起飞!
  5. 如何把Kubernetes config view里的base64编码过后的secret信息还原
  6. Asp.Net Core 单元测试正确姿势
  7. 两条边延长角会有什么变化_田园易经:什么样的风水环境会影响人的健康?
  8. Linux的cron任务的自启动判断、及启动、关闭 - Xshell命令篇
  9. java ftp 断点,java实现ftp断点续传
  10. html5 drawimage参数,HTML canvas
  11. pla算法 matlab,PAL算法原理及代码实现
  12. leetcode------Binary Tree Level Order Traversal II
  13. 未找到入口 app.json 文件,或者文件读取失败,请检查后重新编译。
  14. Code snippet of the day : Get Chinese Spell in C#
  15. SCHNOKA施努卡:扁线电机定子生产过程中线圈插入
  16. libusb 串口 android,rk3399pro通过修改内核编译支持luat air720上网及串口通讯
  17. XDT——基于Python的XSS自动化检测工具
  18. 「小组联考」第二周三次考试
  19. 喝了38年的雪碧变样了!中国首发上市,白标原味、黑标无糖,加入两款新品 | 美通社头条...
  20. [转]修复windows的启动项:MBRFix工具

热门文章

  1. Linux下Socket编程之TCP应用
  2. 如何通俗易懂地解释卷积?
  3. java上传文件需要的依赖_java – 执行文件上载休息Web服务时缺少方法依赖性
  4. 有关lua,luci的介绍
  5. 企业微信朋友圈和个人微信朋友圈有哪些区别?
  6. PS For Mac 内含破解文件下载地址
  7. 如何帮女朋友快速抢到各种票!火车票,演唱会票等!
  8. GUI界面的视频帧提取
  9. python--socket(套接字/插口)
  10. TortoiseGit commands(命令)(完整中文翻译)