Java数据校验详解

一切从元编程开始

一个健壮的系统都要对外部提交的数据进行完整性、合法性的校验。即使开发一个不面对最终用户的工具包,也需要对传入的数据进行缜密的校验来防止引发底层难以追踪的问题。各路大神当然也会注意到这个问题,所以在“元编程”(见JSR250与资源控制)提出之后相续提交了JSR-303、JSR-349以及JSR-380来完善使用注解进行数据校验的机制,这三个JSR也被称为Bean Validation 1.0、Bean Validation 1.1和Bean Validation 2.0,后文统称为Bean Validation。

先看一个不使用Bean Validation校验数据的代码:

publicclassStandardValidation{publicstaticvoidmain(String[] args){System.out.println(validationWithoutAnnotation(" ", -1));}publicstaticStringvalidationWithoutAnnotation(String inputString, Integer inputInt){String error =null;if(null== inputString) {error ="inputString不能为null";}elseif(null== inputInt) {error ="inputInt不能为null";}elseif(1> inputInt.compareTo(0)) {error ="inputInt必须大于0";}elseif(inputString.isEmpty() || inputString.trim().isEmpty()) {error ="inputString不能为空字符串";}else{// DO}returnerror;}}

相信很多码友多少都写过类似的代码。使用IF—ELSE是否优雅这种高端问题暂且不谈,但是大量的IF—ELSE会导致业务内容越来越多的嵌套在代码中。针对这些问题Bean Validation为数据校验提供了更加规范化、通用化、复用程度更高的校验方法。

数据校验的原理并不复杂,主要是用注解(Annotation)在域或setter方法上声明JavaBean中数据的准则。Java的数据校验代码主要在javax.validation包中,包括注解、校验器以及校验器工厂,接下来通过例子说明。(例子可执行代码在本人的gitee库,本文代码在chkui.springcore.example.javabase.validation包)

标准数据校验

JSR提交的Javax.validation定义中已经为数据校验定义了很多方法和注解,但是需要清晰的是JSR仅仅制定了一个规范,具体的功能是由各种框架实现的。本文的例子引入了Hibernate Validator 6.0.12.Final包,他与Spring Validator一样,都是根据JSR规范实现校验功能。

数据校验是围绕一个实体类展开的,下面的代码声明了一个实体类,通过注解标注每个域上的赋值规则:

packagechkui.springcore.example.javabase.validation.entity;publicclassGame{@NotNull//非空@Length(min=0, max=5)//字符串长度小于5,这个是一个Hibernate Validator增加的注解privateString name;@NotNullprivateString description;@NotNull@Min(0)//最小值>=0@Max(10)//最大值<=10privateintcurrentVersion;//getter and setter…………}

使用校验器对其进行校验:

publicStandardValidation {publicvoidvalidate(){//引入校验工具ValidatorFactory factory = Validation.buildDefaultValidatorFactory();//获取校验器Validator validator = factory.getValidator();Game wow =newGame();//执行校验Set> violationSet = validator.validate(wow);violationSet.forEach(violat -> {violat.getPropertyPath();//校验错误的域violat.getMessage());//校验错误的信息});//设置值之后再次进行校验wow.setName("World Of Warcraft");wow.setDescription("由著名游戏公司暴雪娱乐所制作的第一款网络游戏,属于大型多人在线角色扮演游戏。");wow.setCurrentVersion(8);violationSet = validator.validate(wow);violationSet.forEach(violat -> {});}}

执行完毕之后violationSet中就是校验的结果。如果校验通过那么返回的Set长度为0。

Bean Validation已经为常规的校验功能预设了很多注解。

自定义校验规则

虽然在javax.validation.constraints已经定义了很多用于校验的注解,但是肯定无法满足复杂多样的业务需求。所以Bean Validation也支持自定义校验规则。在JSR的文档中对数据域的一个校验被称为Constraint(约束),一个Constraint由一个Annotation(注解)绑定1~n个Validator(校验器)组成。 因此可以通过新增AnnotationValidator来定义新的校验方式(或者说是定义新的Constraint)。

组合注解校验

可以通过组合已有的注解来实现新的数据校验规则。例如下面的例子。

定义新的校验注解:

packagechkui.springcore.example.javabase.validation.annotation;@Min(1)//最小值>=1@Max(300)//最大值<=300@Constraint(validatedBy = {})//不制定校验器@Documented@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD })@Retention(RetentionPolicy.RUNTIME)public@interfacePrice {Stringmessage()default"定价必须在$1~$200之间";Class[] groups()default{ };Class[] payload()default{ };}

在@Price注解中我们标记了@Min(1)和@Max(300),之后直接在域上标记@Price就会校验对应的值是否满足这个条件:

packagechkui.springcore.example.javabase.validation.entity;publicclassGame{@Priceprivatefloatprice;//Other field//setter and getter}

自定义校验器

除了组合javax.validation.constraints中的注解,还可以自定义校验器(Validator)进行数据校验。

声明一个用于自定义校验的注解:

packagechkui.springcore.example.javabase.validation.annotation;@Constraint(validatedBy = { TypeValidator.class })//指定校验器@Documented@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD })@Retention(RetentionPolicy.RUNTIME)public@interfaceType {Stringmessage()default"游戏类型错误,可选类型为RPG、ACT、SLG、ARPG";Class[] groups()default{};Class[] payload()default{};}

注意@Constraint(validatedBy = { TypeValidator.class })这一行代码,他的作用就是将这个注解和校验器进行绑定,当我们执行Validator::validator方法时对应的校验器会被调用。

TypeValidator类:

packagechkui.springcore.example.javabase.validation.validator;publicclassTypeValidatorimplementsConstraintValidator{privatefinalList TYPE = Arrays.asList(newString[]{"RPG","ACT","SLG","ARPG"});@OverridepublicbooleanisValid(String value, ConstraintValidatorContext context){returnTYPE.contains(value);}}

TypeValidator必须实现ConstraintValidator这个接口,并在范型中声明对应的校验注解和数据类型(ConstraintValidator<T, E>,T是绑定的注解类型、E是数据类型)。TypeValidator中判断数值是不是"RPG", "ACT", "SLG", "ARPG"当中的一个,若不是则TypeValidator::isValid返回false表示校验没通过。

在实体类的域上使用自定义的@Type注解:

publicclassGame{@NotNull@TypeprivateString type;//Other field ......//getter and setter ......}

分组校验

对于业务来说数据录入的规则并不是一成不变的,往往需要根据某些状态来对单个或一组数据进行校验。这个时候我们可以用到分组功能——根据状态启用一组约束。

观察自定义注解或javax.validation.constraints包中预定以的注解,都有一个groups参数:

public@interfaceMax {Stringmessage()default"{javax.validation.constraints.Max.message}";

Class<?>[] groups() default { }; //用于分组的参数

Class<? extends Payload>[] payload() default { };

long value();

}

如果未指定该参数,那么校验都属于javax.validation.groups.Default分组。

先定义一个分组,用一个没有任何功能的类或者接口即可:

packagechkui.springcore.example.javabase.validation.groups;publicinterfaceBetaGroup{}

然后在校验的注解上通过groups指定分组:

publicclassGame{@NotNull@Min(0)//最小值>=0@Max(10)//最大值<=10@Max(value=0, message="未发行的游戏版本为0!", groups = BetaGroup.class)//分组校验privateintcurrentVersion;@AssertTrue(groups = BetaGroup.class)//分组校验//表示是否为内侧版privatebooleanbeta;//Other field ......//getter and setter ......}

然后执行分组校验:

publicenumStandardValidation {publicvoidvalidate(){//引入校验工具ValidatorFactory factory = Validation.buildDefaultValidatorFactory();Validator validator = factory.getValidator();Game wow =newGame();wow.setName("World Of Warcraft");wow.setDescription("由著名游戏公司暴雪娱乐所制作的第一款网络游戏,属于大型多人在线角色扮演游戏。");wow.setCurrentVersion(8);wow.setType("RPG");wow.setPrice(401.01F);//使用默认分组校验violationSet = validator.validate(wow);//指定分组校验violationSet = validator.validate(wow, BetaGroup.class);}}

Validator::validator方法未指定分组时,相当于使用javax.validation.groups.Default分组。而在violationSet=validator.validate(wow, BetaGroup.class);这一行代码指定分组之后,只会执行groups = BetaGroup.class注解的校验。

可以一次指定多个分组的校验,这样有利于处理复杂的状态:

validator.validate(wow, Default.class, BetaGroup.class, OtherGroup.class);

校验错误级别

校验的注解中还有一个参数——payload,他表示“校验问题”的级别。这个参数就像使用Log4j输出日志会指定DEBUG、INFO、WARN等级别一样,在校验数据时会有对“校验问题”进行分类的需求,比如某些页面会对用户录入的数据进行“错误”或“警告”的提示。

在使用payload时需要先声明PalyLoad接口类以标定“问题级别”:

packagechkui.springcore.example.javabase.validation;publicclassPayLoadLevel{//警告级别staticpublicinterfaceWARNextendsPayload{}//错误级别staticpublicinterfaceErrorextendsPayload{}}

然后在JavaBean上指定“校验问题”的级别:

publicclassGame{//默认分组校验错误时,错误级别为Error@NotNull(payload=PayLoadLevel.Error.class)@Min(value=0, payload=PayLoadLevel.Error.class)@Max(value=10, payload=PayLoadLevel.Error.class)//BetaGroup分组错误级别为WARN@Max(value=0, message="未发行的游戏版本为0!", groups = BetaGroup.class, payload=PayLoadLevel.WARN.class)privateintcurrentVersion;@AssertTrue(groups = BetaGroup.class, payload=PayLoadLevel.WARN.class)privatebooleanbeta;//Other field ......//getter and setter ...... }

然后在执行校验的时候使用ConstraintViolation::getConstraintDescriptor::getPayload方法获取每一个校验问题的错误级别:

violationSet = validator.validate(wow, BetaGroup.class);violationSet.forEach(violat -> {violat.getPropertyPath();//错误域的名称violat.getMessage();//错误消息violat.getConstraintDescriptor().getPayload();//错误级别});

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容