一切从元编程开始
一个健壮的系统都要对外部提交的数据进行完整性、合法性的校验。即使开发一个不面对最终用户的工具包,也需要对传入的数据进行缜密的校验来防止引发底层难以追踪的问题。各路大神当然也会注意到这个问题,所以在“元编程”(见JSR250与资源控制)提出之后相续提交了
先看一个不使用
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;
}
}
相信很多码友多少都写过类似的代码。使用
数据校验的原理并不复杂,主要是用注解(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。
自定义校验规则
虽然在
组合注解校验
可以通过组合已有的注解来实现新的数据校验规则。例如下面的例子。
定义新的校验注解:
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
}自定义校验器
除了组合
声明一个用于自定义校验的注解:
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 {};
}注意
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);
}
}在实体类的域上使用自定义的@Type注解:
public class Game {
@NotNull
@Type
private String type;
//Other field ......
//getter and setter ......
}分组校验
对于业务来说数据录入的规则并不是一成不变的,往往需要根据某些状态来对单个或一组数据进行校验。这个时候我们可以用到分组功能——根据状态启用一组约束。
观察自定义注解或
public @interface Max {
String message() default "{javax.validation.constraints.Max.message}";
Class<?>[] groups() default { }; //用于分组的参数
Class<? extends Payload>[] payload() default { };
long value();
}如果未指定该参数,那么校验都属于
先定义一个分组,用一个没有任何功能的类或者接口即可:
package chkui.springcore.example.javabase.validation.groups;
public interface BetaGroup {}然后在校验的注解上通过
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.validate(wow, Default.class, BetaGroup.class, OtherGroup.class);校验错误级别
校验的注解中还有一个参数——
在使用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();//错误级别
});