Hibernate Validator 参数校验优雅实战

简介

在项目中,难免需要对参数进行合法性的效验,多次出现if效验数据使得业务代码显得臃肿。

JSR提供了一套Bean校验规范的API,维护在包javax.validation.constraints下。该规范使用属性或者方法参数或者类上的一套简洁易用的注解来做参数校验。在开发过程中,仅需在需要校验的地方加上形如@NotNull, @NotEmpty 等注解。

Hibernate validator框架 可以很优雅的方式实现参数的效验。hibernate Validator提供了JSR303规范中所有内置约束的实现,除此之外还有一些附加约束。

快速实战

在springboot中 不需要引入Hibernate Validator , 因为 在引入的 spring-boot-starter-web(springbootweb启动器)依赖的时候中,内部已经依赖了 hibernate-validator 依赖包。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.9.Final</version>
</dependency>

常用注解

hibernate-validator提供的校验方式为在类的属性上加入相应的注解来达到校验的目的。hibernate-validator提供的用于校验的注解如下:

注解 说明
@NotEmpty 不能为空,这里的空是指空字符串
@NotBlank 不能为空,检查时会将空格忽略
@Pattern(regex=) 被注释的元素必须符合指定的正则表达式
@NotNull 不能为null
@Min 该字段的值只能大于或等于该值
@Max 该字段的值只能小于或等于该值
@Length(min=,max=) 所属的字段的长度是否在min和max之间,只能用于字符串
@AssertTrue 用于boolean字段,该字段只能为true
@AssertFalse 用于boolean字段,该字段只能为false
@Email 检查是否是一个有效的email地址
@Future 检查该字段的日期是否是属于将来的日期
@Past 必须是过去的日期
@Size 元素的大小必须在指定范围内

参数上添加注解

@Data
public class City implements Serializable {

    private static final long serialVersionUID = -1L;

    /**
     * 城市编号
     */
    private Long id;

    /**
     * 省份编号
     */
    private Long provinceId;

    /**
     * 城市名称
     */
    @NotBlank(message = "城市名称不能为空")
    private String cityName;
}

校验的Bean前添加@Valid或者@Validated注解

@RequestMapping(value = "/save",method = RequestMethod.POST)
    public void save(@Valid @RequestBody City city){
        System.out.println(city.toString());
    }

postman调用:

返回结果:

{
    "timestamp": "2022-11-11T03:15:02.176+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.city.cityName",
                "NotBlank.cityName",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "city.cityName",
                        "cityName"
                    ],
                    "arguments": null,
                    "defaultMessage": "cityName",
                    "code": "cityName"
                }
            ],
            "defaultMessage": "城市名称不能为空",
            "objectName": "city",
            "field": "cityName",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='city'. Error count: 1",
    "path": "/save"
}

这样返回的结果不够友好,可以添加全局异常处理返回的结果。

定义返回数据实体

@Data
public class ResultBody {
    /**
     * 响应代码
     */
    private String code;

    /**
     * 响应消息
     */
    private String message;

    public static ResultBody fail(String code,String message){

        return new ResultBody(code,message);
    }
  }

全局异常处理

@RestControllerAdvice
public class ValidatorConfiguration {


    @ExceptionHandler({BindException.class,MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResultBody handleError(MethodArgumentNotValidException e) {

        return this.handleError(e.getBindingResult());
    }

    private ResultBody handleError(BindingResult result) {
        FieldError error = result.getFieldError();
        String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
        return ResultBody.fail("400", message);
    }
}

再次调用返回结果:

{
    "code": "400",
    "message": "cityName:城市名称不能为空",
    "result": null
}

校验模式

hibernate validator 有两种校验模式:普通模式快速失败模式

  • 普通模式它会校验所有属性,并返回所有的失败信息
  • 快速失败模式则是只有一个校验失败就会返回
@Configuration
public class HibernateValidatorConfiguration {
    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                //  快速失败模式  true表示启用,false表示普通模式
                .addProperty("hibernate.validator.fail_fast","true")
                .buildValidatorFactory();

        return validatorFactory.getValidator();
    }
}

普通模式全局异常处理:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultBody resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){

    List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();
    if(!CollectionUtils.isEmpty(objectErrors)) {
        StringBuilder msgBuilder = new StringBuilder();
        for (ObjectError objectError : objectErrors) {
            msgBuilder.append(objectError.getDefaultMessage()).append(",");
        }
        String errorMessage = msgBuilder.toString();
        if (errorMessage.length() > 1) {
            errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
        }
        return ResultBody.fail("400", errorMessage);

    }
    return null;
}

分组校验

同一个字段在不同场景下,使用不同的校验规则,这时候就用到了分组校验。

public class User {
    public interface Default {
    }

    public interface Update {
    }

    @NotNull(message = "id不能为空", groups = Update.class)
    private Long id;

    @NotNull(message = "名字不能为空", groups = Default.class)
    @Length(min = 4, max = 10, message = "name 长度必须在 {min} - {max} 之间", groups = Default.class)
    private String name;

    @NotNull(message = "年龄不能为空", groups = Default.class)
    @Min(value = 18, message = "年龄不能小于18岁", groups = Default.class)
    private Integer age;

}
/**
 * 使用Defaul分组进行验证
 * @param
 * @return
 */
@PostMapping("/validate")
public String addUser(@Validated(value = User.Default.class) @RequestBody User user) {
    return "validate";
}

/**
 * 使用Update分组进行验证
 * @param
 * @return
 */
@PutMapping("/validate1")
public String updateUser(@Validated(value = {User.Update.class}) @RequestBody User user) {
    return "validate1";
}

自定义校验规则

定义注解:

  • 与普通注解相比,这种自定义注解需要增加元注解@Constraint,并通过validatedBy参数指定验证器。
  • 依据JSR规范,定义三个通用参数:message(校验失败保存信息)、groups(分组)和payload(负载)。
  • 自定义额外所需配置参数
  • 定义内部List接口,参数是该自定义注解数组,配合元注解@Repeatable,可使该注解可以重复添加。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Constraint(validatedBy = FlagValidatorClass.class)
public @interface FlagValidator {
    // flag的有效值,多个使用,隔开
    String values();

    // flag无效时的提示内容
    String message() default "flag必须是预定义的那几个值";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

定义校验类:

  • 该验证器需要实现ConstraintValidator接口,ConstraintValidator接口包含两个类型参数,第一个指定验证器要校验的注解,第二个参数指定要验证的数据类型。
  • 实现initialize方法,通常在该注解中拿到注解的参数值。
  • 实现isValid方法,方法第一个参数是要校验的属性值;校验逻辑写在该方法内;校验通过返回true,校验失败返回false。
public class FlagValidatorClass implements ConstraintValidator<FlagValidator,Object> {
    /**
     * FlagValidator注解规定的那些有效值
     */
    private String values;

    @Override
    public void initialize(FlagValidator flagValidator) {
        this.values = flagValidator.values();
    }

    /**
     * 用户输入的值,必须是FlagValidator注解规定的那些值其中之一。
     * 否则,校验不通过。
     * @param value 用户输入的值,如从前端传入的某个值
     */
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        // 切割获取值
        String[] value_array = values.split(",");
        Boolean isFlag = false;

        for (int i = 0; i < value_array.length; i++){
            // 存在一致就跳出循环
            if (value_array[i] .equals(value)){
                isFlag = true; 
                break;
            }
        }

        return isFlag;
    }
}

使用注解:

// flag值必须是1或2或3,否则校验失败
@FlagValidator(values = "1,2,3")
private String flag ;

使用Hibernate Validator编程式校验

在有些场景不是http请求,比如消费mq数据,这时候要自行实现校验方式。

public static String validateParams(Object voObject) {
   ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
   Validator validator = factory.getValidator();
   Set<ConstraintViolation<Object>> violations = validator.validate(voObject);
   if (violations.size() > 0) {
      List<String> msgList = new ArrayList<>();
      for (ConstraintViolation<Object> violation : violations) {
         msgList.add(violation.getMessage());
      }
      return StringUtils.join(msgList.toArray(), ",");
   } else {
      return null;
   }
}

Validation类是Bean Validation的入口点,buildDefaultValidatorFactory()方法基于默认的Bean Validation提供程序构建并返回ValidatorFactory实例。使用默认验证提供程序解析程序逻辑解析提供程序列表。代码上等同于Validation.byDefaultProvider().configure().buildValidatorFactory()。

以上代码根据java spi查找ValidationProvider的实现类,如果类路径加入了hibernate-validator,则使用HibernateValidator,关于HibernateValidator细节暂不探讨。

之后调用该ValidatorFactory.getValidator()返回一个校验器实例,使用这个校验器的validate方法对目标对象的属性进行校验,返回一个ConstraintViolation集合。ConstraintViolation用于描述约束违规。 此对象公开约束违规上下文以及描述违规的消息。

总结

本文使用 文章同步助手 同步

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

推荐阅读更多精彩内容