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校验数据的代码:

public class StandardValidation {

	public static void main(String[] args) {
		System.out.println(validationWithoutAnnotation(" ", -1));
	}

	public static String validationWithoutAnnotation(String inputString, Integer inputInt) {
		String error = null;
		if (null == inputString) {
			error = "inputString不能为null";
		} else if (null == inputInt) {
			error = "inputInt不能为null";
		} else if (1 > inputInt.compareTo(0)) {
			error = "inputInt必须大于0";
		} else if (inputString.isEmpty() || inputString.trim().isEmpty()) {
			error = "inputString不能为空字符串";
		} else {
			// DO
		}
		return error;
	}
}

相信很多码友多少都写过类似的代码。使用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规范实现校验功能。

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

package chkui.springcore.example.javabase.validation.entity;
public class Game {
	@NotNull //非空
	@Length(min=0, max=5) //字符串长度小于5,这个是一个Hibernate Validator增加的注解
	private String name;
	
	@NotNull
	private String description;
	
	@NotNull
	@Min(0) //最小值>=0
	@Max(10) //最大值<=10
	private int currentVersion; 
    //getter and setter…………
}

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

public StandardValidation {
	public void validate() {
		//引入校验工具
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        //获取校验器
		Validator validator = factory.getValidator();
		Game wow = new Game();
        //执行校验
		Set<ConstraintViolation<Game>> 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)。

组合注解校验

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

定义新的校验注解:

package chkui.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 @interface Price {
	String message() default "定价必须在$1~$200之间";
	Class<?>[] groups() default { };
	Class<? extends Payload>[] payload() default { };
}

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

package chkui.springcore.example.javabase.validation.entity;
public class Game {
    @Price
	private float price;
    //Other field
    //setter and getter
}

自定义校验器

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

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

package chkui.springcore.example.javabase.validation.annotation;
@Constraint(validatedBy = { TypeValidator.class }) //指定校验器
@Documented
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Type {
	String message() default "游戏类型错误,可选类型为RPG、ACT、SLG、ARPG";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
}

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

TypeValidator类:

package chkui.springcore.example.javabase.validation.validator;
public class TypeValidator implements ConstraintValidator<Type, String> {
	private final List<String> TYPE = Arrays.asList(new String[]{"RPG", "ACT", "SLG", "ARPG"});
	@Override
	public boolean isValid(String value, ConstraintValidatorContext context) {
		return TYPE.contains(value);
	}
}

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

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

public class Game {
	@NotNull
	@Type
	private String type;
    //Other field ......
    //getter and setter ......
}

分组校验

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

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

public @interface Max {
	String message() default "{javax.validation.constraints.Max.message}";
	Class<?>[] groups() default { }; //用于分组的参数
	Class<? extends Payload>[] payload() default { };
	long value();
}

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

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

package chkui.springcore.example.javabase.validation.groups;
public interface BetaGroup {}

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

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

然后执行分组校验:

public enum StandardValidation {
	public void validate() {
		//引入校验工具
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		Validator validator = factory.getValidator();

		Game wow = new Game();
		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接口类以标定“问题级别”:

package chkui.springcore.example.javabase.validation;
public class PayLoadLevel {
    //警告级别
	static public interface WARN extends Payload {}
    //错误级别
	static public interface Error extends Payload {}
}

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

public class Game {
    //默认分组校验错误时,错误级别为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)
	private int currentVersion; 
	
	@AssertTrue(groups = BetaGroup.class, payload=PayLoadLevel.WARN.class)
	private boolean beta;
    //Other field ......
    //getter and setter ......	
}

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏微服务生态

Flume-NG源码分析-整体结构及配置载入分析

终于开始Flume源码的分析研究工作了,我也是边学边和大家分享,内容上难免有不足之处,望大家见谅。

1444
来自专栏猿天地

注解面试题-请了解下

金三银四,三四月是找工作最好的时期。错过了三月千万别放弃四月。 在面试的时候,有些面试官会问注解相关的问题, 注解最典型的代表框架就是Spring了,特别是Sp...

3599
来自专栏Cloud Native - 产品级敏捷

三分钟学会 Java 单元测试

前言: 此篇文章使用 Junit 4.0, 希望给无任何单元测试经验的开发者, 能在最短的时间内, 开展单元测试的工作◦ 本文:  学习 Junit 的测试框架...

2278
来自专栏java相关

SpringBoot中Async异步方法和定时任务介绍

2094
来自专栏美团技术团队

这个Spring高危漏洞,你修补了吗?

前言 2009年9月Spring 3.0 RC1发布后,Spring就引入了SpEL(Spring Expression Language)。对于开发者而言,引...

1.4K11
来自专栏小勇DW3

AOP中使用Aspectj对接口访问权限进行访问控制

只配置这段会报:The prefix "aop" for element "aop:aspectj-autoproxy" is not bound.

1804
来自专栏coderhuo

虚拟内存探究 -- 第三篇:一步一步画虚拟内存图

这是虚拟内存系列文章的第三篇。 前面我们提到在进程的虚拟内存中可以找到哪些东西,以及在哪里去找。 本文我们将通过打印程序中不同元素内存地址的方式,一步一步细...

2484
来自专栏WindCoder

RequestParam与RequestBod等参数注解简析

该注解常用来处理Content-Type: 不是application/x-www-form-urlencoded和multipart/form-data编码的...

6181
来自专栏极客编程

用Java为Hyperledger Fabric(超级账本)开发区块链智能合约链代码之部署与运行示例代码

您已经定义并启动了本地区块链网络,而且已构建 Java shim 客户端 JAR 并安装到本地 Maven 存储库中,现在已准备好在之前下载的 Hyperled...

2561
来自专栏杨建荣的学习笔记

一些“简单”的linux命令(r2笔记46天)

有些linux命令看起来极其简单,只包含2个字符,但确有很强的功能性。看起来还是有些陌生的命令,不过在工作中别忘记它们的存在。 ab 这条命令式做为性能测试所...

3098

扫码关注云+社区

领取腾讯云代金券