没有任何技术方案会是一种银弹,任何东西都是有利弊的
【小家Java】深刻了解数据校验:Java Bean Validation 2.0(JSR30三、JSR34九、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动做
【小家Java】深刻了解数据校验(Bean Validation):从深处去掌握@Valid的做用(级联校验)以及经常使用约束注解的解释说明前端
<center>对Spring感兴趣可扫码加入wx群:Java高工、架构师3群
(文末有二维码)</center>java
通常来讲,对于web项目咱们都有必要对请求参数进行校验,有的前端使用JavaScript
校验,可是为了安全起见后端的校验都是必须的。所以数据校验不只仅是在web
下,在方方面面都是一个重要的点。前端校验有它的JS校验框架(好比我以前用的jQuery Validation Plugin
),后端天然也少不了。程序员
前面洋洋洒洒已经把数据校验Bean Validation
讲了不少了,若是你已经运用在你的项目中,势必将大大提升生产力吧,本文做为完结篇(不是总结篇)就不用再系统性的介绍Bean Validation
他了,而是旨在介绍你在使用过程当中不得不关心的周边、细节~web
若是说前面是用机
,那么本文就有点玩机
的意思~
BV
(Bean Validation)的使用范围本次再次强调了这一点(设计思想是我认为特别重要的存在):使用范围。Bean Validation
并不局限于应用程序的某一层或者哪一种编程模型, 它能够被用在任何一层, 除了web
程序,也能够是像Swing
这样的富客户端程序中(GUI编程
)。spring
我抄了一副业界著名的图给你们:Bean Validation
的目标是简化Bean
校验,将以往重复的校验逻辑进行抽象和标准化,造成统一API规范;编程
说到抽象统一API,它可不是乱来的,只有当你能最大程度的获得公有,这个动做才有意义,至少它通常都是与业务无关的。 抽象能力是对程序员分级的最重要标准之一
若是子类继承自他的父类,除了校验子类,同时还会校验父类,这就是约束继承(一样适用于接口)。后端
// child和person上标注的约束都会被执行 public class Child extends Person { ... }
注意:若是子类覆盖了父类的方法,那么子类和父类的约束都会被校验。数组
若是要验证属性关联的对象,那么须要在属性上添加@Valid
注解,若是一个对象被校验,那么它的全部的标注了@Valid
的关联对象都会被校验,这些对象也能够是数组、集合、Map等,这时会验证他们持有的全部元素。缓存
Demo
:安全
@Getter @Setter @ToString public class Person { @NotNull private String name; @NotNull @Positive private Integer age; @Valid @NotNull private InnerChild child; @Valid // 让它校验List里面全部的属性 private List<InnerChild> childList; @Getter @Setter @ToString public static class InnerChild { @NotNull private String name; @NotNull @Positive private Integer age; } }
校验程序:
public static void main(String[] args) { Person person = new Person(); person.setName("fsx"); Person.InnerChild child = new Person.InnerChild(); child.setName("fsx-age"); child.setAge(-1); person.setChild(child); // 设置childList person.setChildList(new ArrayList<Person.InnerChild>(){{ Person.InnerChild innerChild = new Person.InnerChild(); innerChild.setName("innerChild1"); innerChild.setAge(-11); add(innerChild); innerChild = new Person.InnerChild(); innerChild.setName("innerChild2"); innerChild.setAge(-12); add(innerChild); }}); Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出错误消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
打印校验失败的消息:
age 不能为null: null childList[0].age 必须是正数: -11 child.age 必须是正数: -1 childList[1].age 必须是正数: -12
失败消息message
自定义每一个约束定义中都包含有一个用于提示验证结果的消息模版message
,而且在声明一个约束条件的时候,你能够经过这个约束注解中的message属性来重写默认的消息模版(这是自定义message
最简单的一种方式)。
若是在校验的时候,这个约束条件没有经过,那么你配置的MessageInterpolator
插值器会被用来当成解析器来解析这个约束中定义的消息模版, 从而获得最终的验证失败提示信息。
默认使用的插值器是org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator
,它借助org.hibernate.validator.spi.resourceloading.ResourceBundleLocator
来获取到国际化资源属性文件从而填充模版内容~
资源解析器默认使用的实现是PlatformResourceBundleLocator
,在配置Configuration
初始化的时候默认被赋值:
private ConfigurationImpl() { this.validationBootstrapParameters = new ValidationBootstrapParameters(); // 默认的国际化资源文件加载器USER_VALIDATION_MESSAGES值为:ValidationMessages // 这个值就是资源文件的文件名~~~~ this.defaultResourceBundleLocator = new PlatformResourceBundleLocator( ResourceBundleMessageInterpolator.USER_VALIDATION_MESSAGES ); this.defaultTraversableResolver = TraversableResolvers.getDefault(); this.defaultConstraintValidatorFactory = new ConstraintValidatorFactoryImpl(); this.defaultParameterNameProvider = new DefaultParameterNameProvider(); this.defaultClockProvider = DefaultClockProvider.INSTANCE; }
这个解析器会尝试解析模版中的占位符( 大括号括起来的字符串,形如这样{xxx}
)。
它解析message
的核心代码以下(好比此处message模版是{javax.validation.constraints.NotNull.message}
为例):
public abstract class AbstractMessageInterpolator implements MessageInterpolator { ... private String interpolateMessage(String message, Context context, Locale locale) throws MessageDescriptorFormatException { // 若是message消息木有占位符,那就直接返回 再也不处理了~ // 这里自定义的优先级是最高的~~~ if ( message.indexOf( '{' ) < 0 ) { return replaceEscapedLiterals( message ); } // 调用resolveMessage方法处理message中的占位符和el表达式 if ( cachingEnabled ) { resolvedMessage = resolvedMessages.computeIfAbsent( new LocalizedMessage( message, locale ), lm -> resolveMessage( message, locale ) ); } else { resolvedMessage = resolveMessage( message, locale ); } ... } private String resolveMessage(String message, Locale locale) { String resolvedMessage = message; // 获取资源ResourceBundle三部曲 ResourceBundle userResourceBundle = userResourceBundleLocator.getResourceBundle( locale ); ResourceBundle constraintContributorResourceBundle = contributorResourceBundleLocator.getResourceBundle( locale ); ResourceBundle defaultResourceBundle = defaultResourceBundleLocator.getResourceBundle( locale ); ... } }
对如上message
的处理步骤大体总结以下:
{
须要处理,直接返回(好比咱们自定义message属性值全是文字,就直接返回了)~resolveMessage()
方法从资源文件里拿内容来处理~拿取资源文件,按照以下三个步骤寻找:
1. `userResourceBundleLocator`:去用户本身的`classpath`里面去找资源文件(默认名字是`ValidationMessages.properties`,固然你也可使用国际化名) 2. `contributorResourceBundleLocator`:加载贡献的资源包 3. `defaultResourceBundle`:默认的策略。去这里`于/org/hibernate/validator`加载`ValidationMessages.properties`
须要注意的是,如上是加载资源的顺序。不管怎么样,这三处的资源文件都会加载进内存的(并没有短路逻辑)。进行占位符匹配的时候,依旧遵照这规律:
1. 最早用本身当前项目`classpath`下的资源去匹配资源占位符,若没匹配上再用下一级别的资源~~~ 2. 规律同上,依次类推,递归的匹配全部的占位符(若占位符没匹配上,原样输出,并非输出`null`哦~)
须要注意的是,由于{
在此处是特殊字符,若你就想输出{
,请转义:\{
了解了这些以后,想自定义失败消息message
,就简直不要太简单了好很差,例子以下:
@Min(value = 10, message = "{com.fsx.my.min.message}") private Integer age;
写一个资源属性文件,命名为ValidationMessages.properties
放在类路径下,文件内容以下:
// 此处可使用占位符{value}读取注解对应属性上的值 com.fsx.my.min.message=[自定义消息]最小值必须是{value}
运行测试用例,打印输出以下失败消息:
age [自定义消息]最小值必须是10: -1
完美(自定义的生效了)
说明:由于个人平台是中文的,所以文件命名为ValidationMessages_zh_CN.properties
的效果也是同样的,由于Hibernate Validation
提供了Locale
国际化的支持
上面使用的是Hibernate Validation
内置的对国际化的支持,因为大部分状况下咱们都是在Spring
环境下使用数据校验,所以有必要讲讲Spring加持状况下的国家化作法。咱们知道Spring MVC
是有专门作国际化的模块的,所以国际化这个动做固然也是能够交给Spring
本身来作的,此处我也给一个Demo
吧:
说明:即便在Spring环境下,你照常使用
Hibernate Validation
的国际化方案,依旧是没有问题的~
一、向容器内配置验证器(含有本身的国际化资源文件):
@Configuration public class RootConfig { @Bean public LocalValidatorFactoryBean localValidatorFactoryBean() { LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); // 使用Spring加载国际化资源文件 //ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); //messageSource.setBasename("MyValidationMsg"); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("MyValidationMsg"); // 注意此处名字就随意啦,毕竟交给spring了`.properties`就不须要了哦 messageSource.setCacheSeconds(120); // 缓存时长 // messageSource.setFileEncodings(); // 设置编码 UTF-8 localValidatorFactoryBean.setValidationMessageSource(messageSource); return localValidatorFactoryBean; } }
运行单测:
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private LocalValidatorFactoryBean localValidatorFactoryBean; @Test public void test1() { Person person = new Person(); person.setAge(-5); Validator validator = localValidatorFactoryBean.getValidator(); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出错误消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); } }
打印校验失败消息以下(完美生效):
age [自定义消息]最小值必须是10: -5
说明:如果Spring
应用,若是你还须要考虑国际化的话,我我的建议使用Spring
来处理国际化,而不是Hibernate
~(有种Spring
的脑残粉感受有木有,固然这不是强制的)
Spring MVC
默认配置的(使用的)校验器的执行代码以下:
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {\ ... @Bean public Validator mvcValidator() { Validator validator = getValidator(); if (validator == null) { if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) { Class<?> clazz; try { String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean"; clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader()); } catch (ClassNotFoundException | LinkageError ex) { throw new BeanInitializationException("Failed to resolve default validator class", ex); } validator = (Validator) BeanUtils.instantiateClass(clazz); } else { validator = new NoOpValidator(); } } return validator; } ... }
代码很简答,就不逐行解释了。我概括以下:
Spring MVC
中校验要想自动
生效,必须导入了javax.validation.Validator
才行,不然是new NoOpValidator()
它木有校验行为Spring MVC
最终默认使用的校验器是OptionalValidatorFactoryBean
(LocalValidatorFactoryBean
的子类)~@EnableWebMvc
也是必须的(SpringBoot
环境另说)那如何自定义一个全局的校验器呢?最佳作法以下:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { ... @Override public Validator getValidator() { // return "global" validator return new LocalValidatorFactoryBean(); } ... }
固然,你还可使用@InitBinder
来设置,甚至能够细粒度设置到只与当前Controller
绑定的校验器都是可行的(好比你可使用自定校验器实现各类私有的、比较复杂的逻辑判断)
JSR
和Hibernate
支持的约束条件已经足够强大,应该是能知足咱们绝大部分状况下的基础验证的。若是仍是不能知足业务需求,咱们还能够自定义约束,也很简单一事。
JSR
和Hibernate
提供的约束注解解释说明: 【小家Java】深刻了解数据校验(Bean Validation):从深处去掌握@Valid的做用(级联校验)以及经常使用约束注解的解释说明
自定义一个约束分以下三步(说是2步也成):
ConstraintValidator
)给个Demo
:此处以自定义一个约束注解来校验集合的长度范围:@CollectionRange
一、自定义注解(此处使用得比较高级)
@Documented @Constraint(validatedBy = {}) @SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Repeatable(value = CollectionRange.List.class) @Size // 校验动做委托给Size去完成 因此它本身并不须要校验器~~~ @ReportAsSingleViolation // 组合组件通常建议标注上 public @interface CollectionRange { // 三个必备的基本属性 String message() default "{com.fsx.my.collection.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // 自定义属性 @OverridesAttribute这里有点方法覆盖的意思~~~~~~ 子类属性覆盖父类的默认值嘛 @OverridesAttribute(constraint = Size.class, name = "min") int min() default 0; @OverridesAttribute(constraint = Size.class, name = "max") int max() default Integer.MAX_VALUE; // 重复注解 @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented public @interface List { CollectionRange[] value(); } }
二、实现一个校验器
此例用不着(下面会有)
三、自定义错误消息
固然,你能够写死在message属性上,可是本处使用配置的方式来~
com.fsx.my.collection.message=[自定义消息]你的集合的长度必须介于{min}和{max}之间(包含边界值)
运行案例:
@Getter @Setter @ToString public class Person { @CollectionRange(min = 5, max = 10) private List<Integer> numbers; } // 测试用例 public static void main(String[] args) { Person person = new Person(); person.setNumbers(Arrays.asList(1, 2, 3)); Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出错误消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
输出校验信息以下(校验成功):
numbers [自定义消息]你的集合的长度必须介于5和10之间(包含边界值): [1, 2, 3]
这块比较简单,不少状况下一个字段是须要有多个约束(不为空且大于0)的。这个时候咱们有两种作法:
咱们知道约束的失败消息message
里是可使用{}
占位符来动态取值的,默认状况下可以取到约束注解里的全部属性值,而且也只能取到那些属性的值。
but,有的时候为了友好展现,咱们须要自定义message
里可取的值怎么办呢?下面给个例子,让你们知道怎么自定义可以使用占位符的参数(备注:须要基于自定义注解):
自定义一个性别约束注解:
@Documented @Retention(RUNTIME) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Constraint(validatedBy = {GenderConstraintValidator.class}) public @interface Gender { // 三个必备的基本属性 String message() default "{com.fsx.my.gender.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int gender() default 0; //0:男生 1:女生 }
配置的消息资源是:
com.fsx.my.gender.message=[自定义消息]此处只能容许性别为[{zhGenderValue}]的
很显然,此处咱们须要读取zhGenderValue
这个自定义的属性值,而且但愿它是中文。因此看看下面我实现的这个校验器吧:
public class GenderConstraintValidator implements ConstraintValidator<Gender, Integer> { int genderValue; @Override public void initialize(Gender constraintAnnotation) { genderValue = constraintAnnotation.gender(); } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { //添加参数 校验失败的时候可用 HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class); hibernateContext.addMessageParameter("zhGenderValue", genderValue == 0 ? "男" : "女"); // 友好展现 //hibernateContext.buildConstraintViolationWithTemplate("{zhGenderValue}").addConstraintViolation(); if (value == null) { return false; // null is not valid } return value == genderValue; } }
运行单测:
@Getter @Setter @ToString public class Person { @Gender(gender = 0) private Integer personGender; } public static void main(String[] args) { Person person = new Person(); person.setPersonGender(1); Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出错误消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
打印以下:
personGender [自定义消息]此处只能容许性别为[男]的: 1
完美(效果达到)
若是说前面文章是用机,那这篇能够称做是玩机了。Bean Validation
是java官方定义的bean验证标准,如今最新的版本为2.x,hibernate validator
做为其标准实现,对其进行了扩展,增长了多种约束,若是仍然不能知足业务需求,咱们还能够自定义约束。
数据校验Bean Validation
这一大块的内容到此就告一段落了,但愿讲解的全部内容能给你实际工做中带来帮助,祝好~
若文章格式混乱,可点击
:
原文连接-原文连接-原文连接-原文连接-原文连接
==The last:若是以为本文对你有帮助,不妨点个赞呗。固然分享到你的朋友圈让更多小伙伴看到也是被做者本人许可的~
==
**若对技术内容感兴趣能够加入wx群交流:Java高工、架构师3群
。
若群二维码失效,请加wx号:fsx641385712
(或者扫描下方wx二维码)。而且备注:"java入群"
字样,会手动邀请入群**