【扩】基于注解的参数校验器Hibernate Validator

前言

你还在为校验入参时写的那一串 if...else... 而苦恼嘛?
你还在为了编写一个功能全面的参数校验器而夜夜不寐嘛?
No~ NoNo~ NoNoNo~ No!
人生苦短,大可不必讲宝贵的编程时间耗费在这些事情上~
是时候换个活法啦!有现成的工具,拿去用吧!
卖个萌。。。(✺ω✺)

public class User {
    @NotBlank(message = "用户名不能为空")
    private String name;
 
    @NotBlank(message = "ERP不能为空")
    @Size(min = 3, message = "ERP长度不能小于3")
    private String erp;
 
    @Min(value = 22, message = "年龄不能低于22岁")
    private int age;
 
    // ... getter and setter
}

看看上面这种参数校验,多么的 优雅~
直接通过在属性上写注解就可以达到给参数增加校验规则的目的啦!


基于注解的参数校验器

JSR303

JSR303是一套JavaBean参数校验的 标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。

image.png

ps. 没有列举全,感兴趣的可以看JSR303官方文档

Hibernate Validator

Hibernate Validator在JSR303的基础上对校验注解进行了扩展

image.png

ps. 同样没列举全,大家感兴趣可以看Hibernate Validator官方文档

Hibernate Validator就决定是你了

基本上Hibernate validator就包括了所有常用的校验规则,而且它又是在JSR303基础上的扩展,所以直接 推荐使用 Hibernate Validator了。

Hibernate Validator版本

image.png

可以看到Hibernate validator目前有两个稳定的版本,各取所需。
这篇文章里面的例子用到的版本是 5.4.2.Final

Maven引用

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-validator</artifactId>
   <version>5.4.2.Final</version>
</dependency>
image.png

代码实现

对Object的属性进行校验

// Bean属性上添加相应的注解,设置校验规则
public class User {
    @NotBlank(message = "用户名不能为空")
    private String name;
    @NotBlank(message = "ERP不能为空")
    @Size(min = 3, message = "ERP长度不能小于3")
    private String erp;
    @Min(value = 22, message = "年龄不能低于22岁")
    private int age;
 
    // ... getter and setter
}

如上,给 User 这个Bean的三个属性增加了相应的规则,message 后面规定的是校验不通过时报的错误信息文案:

  • 用户名name字段不能为null,也不能为空字符串(会过滤空格)。
  • 用户erp字段不能为null,也不能为空字符串(会过滤空格),且字符串长度不能小于3。
  • 用户年龄age字段不能小于22。
// Controller的接口参数前面添加@Valid注解
import javax.validation.Valid;
 
@Controller
@RequestMapping(value = "/validate")
public class ValidateDemoController {
 
    @RequestMapping(value = "/user", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedUserInfo(@RequestBody @Valid User user) {
        return user.toString();
    }
 
}

如上,在Controller的接口参数(刚定义的Bean)之前添加 @Valid 注解。
这样就可以用啦,校验不通过的时候会抛出异常,具体怎么处理异常就根据自己的项目需要来吧。

分组校验

有的时候,在不同场景下我们需要对同一个Bean里面的不同参数进行校验。
比如说,在新增用户的时候我需要校验姓名、erp和年龄,而在修改用户的时候我只需要校验erp。
又或者,部门A的员工的年龄不能低于22岁,无上限;而部门B的员工年龄不能高于35岁,无下限。
难道我们需要根据每一个场景都增加一个Bean嘛?会不会有点太浪费?
不用的!这里提供了一个 分组 的概念,不同的规则可以划分到不同的组里面,校验的时候选择相应的组,就会只校验该组下面的所有规则。

// 首先定义两个接口,作为两个分组
public interface UserValidGroupOne {
}
 
public interface UserValidGroupTwo {
}

定义两个接口 UserValidGroupOneUserValidGroupTwo,作为两个分组。

// Bean参数校验规则划分为两个组
public class UserByGroup {
    @NotBlank(groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "用户名不能为空")
    private String name;
 
    @NotBlank(groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "ERP不能为空")
    @Size(min = 3, groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "ERP长度不能小于3")
    private String erp;
 
    @Min(value = 22, groups = {UserValidGroupOne.class}, message = "年龄不能低于22岁")
    @Max(value = 35, groups = {UserValidGroupTwo.class}, message = "年龄不能高于35岁")
    private int age;
 
    // ... getter and setter
}

属性前写相应注解增加校验规则,注解里面的 groups 属性表示这条规则属于哪个分组,不加 groups 则表示在使用 Deafault 规则时起作用。
比如上面代码中描述的是:

  • name和erp字段的校验规则同属于两个分组。
  • age字段的校验规则,在分组1中不能小于22,而在分组2中不能大于35。
//接口参数前面注明要用哪个分组的规则来进行校验
import org.springframework.validation.annotation.Validated;
 
@Controller
@RequestMapping(value = "/validate")
public class ValidateDemoController {
     
    @RequestMapping(value = "/userByGroup1", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedUserInfoByGroupOne(@RequestBody @Validated(value = {UserValidGroupOne.class}) UserByGroup userByGroup) {
        return userByGroup.toString();
    }
 
    @RequestMapping(value = "/userByGroup2", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedUserInfoByGroupTwo(@RequestBody @Validated(value = {UserValidGroupTwo.class}) UserByGroup userByGroup) {
        return userByGroup.toString();
    }
 
}

如上,在Controller的接口参数(刚定义的Bean)之前添加 @Validated 注解,注意不是 在【对Object的属性进行校验】时讲的 @Valid 注解。
@Validatedvalue 属性上注明要使用的规则分组。
可写多个分组,但是只有第一个分组才生效。
若使用 @Valid 则表示使用 默认 校验规则。

自定义注解

虽然Hibernate Validator提供了基本上所有常用的校验规则,可还是有些场景不能满足。
比如说,现在需要一个校验规则,一个List中不能包含null。Hibernate Validator并没有提供相应注解。
这时候就需要我们自定义注解,来扩展工具,满足我们自己的需求。

//自定义参数校验注解,校验List集合中是否有null元素
import com.jd.ershou.service.impl.ListNotHasNullValidatorImpl;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
 
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
/**
 * 自定义参数校验注解
 * 校验 List 集合中是否有null 元素
 * Created by weixiaoyu on 2018/5/2.
 */
@Target({ANNOTATION_TYPE, METHOD, FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ListNotHasNullValidatorImpl.class)
public @interface ListNotHasNull {
    /**
     * 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
     */
    int value() default 0;
 
    String message() default "List集合中不能含有null元素";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    /**
     * 定义List,为了让Bean的一个属性上可以添加多套规则
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        ListNotHasNull[] value();
    }
 
}

首先定义自定义注解 @ListNotHasNull,校验List中不能含有null元素,注明其注解实现类为 ListNotHasNullValidatorImpl.class
接下来编写实现类的逻辑。

//注解@ListNotHasNull的实现类
import org.springframework.stereotype.Service;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;
 
/**
 * 自定义注解ListNotHasNull的实现类
 * 用于判断List集合中是否含有null元素
 * Created by weixiaoyu on 2018/5/2.
 */
@Service
public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> {
     
    private int value;
 
    @Override
    public void initialize(ListNotHasNull constraintAnnotation) {
        // 传入value值,可以在校验中使用
        this.value = constraintAnnotation.value();
    }
 
    @Override
    public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
        if (list != null) {
            for (Object object : list) {
                if (object == null) {
                    //如果List集合中含有Null元素,校验失败
                    return false;
                }
            }
        }
        return true;
    }
 
}

自定义注解的实现类实现了 ConstraintValidator 接口。
重要的是要重写 isValid 方法,其中的逻辑就是需要的校验规则。
接下来是测试用例。

//Bean的属性中含有List
public class Person {
    @NotBlank(message = "姓名不能为空")
    private String name;
 
    @NotBlank(message = "性别不能为空")
    private String sex;
 
    @NotEmpty(message = "家庭成员不能为空")
    @ListNotHasNull(message = "所有家庭成员信息中不能有为null的")
    @Valid // 此处加@Valid注解的原因是注明要递归校验,加上这个注解就会递归校验List中每个元素的属性是否符合规则
    private List<FamilyMember> familyMembers;
     
    // ... getter and setter
}

家庭成员 familyMembers 是一个List,其中每一个元素都是 FamilyMember 类。
familyMembers 上增加刚才自定义的 @ListNotHasNull 注解。
ps. 此处加 @Valid 注解的原因是注明要递归校验,加上这个注解就会递归校验List中每个元素 FamilyMember 的属性是否符合其内部定义规则。

//Controller的接口参数前面添加@Valid注解
import javax.validation.Valid;
 
@Controller
@RequestMapping(value = "/validate")
public class ValidateDemoController {
 
    @RequestMapping(value = "/person", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedPersonInfo(@RequestBody @Valid Person person) {
        return person.toString();
    }
 
}

同样,在Controller的接口参数(刚定义的Bean)之前添加 @Valid 注解,校验不通过则抛出异常。

Spring validator方法级别的校验

JSR和Hibernate Validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验。
Spring在此基础上进行了扩展,添加了 MethodValidationPostProcessor 拦截器,可以实现对方法参数的校验。
首先需要将 MethodValidationPostProcessor 注入到Spring容器中。

//注入到Spring容器中
@Configuration
public class PpValidatorBeanConfigurer {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

这里是通过Java Bean的方式,用 @Bean 注解,将 MethodValidationPostProcessor 注入到Spring容器中的。
也可以通过配置 xml 文件的方式,根据项目的需要选择使用。

//在Controller类上添加@Validated注解,在接口方法的每一个参数前面添加相应校验规则注解
@Controller
@RequestMapping(value = "/validate")
@Validated
public class ValidateMethodController {
 
    @RequestMapping(value = "/param", method = RequestMethod.GET)
    @ResponseBody
    public String printValidatedParam(
            @NotBlank(message = "用户名不能为空") String name,
            @Size(min = 3, message = "ERP长度不能小于3") String erp,
            @Min(value = 22, message = "年龄不能低于22岁") int age) {
 
        String msg = "name=" + name + ", erp=" + erp + ", age=" + age;
        return msg;
    }
 
}

需要在Controller类的上方添加 @Validated 注解,然后在接口方法的每一个参数前面添加相应的校验规则注解。
这样方法级别的校验就比较 灵活 了不是~

异常处理

之前一直在说,校验不通过的时候会 抛出异常,具体异常如何处理请根据自己的项目需要来。
但具体都抛出哪些异常,如何处理,我这边在写测试用例的时候捕获到了如下 三类 异常:

org.springframework.validation.BindException org.springframework.web.bind.MethodArgumentNotValidException
javax.validation.ConstraintViolationException

推荐 使用 @ControllerAdvice 搭配 @ExceptionHandler 来捕获Controller层抛出来的异常。
写了简单的处理逻辑,仅供参考

//异常处理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
 
/**
 * Created by weixiaoyu on 2018/5/2.
 */
@ControllerAdvice
public class PpValidatorExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(PpValidatorExceptionHandler.class);
 
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public String handleThrowableException(Throwable ex) {
        LOGGER.error("PpValidatorExceptionHandler.handleThrowableException", ex);
        String msg = "Throwable error: " + ex.toString();
        return msg;
    }
 
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public String handleBindException(BindException e1) {
        LOGGER.error("PpValidatorExceptionHandler.handleBindException", e1);
        List<ObjectError> errors = e1.getAllErrors();
        StringBuilder stringBuilder = new StringBuilder();
        for (ObjectError error : errors) {
            if (stringBuilder.length() != 0) {
                stringBuilder.append(",");
            }
            stringBuilder.append(error.getDefaultMessage());
        }
        String msg ="BindException error: " + stringBuilder.toString();
        return msg;
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e2) {
        List<ObjectError> errors = e2.getBindingResult().getAllErrors();
        StringBuilder stringBuilder = new StringBuilder();
        for (ObjectError error : errors) {
            if (stringBuilder.length() != 0) {
                stringBuilder.append(",");
            }
            stringBuilder.append(error.getDefaultMessage());
        }
        String msg ="MethodArgumentNotValidException error: " + stringBuilder.toString();
        return msg;
    }
 
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public String handleConstraintViolationException(ConstraintViolationException e3) {
        Set<ConstraintViolation<?>> violations = e3.getConstraintViolations();
        StringBuilder stringBuilder = new StringBuilder();
        for (ConstraintViolation<?> violation : violations) {
            if (stringBuilder.length() != 0) {
                stringBuilder.append(",");
            }
            stringBuilder.append(violation.getMessage());
        }
        String msg ="ConstraintViolationException error: " + stringBuilder.toString();
        return msg;
    }
 
}

结语

安利了这么多,觉得好使不?
那必须好使啊~~~
部门在我的安利下所有新项目都在用 Hibernate Validator 参数校验器,经过了多种线上环境验证。大家就放心去用吧~
最后,有问题的话咱们共同探讨~ 共同学习~ 共同进步哈~

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

推荐阅读更多精彩内容

  • Web系统中,校验是必不可少的环节,校验一般分为前端校验和后端校验,前端校验一般使用脚本语言,对即将要提交的数据进...
    LeaveStyle阅读 3,018评论 0 0
  • 翻译:叩丁狼教育吴嘉俊 [译者注:这篇文章是开源项目CUBA Platform的作者,在这篇文章中,作者阐述了CU...
    叩丁狼教育阅读 3,542评论 1 34
  • 在写程序的时候经常需要进行数据校验,比如服务端对http请求参数校验,数据入库时对字段长度进行校验,接口参数校验,...
    dayspring阅读 9,832评论 0 9
  • 前言   本篇文章主要简单了解下Spring中一些JSR规范所提供的注解,所谓JSR规范,是Java Specif...
    骑着乌龟去看海阅读 1,025评论 0 3
  • 流水账 起来第一件事情就是修车。老爸怀疑是离合器的问题,然而修理厂的师傅开出去溜了一圈,很肯定是动力问题:离合器有...
    狸狸的守护者阅读 121评论 0 0