Bean Validation

本文详细介绍Bean的校验方法,包括HibernateValidator的使用、自定义校验、分组校验等内容,并提供实例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值