Bean的校验主要是校验Bean的属性是否满足给定的约束条件。具体实现方式有Hibernate Validator和Spring Validator,它们都是对validation-api的实现。这里主要介绍Hibernate Validator(Hibernate出品的一个校验框架,跟数据库没关系),在Spring MVC的Controller参数校验时会结合用到Spring Validator一点东西。
基本用法
1、添加maven依赖
<!--校验-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
<!--高版本需要javax.el-->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b08</version>
</dependency>
需要注意的是,hibernate-validator5以下版本,不需要javax.el,高版本才需要。
2、添加约束
我们以汽车为例,制造商不能为空,车牌号2-14位,座位数至少2个,那么通过注解的方式添加约束如下所示:
@Data
public class Car {
@NotBlank
private String manufacturer;
@NotBlank
@Size(min = 2, max = 14)
private String licensePlate;
@Min(value = 2)
private int seatCount;
public Car(String manufacturer,String licensePlate,int seatCount){
this.manufacturer = manufacturer;
this.licensePlate = licensePlate;
this.seatCount = seatCount;
}
}
@NotBlank,@Size等都是Hibernate Validator内置的一些校验注解,其它还有@Future,@Past,@Pattern,@Valid,@Max等等,这些注解从字面意思上都很好理解。要重点说的就是这个@Valid注解,它表示需要递归的去校验,如果bean的某个字段是另外一个带有校验注解的bean,那么也需要对里面的字段进行校验,否则不进行校验。
3、进行校验
约束添加好了,那么如何进行校验呢?不多说,先上代码:
public String checkCar(Car car) {
// 拿到validator对象
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// 开始校验
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
// 获取校验结果
if(constraintViolations.isEmpty()){
return "";
}
StringBuilder sb = new StringBuilder();
Iterator it = constraintViolations.iterator();
while(it.hasNext()){
ConstraintViolation<Car> cv = (ConstraintViolation<Car>)it.next();
sb.append(cv.getMessage()).append(";");
}
return sb.toString();
}
Validator通过validate方法来校验一个bean,然后将结果放在一个ConstraintViolation的集合中。Validator实例则通过工厂方法得到。
我们来测试一下:
@Slf4j
public class CarFactoryTest {
@Test
public void testCheckCar(){
Car car = new Car(null,"1",1);
CarFactory cf = new CarFactory();
log.info(cf.checkCar(car));
}
}
运行结果:
可以看到,校验确实起到了作用。不过似乎这个错误提示不太友好,是什么不能小于2?又是什么不能为空?没有具体到某个字段。这个可以通过自定义mesage解决。
自定义Message
在添加约束注解的时候,我们可以提供一个message来覆盖默认的message。例如:
@NotBlank(message = "制造商不能为空")
private String manufacturer;
@Min(message = "座位最少为2",value = 2)
private int seatCount;
很简单,但似乎还有一点不妥,如果座位最少改成了4座,那这个message就不正确了,我们希望提示中的这个数字要和给定的限制一致。在Hibernate Validator中可以通过{}来引用注解当中的属性值,例如:
@Min(message = "座位最少为{value}",value = 2)
private int seatCount;
这样是不是好多了?
可能还有一个需求,就是我们想要提示的时候,把错误的值也展示出来,这个可以通过${}表达式做到,例如:
@NotBlank(message = "车牌号不能为空")
@Size(message = "车牌号必须{min}-{max}位,当前值是:${validatedValue}",min = 2, max = 14)
private String licensePlate;
${validateValue}就表示当前错误的值。好,我们再来看看运行效果:
非常完美!
Controller参数校验
在Spring MVC的Web项目中,Bean Validation最重要的作用就是对controller的参数进行校验。
/**
* 登录请求
* controller中的校验示例,基于hibernate的validation
* */
@RequestMapping(value = "/login_2.do", method = RequestMethod.POST)
public String login_2(Model model, @Valid LoginInfoBean_2 loginInfoBean, BindingResult result){
// spring mvc 中校验参数
StringBuilder sb = new StringBuilder();
if(null != result && result.hasErrors()){
if (null != result.getFieldErrors()) {
for (FieldError fe : result.getFieldErrors()) {
sb.append(fe.getDefaultMessage()).append(" ");
}
}
model.addAttribute("errInfo",sb.toString());
return "login";
}
// 业务逻辑省略...
}
我们以登录为例,登录信息我们封装在LoginInfoBean_2里面,如下所示:
@Data
public class LoginInfoBean_2 implements Serializable{
@NotBlank(message = "姓名不能为空")
private String name_2;
@NotBlank(message = "密码不能为空")
@Length(message = "密码长度{min}-{max}位",min = 6,max = 16)
private String pwd_2;
}
在contoller中,我们通过@Valid注解表示需要对LoginInfoBean中的字段进行校验,然后我们后面紧跟着一个BindingResult,校验的结果会放在这个参数里面。与基本的用法不同,我们这里不需要手动校验,只需要提取校验结果即可,校验的工作已经由spring帮我们做了。插一句题外话,如果深入一点的话,利用AOP,我们可以将校验放到切面里面去,这样controller里面就只用关注业务即可。
分组校验
楼主之前写过的一个应用中,有更新菜单和删除菜单两个接口,菜单的bean如下所示:
@Data
public class Menu {
@NotBlank(message = "id不能为空" )
private String id;
@NotBlank(message = "名称不能为空")
private String name;
@NotBlank(message = "url不能为空")
private String url;
private String icon;
}
对于更新来说,id,name,url都不能为空;对于删除来说,只需要id不为空就可以了。为了方便,controller里面接收的参数都是这个bean。那么问题来了,在对删除菜单进行校验的时候,因为注解的缘故,对name和url也进行了校验。这不符合期望,怎么办?这个时候分组就派上了用场。
@Data
public class Menu {
@NotBlank(message = "id不能为空" ,groups = {MenuUpdateCheck.class, MenuDeleteCheck.class})
private String id;
@NotBlank(message = "姓名不能为空",groups = MenuUpdateCheck.class )
private String name;
@NotBlank(message = "url不能为空",groups = MenuUpdateCheck.class)
private String url;
private String icon;
}
在使用约束的时候,可以指定该约束所属分组。更新分组为MenuUpdateCheck,删除分组为MenuDeleteCheck。分组是一个简单得不能再简单的接口,定义如下:
public interface MenuDeleteCheck {
}
然后我们来进行校验:
public void testGroupValidation(){
Menu menu = new Menu();
// 拿到validator对象
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// 开始校验
Set<ConstraintViolation<Menu>> constraintViolations = validator.validate( menu, MenuDeleteCheck.class);
// 获取校验结果
if(constraintViolations.isEmpty()){
log.info("校验通过");
}
StringBuilder sb = new StringBuilder();
Iterator it = constraintViolations.iterator();
while(it.hasNext()){
ConstraintViolation<Menu> cv = (ConstraintViolation<Menu>)it.next();
sb.append(cv.getMessage()).append(";");
}
log.info("校验不通过.{}",sb.toString());
}
几乎和原来一样的代码,唯一不同的是validate方法里面给定了group参数,表示只对bean上面的MenuDeleteCheck分组进行校验。
运行结果:
可以看到,对于删除操作,确实只校验了id字段。
分组继承
上面的分组有一个缺点,就是每个要校验的约束都要指定分组,写起来很麻烦。我们想默认都启用,只是针对某个分组启用特定的约束。通过分组继承可以做到。上代码:
@Data
public class Menu {
@NotBlank(message = "id不能为空" ,groups = {MenuUpdateCheck.class, MenuDeleteCheck.class})
private String id;
@NotBlank(message = "姓名不能为空")
private String name;
@NotBlank(message = "url不能为空")
private String url;
private String icon;
}
我们只保留了id这个需要属于多个分组的groups属性,然后更改MenuUpdateCheck,使其继承Default。
public interface MenuUpdateCheck extends Default {
}
这样MenuUpdate也就继承了Default分组。那么什么是Default分组?所有没有写明group的都属于Default分组。通过继承后,name,url也属于了MenuUpdateCheck分组,然后加上本来属于MenuUpdateCheck分组,也达到了目的。
运行结果:
分组继承还可以用于Bean有继承关系的情形中,例如父对象启动Default group,子对象特有的字段校验启用单独的一个group。这里就不再展开了。此外还有指定group检查顺序的@GroupSequence,以及递归检查时的group切换@ConvertGroup,可以参考官方文档。
自定义校验
Hibernate Validator提供了一系列内置的校验注解,可以满足大部分的校验需求。但是,仍然有一部分校验需要特殊定制,例如某个字段的校验,我们提供两种校验强度,当为normal强度时我们除了<>号之外,都允许出现。当为strong强度时,我们只允许出现常用汉字,数字,字母。内置的注解对此则无能为力,我们试着通过自定义校验来解决这个问题。
为了方便,首先我们定义一个正则枚举:
public enum CharactorRule {
Normal("[<>]+",false),
Strong("^([\\u4e00-\\u9fa5]|[0-9]|[a-z]|[A-Z])*$",true);
@Getter
private String pattern; // 正表达式
@Getter
private boolean positive; // 是否正向匹配
private CharactorRule(String pattern,boolean positive){
this.pattern = pattern;
this.positive = positive;
}
}
枚举有个两个对象Normal和Strong,然后指定了各自的正则表达式,是正向匹配还是反向匹配。
然后,我们定义一个自定义的校验注解:
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CommonCharactorValidator.class}) //具体的实现
public @interface CommonCharactor {
String message() default "包含非法字符";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CharactorRule charactorRule() default CharactorRule.Normal;
}
该注解中,我们添加了一个validateaRule属性,默认是Normal类型。该注解的校验工作由CommonCharactorValidator类来处理。
然后我们来看CommonCharactorValidator里面有什么:
public class CommonCharactorValidator implements ConstraintValidator<CommonCharactor, CharSequence> {
private Pattern pattern;
private CharactorRule validateRule;
public CommonCharactorValidator(){}
/**
* 初始化,你可以获取注解上的内容并进行处理
* */
public void initialize(CommonCharactor commonCharactor) {
validateRule = commonCharactor.charactorRule();
pattern = Pattern.compile(commonCharactor.charactorRule().getPattern());
}
/**
* 覆写验证逻辑
* */
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
return validateRule.isPositive()?pattern.matcher(charSequence.toString()).matches():!pattern.matcher(charSequence.toString()).matches();
}
}
具体的校验类需要实现ConstraintValidator接口,第一个泛型参数是所对应的校验注解类型,第二个是校验对象类型。在初始化方法initialize中,我们可以先做一些别的初始化工作,例如这里我们获取到注解上的validateRule并保存下来,然后生成正则表达式对象。真正的验证逻辑由isValid完成,如果是正向匹配那么匹配到的就返回true,否则返回false,如果是反向匹配,则是相反的逻辑。
然后我们来测试一下,先定一个Person类:
@Data
@AllArgsConstructor
public class Person {
@NotBlank(message = "姓名不能为空")
@CommonCharactor(message = "姓名只能由汉字,数字,字母组成",charactorRule = CharactorRule.Normal)
private String name;
}
接下来是测试代码:
@Slf4j
public class CustomValidationTest {
/**
* 测试自定义校验
* */
@Test
public void testCustomValidation(){
Person person = new Person("<"); // 测试不通过打开这个
//Person person = new Person("Gameloft9"); // 测试通过打开这个
// 拿到validator对象
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// 开始校验
Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );
// 获取校验结果
if(constraintViolations.isEmpty()){
log.info("校验通过!");
return;
}
StringBuilder sb = new StringBuilder();
Iterator it = constraintViolations.iterator();
while(it.hasNext()){
ConstraintViolation<Car> cv = (ConstraintViolation<Car>)it.next();
sb.append(cv.getMessage()).append(";");
}
log.info("校验不通过.{}",sb.toString());
}
}
运行结果:
两个字,完美!
除了Bean的校验,还有参数,方法,返回值校验。由于用得不多,这里就不展开了,感兴趣可以参考官网文档。
附源码地址:https://github.com/gameloft9/bean-validation-demo。