每篇一句

没有任何技术方案会是一种银弹,任何东西都是有利弊的

前言

一般来说,对于web项目我们都有必要对请求参数进行校验,有的前端使用JavaScript校验,但是为了安全起见后端的校验都是必须的。因此数据校验不仅仅是在web下,在方方面面都是一个重要的点。前端校验有它的JS校验框架(比如我之前用的jQuery Validation Plugin),后端自然也少不了。

前面洋洋洒洒已经把数据校验Bean Validation讲了很多了,如果你已经运用在你的项目中,势必将大大提高生产力吧,本文作为完结篇(不是总结篇)就不用再系统性的介绍Bean Validation他了,而是旨在介绍你在使用过程中不得不关心的周边、细节~

如果说前面是用机,那么本文就有点玩机的意思~

BV(Bean Validation)的使用范围

本次再次强调了这一点(设计思想是我认为特别重要的存在):使用范围。
Bean Validation并不局限于应用程序的某一层或者哪种编程模型, 它可以被用在任何一层, 除了web程序,也可以是像Swing这样的富客户端程序中(GUI编程)。

我抄了一副业界著名的图给大家:

Bean Validation的目标是简化Bean校验,将以往重复的校验逻辑进行抽象和标准化,形成统一API规范;

说到抽象统一API,它可不是乱来的,只有当你能最大程度的得到公有,这个动作才有意义,至少它一般都是与业务无关的。抽象能力是对程序员分级的最重要标准之一

约束继承

如果子类继承自他的父类,除了校验子类,同时还会校验父类,这就是约束继承(同样适用于接口)。

// child和person上标注的约束都会被执行
public class Child extends Person {...
}

注意:如果子类覆盖了父类的方法,那么子类和父类的约束都会被校验

约束级联(级联校验)

如果要验证属性关联的对象,那么需要在属性上添加@Valid注解,如果一个对象被校验,那么它的所有的标注了@Valid的关联对象都会被校验,这些对象也可以是数组、集合、Map等,这时会验证他们持有的所有元素

Demo

@Getter
@Setter
@ToString
public class Person {@NotNullprivate String name;@NotNull@Positiveprivate Integer age;@Valid@NotNullprivate InnerChild child;@Valid // 让它校验List里面所有的属性private List<InnerChild> childList;@Getter@Setter@ToStringpublic static class InnerChild {@NotNullprivate String name;@NotNull@Positiveprivate 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);// 设置childListperson.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的处理步骤大致总结如下:

  1. 若没占位符符号{需要处理,直接返回(比如我们自定义message属性值全是文字,就直接返回了)~
  2. 占位符或者EL,交给resolveMessage()方法从资源文件里拿内容来处理~
  3. 拿取资源文件,按照如下三个步骤寻找:
    1. userResourceBundleLocator:去用户自己的classpath里面去找资源文件(默认名字是ValidationMessages.properties,当然你也可以使用国际化名)
    2. contributorResourceBundleLocator:加载贡献的资源包
    3. defaultResourceBundle:默认的策略。去这里于/org/hibernate/validator加载ValidationMessages.properties
  4. 需要注意的是,如上是加载资源的顺序。无论怎么样,这三处的资源文件都会加载进内存的(并无短路逻辑)。进行占位符匹配的时候,依旧遵守这规律
    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国际化的支持


Spring环境下自定义国际化消息

上面使用的是Hibernate Validation内置的对国际化的支持,由于大部分情况下我们都是在Spring环境下使用数据校验,因此有必要讲讲Spring加持情况下的国家化做法。我们知道Spring MVC是有专门做国际化的模块的,因此国际化这个动作当然也是可以交给Spring自己来做的,此处我也给一个Demo吧:

说明:即使在Spring环境下,你照常使用Hibernate Validation的国际化方案,依旧是没有问题的~

1、向容器内配置验证器(含有自己的国际化资源文件):

@Configuration
public class RootConfig {@Beanpublic 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-8localValidatorFactoryBean.setValidationMessageSource(messageSource);return localValidatorFactoryBean;}
}

运行单测:

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class})
public class TestSpringBean {@Autowiredprivate LocalValidatorFactoryBean localValidatorFactoryBean;@Testpublic 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中如何自定义全局校验器Validator

Spring MVC默认配置的(使用的)校验器的执行代码如下:

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {\...@Beanpublic 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;}...
}

代码很简答,就不逐行解释了。我归纳如下:

  1. Spring MVC中校验要想自动生效,必须导入了javax.validation.Validator才行,否则是new NoOpValidator()它木有校验行为
  2. Spring MVC最终默认使用的校验器是OptionalValidatorFactoryBeanLocalValidatorFactoryBean的子类)~
  3. 显然,要想校验生效@EnableWebMvc也是必须的(SpringBoot环境另说)

那如何自定义一个全局的校验器呢?最佳做法如下:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {...@Overridepublic Validator getValidator() {// return "global" validatorreturn new LocalValidatorFactoryBean();}...
}

当然,你还可以使用@InitBinder来设置,甚至可以细粒度设置到只与当前Controller绑定的校验器都是可行的(比如你可以使用自定校验器实现各种私有的、比较复杂的逻辑判断)

说到这自定义Validator了,此处再说一下自定义MessageCodesResolver消息状态码解析器吧。
MessageCodesResolverSpring进行数据校验失败时,会通过MessageCodesResolver生成错误码放入Errors错误对象。Spring默认使用的逻辑完全同上~

public interface MessageCodesResolver {String[] resolveMessageCodes(String errorCode, String objectName);String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class<?> fieldType);
}

它的唯一实现类是:DefaultMessageCodesResolver。它的两个方法做的事情比较简单,效果如下(注意:此处所谓的错误码就是这些字符串):

需要注意的是:这两个组件虽然都是在Spring里的,但是如果你向如上方式来提供,它就单属于Spring MVC容器的(SpringBoot另说)

自定义约束

JSRHibernate支持的约束条件已经足够强大,应该是能满足我们绝大部分情况下的基础验证的。如果还是不能满足业务需求,我们还可以自定义约束,也很简单一事。

JSRHibernate提供的约束注解解释说明:【小家Java】深入了解数据校验(Bean Validation):从深处去掌握@Valid的作用(级联校验)以及常用约束注解的解释说明

自定义一个约束分如下三步(说是2步也成):

  1. 自定义一个约束注解
  2. 实现一个校验器(实现接口:ConstraintValidator)
  3. 定义默认的校验错误信息

给个Demo:此处以自定义一个约束注解来校验集合的长度范围@CollectionRange

1、自定义注解(此处使用得比较高级)

@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)@Documentedpublic @interface List {CollectionRange[] value();}
}

2、实现一个校验器
此例用不着(下面会有)
3、自定义错误消息
当然,你可以写死在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)的。这个时候我们有两种做法:

  1. 就在该属性上标注多个注解即可(推荐
  2. 自定义一个注解,把这些注解封装起来,形成一个新的约束注解(使用场景相对较少)

自定义message消息可使用的变量

我们知道约束的失败消息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;@Overridepublic void initialize(Gender constraintAnnotation) {genderValue = constraintAnnotation.gender();}@Overridepublic 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这一大块的内容到此就告一段落了,希望讲解的所有内容能给你实际工作中带来帮助,祝好~

相关阅读

深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【享学Java】
Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动作【享学Spring】
深入了解数据校验(Bean Validation):从深处去掌握@Valid的作用(级联校验)以及常用约束注解的解释说明【享学Spring】


关注A哥

Author A哥(YourBatman)
个人站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
活跃平台
公众号 BAT的乌托邦(ID:BAT-utopia)
知识星球 BAT的乌托邦
每日文章推荐 每日文章推荐

Bean Validation完结篇:你必须关注的边边角角(约束级联、自定义约束、自定义校验器、国际化失败消息...)【享学Spring】相关推荐

  1. 17、Spring Boot普通类调用bean【从零开始学Spring Boot】

    转载:http://blog.csdn.net/linxingliang/article/details/52013017 我们知道如果我们要在一个类使用spring提供的bean对象,我们需要把这个 ...

  2. notnull注解_Hibernate Validator 第19篇:自定义约束-约束注解

    Bean Validation API定义了一整套标准的约束注解,例如@NotNull,@Size等等.这种情况下,这些内建的约束是不够的,你可以很容易地创建自定义的约束,根据你自己的校验需要. 首先 ...

  3. 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知

    乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免 ...

  4. bean validation校验方法参数_Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    本文同名博客老炮说Java:https://www.laopaojava.com/,每天更新Spring/SpringMvc/SpringBoot/实战项目等文章资料 顺便再给大家推荐一套Spring ...

  5. 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸

    乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免 ...

  6. 2. Bean Validation声明式校验方法的参数、返回值

    你必须非常努力,才能干起来毫不费力.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习.关注公众 ...

  7. 详解Bean Validation

    理性阅读,欢迎探讨 1. Bean Validation是什么? Bean Validation 是一个数据验证规范,属于Java EE 6的子规范,详情参考维基百科,这里不做赘述. 既然是规范,那么 ...

  8. JSR 303 - Bean Validation 介绍及最佳实践

    关于 Bean Validation 在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情.应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的.在通常的情况下 ...

  9. Java Bean Validation 最佳实践

    <h1 class="postTitle"><a id="cb_post_title_url" class="postTitle2& ...

最新文章

  1. 思谋科技A轮融资超1亿美元 ,成为最年轻“准独角兽”AI企业
  2. 最锋利的Visual Studio Web开发工具扩展:Web Essentials详解
  3. 判断输入的字符是不是数字
  4. ASP.NET中添加View与Razor引擎以及View解析和Controller向View传值
  5. python3的3D开发-基于blender(1)
  6. ecies算法c语言实现,Bouncy Castle算法库中ECIES算法调用示例
  7. NEFU705(数论+DP)
  8. 《Python Cookbook 3rd》笔记(5.4):读写字节数据
  9. LINQ to JavaScript
  10. php 占比图,深度|丹尼·格林和克莱·汤普森,两位顶级3D差距在哪里?
  11. 《Lost》大结局最权威最彻底解读
  12. 「释放自我」后,敢说真话的人干趴了会写PPT的人!
  13. 电话和互联网使用:首次移动电话数下降
  14. 华为南研所2014春季机试题目-1字符串拼接
  15. E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 1446 (unattended-upgr)报错信解决
  16. [python]编写程序产生 ISBN 号的校验位。
  17. 【web程序开发】前端标签介绍
  18. Oracle OPM 成本
  19. 小皮面板数据库服务启动不了,启动后立马停止
  20. 010机场等出租车排队时我该选择排哪队?

热门文章

  1. Latex:跨页多图
  2. 全球智慧城市政府50强排行榜——中国九个城市成功跻身
  3. 建模助手『 一键参数』快速修改构件,族参数
  4. 文学研究助手(设计性实验)
  5. 直播终端技术比较:Native vs H5 vs WebRTC vs 小程序
  6. 轻易云集成平台基本概念
  7. PS仿真-GUN ACCESS操作
  8. 告别苹果,英特尔明天会更好
  9. pip install exifread 安装exfriead模块
  10. 这届铲屎官不错,既舍得花钱,又会科学养猫养狗