jsr303,hibernate validator的注解使用教程和爬坑(一篇懂)

一、前言

本文的目的是摸清jsr303注解的坑。

我对jsr303的态度是:能不用就不用。

原因:
1、坑多。不是所有开发人员都会花时间去摸细节
2、满足不了复杂校验。花拳绣腿,不能满足实际开发中复杂的校验场景,与其部分校验用jsr303,部分自写,还不如全部自写。

本文基于 6.0.17.Final 进行研究。其他版本是否会跟我的版本有出入? 未知(不过推测不可能有大的变化,不然以前用这个注解在升级后校验规则的行为变了,那就坑了)

二、使用注意

  • @Valid 和 @Validated,用@Valid,别用hibernate的注解,用javax的

    有些开发偶尔用@Valid,偶尔用@Validated,一个是javax的,一个是spring的注解。别这样,虽然@Validated有它独特的group功能,但是
    
  • 如下注意点

    // 不用@valid不检查
    @PostMapping("/test1")
    public String test1(T01_NoMessage dto) {
    	return "ok";
    }
    
    // 用了@Valid才检查
    @PostMapping("/test1")
    public String test1(@Valid T01_NoMessage dto) {
    	return "ok";
    }
    
    // 传JSON 可以生效
    @PostMapping("/2/test0")
    public Object test0(@Valid @RequestBody T50_Json dto) {
    	return dto;
    }
    
    // 不是dto类不生效(其他奇奇怪怪的入参不生效)
    @PostMapping("/test1")
    public String test1(@Valid List<T01_NoMessage> dto) {
    	return "ok";
    }
    
    // 不生效
    @PostMapping("/3/test1")
    public Object test1(@Valid @NotBlank String name) {
    	return "name:" + name;
    }
    
    
    // 生效:除了dto外还有其他字段、路径参数
    @PostMapping("/3/test4/{id}")
    public Object test4(
    	@PathVariable("id") int id,
    	String other,
    	@Valid T66_NotOnly dto
    ) {
    	return dto;
    }
    
    // 生效:传JSON,除了dto外,还有other。(PS:这个other怎么传参的? 通过`?other=xxx`即可)
    @PostMapping("/3/test5")
    public Object test5(
    	String other,
    	@Valid @RequestBody T66_NotOnly dto
    ) {
    	return dto;
    }
    
  • 嵌套类必须也用@Valid,否则不生效。详细的嵌套问题在后面单独讨论

    // A类中有B类,这就叫做内嵌
    
  • 正则表达式单和双斜杠都可以

    // 可以,两个等价的(建议用一个斜杠的,因为感觉比较正统)
    @Pattern(regexp = "^[\u4e00-\u9fa5a-zA-Z0-9_]{4,64}$")
    @Pattern(regexp = "^[\\u4e00-\\u9fa5a-zA-Z0-9_]{4,64}$")
    
  • Mockmvc测试controller的方法,jsr303正常生效;但是如果仅仅方法调用,不生效

    // 被校验
    @Test
    public void test2() throws Exception {
    	mockMvc.perform(MockMvcRequestBuilders.post("/3/test9").param("name", "stone")).andDo(MockMvcResultHandlers.print());
    }
    
    // 不被校验
    @Test
    public void test() {
    T69_SpringTest dto = new T69_SpringTest();
    	dto.setName(null);
    	t03_TestController.test9(dto);
    }
    
    // IDEA 的bug:IDEA自动补全时,会在变量前面生成@Valid注解
    @Test
    public void test() {
      // 这里的 @Valid 是代码自动补全的(IDEA 提示 `Create local variable 'dto'`),为什么会提示这个?
      // 实际上这个 @Valid 的注解,并不会触发拦截。实际上可能是 IDEA 的一个bug
      @Valid T69_SpringTest dto = new T69_SpringTest();
      //dto.setName("");
      dto.setName(null);
      t03_TestController.test9(dto);
    }
    
嵌套问题

注意,x-www-form-urlencorded的传参方式的dto是没有内嵌结构的,因为它是键值对传参,而键值对传参非常扁平,不会有嵌套结构。前端根本是不可能传这么复杂的、有层次结构的参数的。比如对于

public class A{
  private String name;
  private B b;
  private List<C> cList;
}

@PostMapping("/test")
public void post(@Valid A a) {
....
}

要怎么传参?
http://xxx?name=stone&b.name=cassie&cList.name=Pg1&cList.name=Pg2

这样吗? 没见过这么奇怪的传参

所以针对传JSON才有嵌套的说法

嵌套是什么?

嵌套是类中有类。比如A类有成员变量B类,或者B类的集合类,如

public class A {
  private B b;
  private List<C> cList;
}

以下是爬坑,注意点

  • 如何校验嵌套的类

    // 只用@NotNull只能管必填,但不会校验b或cList
    public class A {
      @NotNull
      private B b;
      @NotNull
      private List<C> cList;
    }
    
    // 之用@Valid只管校验,就是传了就校验,不传也行(因为允许null)
    public class A {
      @Valid
      private B b;
      @Valid
      private List<C> cList;
    }
    
    // 如果要校验 "必填且必须校验",则@Notnull和@Valid一起用
    public class A {
      @NotNull
      @Valid
      private B b;
      @NotNull
      @Valid
      private List<C> cList;
    }
    
    顺序

    Jsr303 注解了Controller的方法后,当发起http请求,如果检查不通过,会进入Controller方法吗? 会进入@ControllerAdvice的捕获方法吗?

  • 检测是在调用Controller方法之前,若不通过,是不会进入的。即打在controller方法的断点是不会执行的

  • jsr303检查抛出异常, 是能够在@ControllerAdvice里捕获的

    (打消疑惑:既然检查没进入Controller方法,那抛出异常时也就不会进入@ControllerAdvice的捕获方法里)

三、常用JSR303注解速查

注解可以用在什么类型的字段上作用
@NotBlank只接受CharSequence必填。非空串 “”,非trim后是空串的如 " "
@NotEmpty只接受CharSequence,Collection,Map,Array必填。字串类型时不能是空串 “”,但可以是 " ";集合类型时必须size大于0
@Notnullany types(即接受所有类型)必填
@Max只接受BigDecimal、BigInteger、byte、short、int、long、Byte、Short、Integer、Long;不支持double/float以及它的包装类不必填,但填了就得符合
@Min同@Max不必填,但填了就得符合
@Size只接受CharSequence,Collection,Map,Array不必填,但填了就得符合
@Pattern只接受CharSequence不必填,但填了就得符合
@Email只接受CharSequence不必填,但填了就得符合
@Future一系列日期的格式,详见补充1不必填,但填了就得符合
@FutureOrPresent同@Future不必填,但填了就得符合
@Past同@Future不必填,但填了就得符合
@PastOrPresent同@Future不必填,但填了就得符合
  • 补充1:

    * java.util.Date
    * java.util.Calendar
    * java.time.Instant
    * java.time.LocalDate
    * java.time.LocalDateTime
    * java.time.LocalTime
    * java.time.MonthDay
    * java.time.OffsetDateTime
    * java.time.OffsetTime
    * java.time.Year
    * java.time.YearMonth
    * java.time.ZonedDateTime
    * java.time.chrono.HijrahDate
    * java.time.chrono.JapaneseDate
    * java.time.chrono.MinguoDate
    * java.time.chrono.ThaiBuddhistDate
    

四、一次生产爬坑和思考

记录一次生产中使用jsr303的疑惑和爬坑事件。

有一次产品说有个字段以前必填的,要改成选填。代码如下

@PostMapping("/2/test66")
public Object test66(@Valid @RequestBody T200_ProdSizeProblem dto) {
  return dto;
}


@Data
public class T200_ProdSizeProblem {
    @NotBlank
    private String name;

    @Valid
    private List<T200_ProdSizeProblemSub> subList;
}


@Data
public class T200_ProdSizeProblemSub {
    @NotBlank(message = "参数列描述不能为空")
    @Size(min = 1, max = 50, message = "最大长度为50")
    private String descr;
}

产品要的需求就是 descr 字段可选,于是我就把 @NotBlank(message = "参数列描述不能为空") 去掉,因为对于 @Size,我是知道它是 “非必填,但是如果填了长度就得符合”。所以我想你可以不填,但是填了就得按照我的长度要求,这样比较好。

但是出bug啦,界面上不填描述,但还是收到后端爆出的错误。

为什么啦? 之前不是说@Size 可以不填,但是填了才会进行size的校验的吗?

一排查,原来是描述不填的时候,desc还是传了空串。

作为后端很无奈,又不能要求前端同学 “在用户不填描述时不要出现descr字段,别传空串”,因为前端这部分逻辑比较复杂了所以不要求前端改,所以只能后端去掉@Size校验

PS:上述代码暴露出一个问题,以前的程序员根本就不知道要加上@NotNull,这个字段是一定要传的,并且size肯定会大于0,所以改成

@Data
public class T200_ProdSizeProblem {
    @NotBlank
    private String name;

  	@NotEmpty
    @Valid
    private List<T200_ProdSizeProblemSub> subList;
}

这就是我说的,很多同事根本就不清楚这些jsr303注释背后的细节和坑 !!!

HibernateValidatorJSR303的参考实现使用指南.pdf JSR 303 的参考实现 使用指南 由 Hardy FerentschikGunnar Morling and thanks to Shaozhuang Liu 4.3.1.Final 版权 © 2009 - 2011 Red Hat, Inc. & Gunnar Morling June 20, 2011 序言 1. 开始入门 1.1. 第一个Maven项目 1.2. 添加约束 1.3. 校验约束 1.4. 更进一步 2. Validation step by step 2.1. 定义约束 2.1.1. 字段级(field level) 约束 2.1.2. 属性级别约束 2.1.3. 类级别约束 2.1.4. 约束继承 2.1.5. 对象图 2.2. 校验约束 2.2.1. 获取一个Validator的实例 2.2.2. Validator中的方法 2.2.3. ConstraintViolation 中的方法 2.2.4. 验证失败提示信息解析 2.3. 校验组 2.3.1. 校验组序列 2.3.2. 对一个类重定义其默认校验组 2.4. 内置的约束条件 2.4.1. Bean Validation constraints 2.4.2. Additional constraints 3. 创建自己的约束规则 3.1. 创建一个简单的约束条件 3.1.1. 约束标注 3.1.2. 约束校验器 3.1.3. 校验错误信息 3.1.4. 应用约束条件 3.2. 约束条件组合 4. XML configuration 4.1. validation.xml 4.2. 映射约束 5. Bootstrapping 5.1. Configuration ValidatorFactory 5.2. ValidationProviderResolver 5.3. MessageInterpolator 5.3.1. ResourceBundleLocator 5.4. TraversableResolver 5.5. ConstraintValidatorFactory 6. Metadata API 6.1. BeanDescriptor 6.2. PropertyDescriptor 6.3. ElementDescriptor 6.4. ConstraintDescriptor 7. 与其他框架集成 7.1. OSGi 7.2. 与数据库集成校验 7.3. ORM集成 7.3.1. 基于Hibernate事件模型的校验 7.3.2. JPA 7.4. 展示层校验 8. Hibernate Validator Specifics 8.1. Public API 8.2. Fail fast mode 8.3. Method validation 8.3.1. Defining method-level constraints 8.3.2. Evaluating method-level constraints 8.3.3. Retrieving method-level constraint meta data 8.4. Programmatic constraint definition 8.5. Boolean composition for constraint composition 9. Annotation Processor 9.1. 前提条件 9.2. 特性 9.3. 配置项 9.4. 使用标注处理器 9.4.1. 命令行编译 9.4.2. IDE集成 9.5. 已知问题 10. 进一步阅读
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值