美文网首页纵横研究院后端基础技术专题社区
代码整洁之道-Spring Validation【原创】

代码整洁之道-Spring Validation【原创】

作者: 比轩 | 来源:发表于2019-07-13 16:51 被阅读1次

前言:

本篇文章不是API参考文档,所以不会将用到的所有内容详细列出来。本文的目的主要是告诉读者关于Java的 Bean Validation在Spring的应用,并针对常见的场景进行说明,力求让读者对Java的Bean Validation有一个完整的认识和理解。

文章关键字:

  • JSR-303
  • Bean Validation 1.0/1.1/2.0
  • MVC Validation
  • Hibernate Validation
  • Spring Validation

代码中为了保证代码的正常运行,经常会对入参参数做大量的校验,以防止非法输入导致程序运行异常,Java 从2009年开始提出了 Bean Validation 1.0(也就是JSR-303)API,力求将输入输入的校验标准化和简单化,更重要的是将校验通用化。Hibernate Validation 是常用的针对Bean Validation API的实现之一(还有Apache BVal),并在Bean Validation 的API基础上,进行了扩展,以覆盖更多的场景。Spring Validation 则在整合了Hibernate Validation 的基础上,以Spring的方式,支持Spring应用的输入输出校验,比如MVC入参校验,方法级校验等等。至此,针对文章关键字已经进行了大概的说明,下面是他们之间的详细关系:

依赖关系

到目前为止Java Bean validation一共有三个版本。

Java Bean Validation版本关系

概览

下面的代码片段是Controller中常见的代码,这里出现了@Valid@Validated@NotEmpty等等和校验相关的注解,但是其目的却很简单:对uuiddtoList两个参数进行校验,并且对list中的元素也进行遍历校验。

后续我们在针对此代码片段进行详细说明。

@Validated
@RestController
public class DemoController {

    @PutMapping("bean/validation/tips/{uuid}")
    @Validated({Default.class, Update.class})
    public ResponseEntity<List<ValidationDTO>> doSomething(
            @PathVariable("uuid") @Size(min=32, max=32) String uuid,
            @RequestBody @Valid @NotEmpty List<ValidationDTO> dtoList) {
            // do something
    }
}

@Valid和@Validated

  • @Valid (javax.validation): 是Bean Validation 中的标准注解,表示对需要校验的 【字段/方法/入参】 进行校验标记

  • @Validated (org.springframework.validation.annotation):是Spring对@Valid扩展后的变体,支持分组校验。

MVC中的校验

Spring中的校验有两种场景,一种是MVC中的controller层校验,一种是添加@Validated的bean的校验,上面提到的例子其实是两种场景的共用的情况。

MVC中的校验比较简单,在Controller的方法入参或者出参添加@Valid或者@Validated注解,即可对标记的对象进行校验。

假设需要校验的目标对象为PersonPerson的每个字段都有一定的业务要求:

public class Person {

    @NotBlank //名称不能为空
    private String name;
    
    @Pattern(regexp = "1[0-9]{10}") // 电话号码满足1开头,11位长的数字
    private String number;

    @NotEmpty //至少有一个地址
    private List<String> address;

  //getter/setter
  
}

则以下几种使用方法都是ok的

// test1: 使用Valied对Person进行校验
@PostMapping("test1")
public ResponseEntity<?> test1(@RequestBody @Valid Person person) {

    return ResponseEntity.ok("ok");
}
// test2: 使用@Validated对person进行校验,并将错误信息绑定到BindingResult中
@PostMapping("test2")
public ResponseEntity<?> test2(@RequestBody @Validated Person person, BindingResult result) {

    if (result.hasErrors()) {
        for (FieldError fieldError : result.getFieldErrors()) {
            //...
        }
        return ResponseEntity.badRequest().body("fail");
    }
    return ResponseEntity.ok("success");
}
// test3: 如果有多个需要校验的参数需要给到BindingResult中,则每个result需要紧跟着被校验对象
@PostMapping("test3")
public ResponseEntity<?> test3(@Validated Person person, BindingResult result,
                               @Validated Person person2, BindingResult result2) {

    if (result.hasErrors()) {
        for (FieldError fieldError : result.getFieldErrors()) {
            //...
        }
        return ResponseEntity.badRequest().body("fail");
    }
    return ResponseEntity.ok("success");
}

综上代码所述:mvc的校验中@Valid@Validated是可以互换的,行为基本一致。test1中没有将校验的结果放到BindingResult中,则controller校验未通过时,会直接扔出异常,如没有自动捕获,则请求会返回BadRequest:400

校验对象树

上述例子中Person是一个较为简单的DTO,如果是一个比较复杂的嵌套的DTO话,则校验的目标就不应该是一个对象,而是一个对象树(可以把每一复杂的对象属性看作一个节点)。这种情况只需要调整DTO中的校验注解,在需要进入到内部校验的对象或者数据集合添加@Valid注解即可。Hibernate Validator官方文档中有较为详细的描述【占坑】。

public static class Employee {

    @NotNull(groups = {Update.class})
     private String uuid;

    @NotBlank(message = "员工姓名不能为空")
    private String name;

    @Pattern(regexp = "1[0-9]{10}")
    private String number;

    @NotEmpty
    private List<String> address;

    @Valid // family中每一个Person对象都进行完整校验
    @NotEmpty
    private List<Person> family;

    @Valid // employee对象也会被作为一个DTO完整校验
    private Employee superior;
}

自定义错误信息&分组校验

上述Employeename字段上的@NotEmpty注解提供了message,其作用是当校验未通过,将会使用message的值作为错误消息返回。如果缺省的话,校验框架会自动生成消息如:"Employee.name can not be empty",大多数情况,校验注解中的message都会配置为Spring的国际化消息的code进行使用。

上述Employeeuuid主键字段上添加了NotNull注解,但是提供了groups,其值为Update.class。其作用是当校验组包含Update.class标记时,此校验注解才会生效,其他未提供组的校验注解默认为Default.class组,也就是默认组。这个就是按组校验,如果要上Employee中所有的校验注解都生效,则需要使用@Validated({Update.class, Default.class}),当然如果只需要默认组生效,直接用@Validated或者@Validated(Default.class)都可以。

下面是用法举例:

// 分组校验
@PostMapping("test1")
public ResponseEntity<?> test4(@RequestBody @Validated({Update.class, Default.class}) Employee employee) {
    return ResponseEntity.ok("ok");
}

MVC的入参校验未生效

ok,到目前都是看起来一切都OK,但是注意下面例子中 test5/test6的情况。

@PostMapping("test5")
public ResponseEntity<?> test5(@Valid @RequestBody List<Person> personList) {
    return ResponseEntity.ok("ok");
}

@PostMapping("test6")
public ResponseEntity<?> test6(@Validated @RequestBody List<Person> personList) {
    return ResponseEntity.ok("ok");
}

接口的批量操作是很常见的需求,比如批量新建数据,这个时候Controller的入参基本上都是集合的形式。但是奇怪的是这种写法并不会生效,无论是@Valid或者@Validated注解。为什么呢?

原因分析:

直接对List集合进行校验的行为和对自定的DTO校验的行为其实是有区别的,区别在于自定义的DTO是被作为一个整体对象校验(可以理解为一个入口),对象里的每一个字段都会被按照标记的注解进行校验。但是将List作为一个整体对象的时候,其内部是没有任何校验注解的,因为java源码中本身就没有。上述的test5test6其本质是方法级别的校验,与下面这个例子test7类似。这个时候@Valid@NotEmpty都想把personList作为一个字段来校验,但是MVC不支持这种模式,所以未生效。

@PostMapping("test7")
public ResponseEntity<?> test7(@Valid @NotEmpty @RequestBody List<Person> personList) {
    return ResponseEntity.ok("ok");
}

解决方案:

解决办法有两种,一种是封装,将接口需要校验的参数封装为一个DTO,然后再校验。第二个种是使用Spring的方法级别的校验,在Controller的类上添加@Validated注解。注意任何Spring的bean都可以添加@Validated注解来进行方法级别的校验,并不是只能用在Controller上。

详解@Validated注解

关于@Validated注解的使用,官方注释里面已经写的很清楚了,我这里简单翻译下:

  1. JSR-303的变种{@link javax.validation.Valid},支持验证组规范。支持基于Spring的JSR-303,但不支持JSR-303的特殊扩展。
  2. 可以用于例如Spring MVC处理程序方法参数。通过{@linkorg.springframework.validation.SmartValidator}支持组验证。
  3. 支持方法级的验证。在方法级别上添加此注解,会覆盖类上的组信息。但是方法上的注释不会作为切入点,要想方法上的注解生效,类上也必须添加注解。
  4. 支持元注解,可以添加在自定义注解上,组装为新的注解

通过官方的注释,已经能够明白这个注解的大部分功能了。

。。。未完待续

相关文章

  • 代码整洁之道-Spring Validation【原创】

    前言: 本篇文章不是API参考文档,所以不会将用到的所有内容详细列出来。本文的目的主要是告诉读者关于Java的 B...

  • SpringMVC数据校验

    SpringMVC数据校验 Validation和JSR 303代码示例地址 Spring的Validation校...

  • [代码整洁之道]-整洁代码

    前段时间,看了代码整洁之道,顺手做了些笔记,分享给大家,和大家一起探讨整洁代码之道。 1.1要有代码 代码是我们最...

  • 代码整洁之道-<函数>

    代码整洁之道-<函数> 代码整洁之道 一书相关读书笔记,整洁的代码是自解释的,阅读代码应该如同阅读一篇优秀的文章,...

  • 代码整洁之道

    01、有意义的命名 在团队开发中,团队小伙伴编码风格各不相同,一个统一的规范就显得尤为重要,最近在做Code Re...

  • 代码整洁之道

    整洁代码 Leblanc : Later equals never.(勒布朗法则:稍后等于永不) 对代码的每次修改...

  • 代码整洁之道

    海到无边天作岸,山登绝顶我为峰。作为猿类的我们,对自己创造的代码有着一种天生的无比自信。这是好事~可是,对于我们的...

  • 代码整洁之道

    1.一次只做一件事的原则 除了最外边必要的空判断,少用return操作符。原则如下图所示:一次只做一件事情.png...

  • 代码整洁之道

    一.整洁代码 借用一条美国童子军简单军规:让营地笔记来时更干净 二.有意义的命名 2.7避免使用编码编码已经太多,...

  • 代码整洁之道

    大概读了一下《代码整洁之道》这本书,总结如下: 1.变量名:有意义、可读性好 2.避免重复和无意义的条件判断 3....

网友评论

    本文标题:代码整洁之道-Spring Validation【原创】

    本文链接:https://www.haomeiwen.com/subject/ptypkctx.html