一坨一坨的 if/else 参数校验,终于被 SpringBoot 参数校验组件整干净了!

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

数据的校验的重要性就不用说了,即便在前端对数据进行校验的状况下,咱们仍是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接经过一些 HTTP 工具直接向后端请求一些违法数据。java

最普通的作法就像下面这样。咱们经过 if/else 语句对请求的每个参数一一校验。git

@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);
    }
}

这样的代码,小伙伴们在平常开发中必定很多见,不少开源项目都是这样对请求入参作校验的。github

可是,不太建议这样来写,这样的代码明显违背了 单一职责原则。大量的非业务代码混杂在业务代码中,很是难以维护,还会致使业务层代码冗杂!web

实际上,咱们是能够经过一些简单的手段对上面的代码进行改进的!这也是本文主要要介绍的内容!正则表达式

废话很少说!下面我会结合本身在项目中的实际使用经验,经过实例程序演示如何在 SpringBoot 程序中优雅地的进行参数验证(普通的 Java 程序一样适用)。spring

不了解的朋友必定要好好看一下,学完立刻就能够实践到项目上去。数据库

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

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

添加相关依赖

若是开发普通 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最新实战项目源码下载

相关文章
相关标签/搜索