乐趣区

关于spring:一坨一坨的-ifelse-参数校验终于被-SpringBoot-参数校验组件整干净了

本文曾经收录进 SpringBootGuide (SpringBoot2.0+ 从入门到实战!)

  • Github 地址:https://github.com/CodingDocs/springboot-guide
  • 码云地址:https://gitee.com/SnailClimb/springboot-guide(Github 无法访问或者访问速度比较慢的小伙伴能够看码云上的对应内容)

数据的校验的重要性就不用说了,即便在前端对数据进行校验的状况下,咱们还是要对传入后端的数据再进行一遍校验,防止用户绕过浏览器间接通过一些 HTTP 工具间接向后端申请一些守法数据。

最一般的做法就像上面这样。咱们通过 if/else 语句对申请的每一个参数一一校验。

@RestController
@RequestMapping("/api/person")
public class PersonController {

    @PostMapping
    public ResponseEntity<PersonRequest> save(@RequestBody PersonRequest personRequest) {if (personRequest.getClassId() == null
                || personRequest.getName() == null
                || !Pattern.matches("(^Man$|^Woman$|^UGM$)", personRequest.getSex())) { }
        return ResponseEntity.ok().body(personRequest);
    }
}

这样的代码,小伙伴们在日常开发中肯定不少见,很多开源我的项目都是这样对申请入参做校验的。

然而,不太倡议这样来写,这样的代码显著违反了 繁多职责准则。大量的非业务代码混淆在业务代码中,十分难以保护,还会导致业务层代码繁杂!

实际上,咱们是能够通过一些简略的伎俩对下面的代码进行改良的!这也是本文次要要介绍的内容!

废话不多说!上面我会联合本人在我的项目中的理论应用教训,通过实例程序演示如何在 SpringBoot 程序中优雅地的进行参数验证(一般的 Java 程序同样实用)。

不理解的敌人肯定要好好看一下,学完马上就能够实际到我的项目下来。

并且,本文示例我的项目应用的是目前最新的 Spring Boot 版本 2.4.5!(截止到 2021-04-21)

示例我的项目源代码地址:https://github.com/CodingDocs/springboot-guide/tree/master/source-code/bean-validation-demo。

增加相干依赖

如果开发一般 Java 程序的的话,你须要可能须要像上面这样依赖:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.9.Final</version>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>javax.el</artifactId>
    <version>2.2.6</version>
</dependency>

不过,置信大家都是应用的 Spring Boot 框架来做开发。

基于 Spring Boot 的话,就比较简单了,只须要给我的项目增加上 spring-boot-starter-web 依赖就够了,它的子依赖蕴含了咱们所须要的货色。另外,咱们的示例我的项目中还应用到了 Lombok。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

然而!!!Spring Boot 2.3 1 之后,spring-boot-starter-validation 曾经不包含在了 spring-boot-starter-web 中,须要咱们手动加上!

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证 Controller 的输出

验证申请体

验证申请体即便验证被 @RequestBody 注解标记的办法参数。

PersonController

咱们在须要验证的参数上加上了 @Valid 注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认状况下,Spring 会将此异样转换为 HTTP Status 400(谬误申请)。

@RestController
@RequestMapping("/api/person")
@Validated
public class PersonController {

    @PostMapping
    public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {return ResponseEntity.ok().body(personRequest);
    }
}

PersonRequest

咱们应用校验注解对申请的参数进行校验!

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {@NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范畴")
    @NotNull(message = "sex 不能为空")
    private String sex;

}

正则表达式阐明:

  • ^string : 匹配以 string 结尾的字符串
  • string$:匹配以 string 结尾的字符串
  • ^string$:准确匹配 string 字符串
  • (^Man$|^Woman$|^UGM$) : 值只能在 Man,Woman,UGM 这三个值中抉择

GlobalExceptionHandler

自定义异样处理器能够帮忙咱们捕捉异样,并进行一些简略的解决。如果对于上面的解决异样的代码不太了解的话,能够查看这篇文章《SpringBoot 解决异样的几种常见姿态》。

@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}

通过测试验证

上面我通过 MockMvc 模仿申请 Controller 的形式来验证是否失效。当然了,你也能够通过 Postman 这种工具来验证。

@SpringBootTest
@AutoConfigureMockMvc
public class PersonControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;
    /**
     * 验证呈现参数不非法的状况抛出异样并且能够正确被捕捉
     */
    @Test
    public void should_check_person_value() throws Exception {PersonRequest personRequest = PersonRequest.builder().sex("Man22")
                .classId("82938390").build();
        mockMvc.perform(post("/api/personRequest")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(personRequest)))
                .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范畴"))
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"));
    }
}

应用 Postman 验证

验证申请参数

验证申请参数(Path Variables 和 Request Parameters)即是验证被 @PathVariable 以及 @RequestParam 标记的办法参数。

PersonController

肯定肯定不要遗记在类上加上 Validated 注解了,这个参数能够通知 Spring 去校验办法参数。

@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {@GetMapping("/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范畴了") Integer id) {return ResponseEntity.ok().body(id);
    }

    @PutMapping
    public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范畴了") String name) {return ResponseEntity.ok().body(name);
    }
}

ExceptionHandler

  @ExceptionHandler(ConstraintViolationException.class)
  ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
  }

通过测试验证

@Test
public void should_check_path_variable() throws Exception {mockMvc.perform(get("/api/person/6")
                    .contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isBadRequest())
      .andExpect(content().string("getPersonByID.id: 超过 id 的范畴了"));
}

@Test
public void should_check_request_param_value2() throws Exception {mockMvc.perform(put("/api/person")
                    .param("name", "snailclimbsnailclimb")
                    .contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isBadRequest())
      .andExpect(content().string("getPersonByName.name: 超过 name 的范畴了"));
}

应用 Postman 验证

验证 Service 中的办法

咱们还能够验证任何 Spring Bean 的输出,而不仅仅是 Controller 级别的输出。通过应用 @Validated@Valid正文的组合即可实现这一需要!

个别状况下,咱们在我的项目中也更偏向于应用这种计划。

肯定肯定不要遗记在类上加上 Validated 注解了,这个参数能够通知 Spring 去校验办法参数。

@Service
@Validated
public class PersonService {public void validatePersonRequest(@Valid PersonRequest personRequest) {// do something}

}

通过测试验证:

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonServiceTest {
    @Autowired
    private PersonService service;

    @Test
    public void should_throw_exception_when_person_request_is_not_valid() {
        try {PersonRequest personRequest = PersonRequest.builder().sex("Man22")
                    .classId("82938390").build();
            service.validatePersonRequest(personRequest);
        } catch (ConstraintViolationException e) {
           // 输入异样信息
            e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
        }
    }
}

输入后果如下:

name 不能为空
sex 值不在可选范畴

Validator 编程形式手动进行参数验证

某些场景下可能会须要咱们手动校验并取得校验后果。

咱们通过 Validator 工厂类取得的 Validator 示例。另外,如果是在 Spring Bean 中的话,还能够通过 @Autowired 间接注入的形式。

@Autowired
Validator validate

具体应用状况如下:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator()
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
  .classId("82938390").build();
Set<ConstraintViolation<PersonRequest>> violations = validator.validate(personRequest);
// 输入异样信息
violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}

输入后果如下:

sex 值不在可选范畴
name 不能为空

自定以 Validator(实用)

如果自带的校验注解无奈满足你的需要的话,你还能够自定义实现注解。

案例一: 校验特定字段的值是否在可选范畴

比方咱们当初多了这样一个需要:PersonRequest 类多了一个 Region 字段,Region 字段只能是 ChinaChina-TaiwanChina-HongKong 这三个中的一个。

第一步,你须要创立一个注解 Region

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {String message() default "Region 值不在可选范畴内";

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

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

第二步,你须要实现 ConstraintValidator接口,并重写isValid 办法。

public class RegionValidator implements ConstraintValidator<Region, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {HashSet<Object> regions = new HashSet<>();
        regions.add("China");
        regions.add("China-Taiwan");
        regions.add("China-HongKong");
        return regions.contains(value);
    }
}

当初你就能够应用这个注解:

@Region
private String region;

通过测试验证

PersonRequest personRequest = PersonRequest.builder()
      .region("Shanghai").build();
mockMvc.perform(post("/api/person")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(personRequest)))
  .andExpect(MockMvcResultMatchers.jsonPath("region").value("Region 值不在可选范畴内"));

应用 Postman 验证

案例二: 校验电话号码

校验咱们的电话号码是否非法,这个能够通过正则表达式来做,相干的正则表达式都能够在网上搜到,你甚至能够搜寻到针对特定运营商电话号码段的正则表达式。

PhoneNumber.java

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface PhoneNumber {String message() default "Invalid phone number";
    Class[] groups() default {};
    Class[] payload() default {};}

PhoneNumberValidator.java

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

    @Override
    public boolean isValid(String phoneField, ConstraintValidatorContext context) {if (phoneField == null) {
            // can be null
            return true;
        }
        //  大陆手机号码 11 位数,匹配格局:前三位固定格局 + 后 8 位任意数
        // ^ 匹配输出字符串开始的地位
        // \d 匹配一个或多个数字,其中 \ 要本义,所以是 \\d
        // $ 匹配输出字符串结尾的地位
        String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$";
        return phoneField.matches(regExp);
    }
}

搞定,咱们当初就能够应用这个注解了。

@PhoneNumber(message = "phoneNumber 格局不正确")
@NotNull(message = "phoneNumber 不能为空")
private String phoneNumber;

通过测试验证

PersonRequest personRequest = PersonRequest.builder()
      .phoneNumber("1816313815").build();
mockMvc.perform(post("/api/person")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(personRequest)))
  .andExpect(MockMvcResultMatchers.jsonPath("phoneNumber").value("phoneNumber 格局不正确"));

应用验证组

验证组咱们根本是不会用到的,也不太倡议在我的项目中应用,了解起来比拟麻烦,写起来也比拟麻烦。简略理解即可!

当咱们对对象操作的不同办法有不同的验证规定的时候才会用到验证组。

我写一个简略的例子,你们就能看明确了!

1. 先创立两个接口,代表不同的验证组

public interface AddPersonGroup {
}
public interface DeletePersonGroup {}

2. 应用验证组

@Data
public class Person {
    // 当验证组为 DeletePersonGroup 的时候 group 字段不能为空
    @NotNull(groups = DeletePersonGroup.class)
    // 当验证组为 AddPersonGroup 的时候 group 字段须要为空
    @Null(groups = AddPersonGroup.class)
    private String group;
}

@Service
@Validated
public class PersonService {@Validated(AddPersonGroup.class)
    public void validatePersonGroupForAdd(@Valid Person person) {// do something}

    @Validated(DeletePersonGroup.class)
    public void validatePersonGroupForDelete(@Valid Person person) {// do something}

}

通过测试验证:

  @Test(expected = ConstraintViolationException.class)
  public void should_check_person_with_groups() {Person person = new Person();
      person.setGroup("group1");
      service.validatePersonGroupForAdd(person);
  }

  @Test(expected = ConstraintViolationException.class)
  public void should_check_person_with_groups2() {Person person = new Person();
      service.validatePersonGroupForDelete(person);
  }

验证组应用下来的体验就是有点反模式的感觉,让代码的可维护性变差了!尽量不要应用!

罕用校验注解总结

JSR303 定义了 Bean Validation(校验)的规范 validation-api,并没有提供实现。Hibernate Validation是对这个标准 / 标准的实现 hibernate-validator,并且减少了 @Email@Length@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation

JSR 提供的校验注解:

  • @Null 被正文的元素必须为 null
  • @NotNull 被正文的元素必须不为 null
  • @AssertTrue 被正文的元素必须为 true
  • @AssertFalse 被正文的元素必须为 false
  • @Min(value) 被正文的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被正文的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被正文的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被正文的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被正文的元素的大小必须在指定的范畴内
  • @Digits (integer, fraction) 被正文的元素必须是一个数字,其值必须在可承受的范畴内
  • @Past 被正文的元素必须是一个过来的日期
  • @Future 被正文的元素必须是一个未来的日期
  • @Pattern(regex=,flag=) 被正文的元素必须合乎指定的正则表达式

Hibernate Validator 提供的校验注解

  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0
  • @Email 被正文的元素必须是电子邮箱地址
  • @Length(min=,max=) 被正文的字符串的大小必须在指定的范畴内
  • @NotEmpty 被正文的字符串的必须非空
  • @Range(min=,max=,message=) 被正文的元素必须在适合的范畴内

拓展

常常有小伙伴问到:“@NotNull@Column(nullable = false) 两者有什么区别?”

我这里简略答复一下:

  • @NotNull是 JSR 303 Bean 验证批注, 它与数据库束缚自身无关。
  • @Column(nullable = false) : 是 JPA 申明列为非空的办法。

总结来说就是即前者用于验证,而后者则用于批示数据库创立表的时候对表的束缚。

我是 Guide 哥,拥抱开源,喜爱烹饪。Github 靠近 10w 点赞的开源我的项目 JavaGuide 的作者。将来几年,心愿继续欠缺 JavaGuide,争取可能帮忙更多学习 Java 的小伙伴!共勉!凎!点击查看我的 2020 年工作汇报!

原创不易,欢送点赞分享。咱们下期再会!

👍举荐 2021 最新实战我的项目源码下载

退出移动版