9
头图

This article has been included in SpringBootGuide (SpringBoot2.0+ from entry to actual combat!)

Needless to say, the importance of data verification, even when the front-end verifies the data, we still need to verify the data passed in to the back-end to prevent users from bypassing the browser and directly using some HTTP tools Request some illegal data directly from the backend.

The most common practice is as follows. We use the if/else statement to check each parameter of the request one by one.

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

This kind of code must be uncommon for small partners in daily development, and many open source projects verify the requested input in this way.

However, it is not recommended to write this way. Such code obviously violates the single responsibility principle . A large amount of non-business code is mixed in the business code, which is very difficult to maintain, and it will also cause the business layer code to be redundant!

In fact, we can improve the above code through some simple means! This is also the main content of this article!

Not much nonsense! Below I will combine my actual use experience in the project and demonstrate through example programs how to elegantly verify the parameters in the SpringBoot program (common Java programs are also applicable).

Friends who don’t know must take a good look, and you can practice the project as soon as you finish learning.

And, the sample project in this article uses the latest Spring Boot version 2.4.5! (As of 2021-04-21)

The source code address of the sample project: https://github.com/CodingDocs/springboot-guide/tree/master/source-code/bean-validation-demo .

Add related dependencies

If you are developing ordinary Java programs, you may need to rely on the following:

<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>

However, I believe everyone uses the Spring Boot framework for development.

Based on Spring Boot, it is relatively simple, just add spring-boot-starter-web dependency to the project, and its sub-dependency contains what we need. In addition, Lombok is also used in our sample project.

<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>

but! ! ! After Spring Boot 2.3 1, spring-boot-starter-validation is no longer included in spring-boot-starter-web , we need to add it manually!

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

Verify Controller input

Verification request body

The verification request body even verifies the method parameters marked @RequestBody

PersonController

@Valid annotations to the parameters that need to be verified. If the verification fails, it will throw MethodArgumentNotValidException . By default, Spring will convert this exception to HTTP Status 400 (Bad Request).

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

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

PersonRequest

We use verification annotations to verify the requested parameters!

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

}

Regular expression description:

  • ^string : match the string starting with string
  • string$ : match the string ending with string
  • ^string$ : Exactly match string
  • (^Man$|^Woman$|^UGM$) : The value can only be selected among the three values of Man, Woman, and UGM

GlobalExceptionHandler

Custom exception handlers can help us catch exceptions and perform some simple processing. If you don't understand the following exception handling code, you can check this article "Several Common Postures of SpringBoot Handling Exceptions" .

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

Passed test verification

Next, I use MockMvc simulate request Controller to verify whether it works. Of course, you can also verify with a tool 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 不能为空"));
    }
}

Use Postman verify

Verify request parameters

The verification request parameters (Path Variables and Request Parameters) are the verification method parameters marked @PathVariable and @RequestParam

PersonController

must not forget to add Validated annotations to the class, this parameter can tell Spring to verify the method parameters.

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

is verified by test

@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 的范围了"));
}

Use Postman verify

Verify the method in the Service

We can also verify the input of any Spring Bean, not just Controller level input. This requirement can be achieved by using @Validated and @Valid

Under normal circumstances, we are also more inclined to use this scheme in the project.

must not forget to add Validated annotations to the class, this parameter can tell Spring to verify the method parameters.

@Service
@Validated
public class PersonService {

    public void validatePersonRequest(@Valid PersonRequest personRequest) {
        // do something
    }

}

passed the test verification:

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

The output is as follows:

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

Validator programmatically verify parameters manually

In some scenarios, we may need to manually verify and obtain the verification result.

The example Validator we obtained through the Validator factory class. In addition, if it is in Spring Bean, it can also be @Autowired directly through 0608e8d40c3a54.

@Autowired
Validator validate

The specific usage is as follows:

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

The output is as follows:

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

Customize with Validator (practical)

If the built-in verification annotations cannot meet your needs, you can also customize the implementation annotations.

Case 1: Check whether the value of a specific field is in the optional range

For example, we now have an additional requirement: PersonRequest has an Region field, and the Region field can only be one of China , China-Taiwan , China-HongKong .

first step, you need to create an annotation 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 {};
}

second step, you need to implement the ConstraintValidator interface and rewrite the isValid method.

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

Now you can use this annotation:

@Region
private String region;

Passed test verification

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 值不在可选范围内"));

Use Postman verify

Case 2: Verify the phone number

Check whether our phone number is legal. This can be done through regular expressions. Related regular expressions can be searched on the Internet. You can even search for regular expressions for specific operator phone numbers.

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

Finished, we can use this annotation now.

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

Passed test verification

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 格式不正确"));

Use verification group

We basically don't use the verification group, and it is not recommended to use it in the project. It is troublesome to understand and troublesome to write. Simply understand!

The validation group is only used when we have different validation rules for different methods of object operation.

I write a simple example, you can see it!

1. Create two interfaces first, representing different verification groups

public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}

2. Use verification group

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

}

Verification by testing:

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

The experience of the verification team is a bit anti-pattern, which makes the maintainability of the code worse! Try not to use it!

Summary of common verification annotations

JSR303 defines the Bean Validation (check) standard validation-api , and does not provide an implementation. Hibernate Validation is the realization of this specification/specification hibernate-validator , and added @Email , @Length , @Range and other annotations. Spring Validation bottom layer of Hibernate Validation .

JSR provides verification notes :

  • @Null annotated element must be null
  • @NotNull annotated element must not be null
  • @AssertTrue annotated element must be true
  • @AssertFalse annotated element must be false
  • @Min(value) annotated element must be a number, and its value must be greater than or equal to the specified minimum value
  • @Max(value) annotated element must be a number, and its value must be less than or equal to the specified maximum value
  • @DecimalMin(value) annotated element must be a number, and its value must be greater than or equal to the specified minimum value
  • @DecimalMax(value) annotated element must be a number, and its value must be less than or equal to the specified maximum value
  • @Size(max=, min=) The size of the annotated element must be within the specified range
  • @Digits (integer, fraction) annotated element must be a number, and its value must be within an acceptable range
  • @Past annotated element must be a date in the past
  • @Future annotated element must be a date in the future
  • @Pattern(regex=,flag=) annotated element must conform to the specified regular expression

Hibernate Validator provides validation annotations :

  • @NotBlank(message =) verification string is not null and the length must be greater than 0
  • @Email annotated element must be an email address
  • @Length(min=,max=) The size of the commented string must be within the specified range
  • @NotEmpty commented string must be non-empty
  • @Range(min=,max=,message=) annotated element must be in the appropriate range

expand

@NotNull often ask: "What is the difference between 0608e8d40c4098 and @Column(nullable = false)

I will answer briefly here:

  • @NotNull is a JSR 303 Bean validation annotation, it has nothing to do with the database constraint itself.
  • @Column(nullable = false) : It is a method that JPA declares that the list is non-empty.

In summary, the former is used for verification, while the latter is used to indicate the constraints on the table when the database is created.

I am Guide brother, embrace open source and like cooking. Github is the author of the open source project JavaGuide , which is close to 10w likes. In the next few years, I hope to continue to improve JavaGuide, and strive to help more friends who learn Java! mutual encouragement! Hoo! Click to view my 2020 work report!

Originality is not easy, welcome to like and share. Let's meet again next time!

👍Recommend 2021 latest actual combat project source code download


JavaGuide
9k 声望1.9k 粉丝

你好,我是 Guide,开源项目 JavaGuide 作者。