This article was originally created by the author. If it needs to be reproduced, please apply for opening it at the famous address at the beginning of the article and public number.
springboot-guide : Spring Boot tutorials for beginners and experienced developers (maintenance in spare time is welcome).
The importance of data validation goes beyond saying that even when the front-end checks the data, we do another validation of the incoming data to prevent users from bypassing the browser and requesting some illegal data directly from the back-end through some HTTP tools.
In this paper, combined with my own practical experience in the project, it can be said that the content described in the article is very practical. Friends who do not know can learn it, and then you can practice on the project immediately.
Below I'll show you how to elegantly validate parameters in Java programs, especially in Spring.
Infrastructure Setup
Related Dependencies
If you are developing normal Java programs, you may need to rely on them as follows:
<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>
Using the Spring Boot program requires only spring-boot-starter-web, and its subdependencies contain what we need.In addition to this dependency, lombok is used in the demo below, so don't forget to add it.
<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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Entity Class
Here is the entity class used in the example.
@Data @AllArgsConstructor @NoArgsConstructor public class Person { @NotNull(message = "classId Cannot be empty") private String classId; @Size(max = 33) @NotNull(message = "name Cannot be empty") private String name; @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex Value is not in the optional range") @NotNull(message = "sex Cannot be empty") private String sex; @Email(message = "email Incorrect format") @NotNull(message = "email Cannot be empty") private String email; }
Regular expression description:
- ^string: matches strings that start with string - string$: matches a string ending with string - ^string$: exactly matches string string - ((^Man$|^Woman$|^UGM$)): Values can only be selected among the three values of Man,Woman,UGM
The following section of the verification notes refers to the following: https://www.cnkirito.moe/spring-validation/ , thank you @ Xu Jing Peak.
JSR provides verification notes:
- The @Null commented element must be null
- The @NotNull commented element must not be null
- The @AssertTrue commented element must be true
- The @AssertFalse commented element must be false
- The @Min(value) commented element must be a number whose value must be greater than or equal to the specified minimum value
- The @Max(value) commented element must be a number whose value must be less than or equal to the specified maximum value
- The commented element of @DecimalMin(value) must be a number whose value must be greater than or equal to the specified minimum value
- The commented element of @DecimalMax(value) must be a number whose value must be less than or equal to the specified maximum value
- The size of the @Size(max=, min=) commented element must be within the specified range
- The @Digits (integer, fraction) commented element must be a number whose value must be within an acceptable range
- The @Past commented element must be a past date
- The @Future commented element must be a future date
- The commented element of @Pattern(regex=,flag=) must conform to the specified regular expression
Check notes provided by Hibernate Validator:
- @NotBlank(message =) Verification string is not null and must be greater than 0 in length
- The @Email commented element must be an e-mail address
- @Length(min=,max=) The size of the commented string must be within the specified range
- The @NotEmpty commented string must be non-empty
- The @Range(min=,max=,message=) commented element must be within the appropriate range
Verify Controller input
Validate RequestBody
Controller:
We add the @Valid comment to the parameter that needs to be validated, and if the validation fails, it throws a MethodArgumentNotValidException.By default, Spring converts this exception to HTTP Status 400 (bad request).
@RestController @RequestMapping("/api") public class PersonController { @PostMapping("/person") public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) { return ResponseEntity.ok().body(person); } }
ExceptionHandler:
Custom exception handlers can help us catch exceptions and do some simple handling.Check out this article if you don't understand the following code to handle exceptions SpringBoot Handles Several Common Postures of 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); } }
Verified by test:
Below I verify the validity of the request Controller by MockMvc simulation, and of course you can also verify it by using a tool like Postman.
Let's try to get all the parameters entered correctly.
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class PersonControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test public void should_get_person_correctly() throws Exception { Person person = new Person(); person.setName("SnailClimb"); person.setSex("Man"); person.setClassId("82938390"); person.setEmail("Snailclimb@qq.com"); mockMvc.perform(post("/api/person") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(person))) .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb")) .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390")) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man")) .andExpect(MockMvcResultMatchers.jsonPath("email").value("Snailclimb@qq.com")); } }
Verify that an exception is thrown in case of an illegal parameter and can be caught correctly.
@Test public void should_check_person_value() throws Exception { Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); mockMvc.perform(post("/api/person") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(person))) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex Value is not in the optional range")) .andExpect(MockMvcResultMatchers.jsonPath("name").value("name Cannot be empty")) .andExpect(MockMvcResultMatchers.jsonPath("email").value("email Incorrect format")); }
Verify the results using Postman as follows:
Validate request parameters (Path Variables and Request Parameters)
Controller:
Make sure you don't forget to add the Validated comment to the class, which tells Spring to check the method parameters.
@RestController @RequestMapping("/api") @Validated public class PersonController { @GetMapping("/person/{id}") public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "Exceed id Scope") Integer id) { return ResponseEntity.ok().body(id); } @PutMapping("/person") public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "Exceed name Scope") 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()); }
Verified by test:
@Test public void should_check_param_value() throws Exception { mockMvc.perform(get("/api/person/6") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isBadRequest()) .andExpect(content().string("getPersonByID.id: Exceed id Scope")); } @Test public void should_check_param_value2() throws Exception { mockMvc.perform(put("/api/person") .param("name","snailclimbsnailclimb") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isBadRequest()) .andExpect(content().string("getPersonByName.name: Exceed name Scope")); }
Validate methods in Service s
We can also validate the input of any Spring component, not at the controller level, and we can do this using a combination of @Validated and @Valid comments.
Make sure you don't forget to add the Validated comment to the class, which tells Spring to check the method parameters.
@Service @Validated public class PersonService { public void validatePerson(@Valid Person person){ // do something } }
Verified by test:
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class PersonServiceTest { @Autowired private PersonService service; @Test(expected = ConstraintViolationException.class) public void should_throw_exception_when_person_is_not_valid() { Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); service.validatePerson(person); } }
Validator Programming Manual Validation of Parameters
In some scenarios we may need to check manually and get the results.
@Test public void check_person_manually() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); Set<ConstraintViolation<Person>> violations = validator.validate(person); //output: //The email format is incorrect //name cannot be empty //The sex value is not in the optional range for (ConstraintViolation<Person> constraintViolation : violations) { System.out.println(constraintViolation.getMessage()); } }
The Validator example above is obtained from the Validator factory class, and of course you can also inject it directly through @Autowired.However, if this is used in a non-Spring Component class, the Validator can only be obtained from the factory class.
@Autowired Validator validate
Customize to Validator (practical)
You can also customize the implementation notes if they do not meet your needs.
Case 1: Check whether the value of a particular field is in an optional range
For example, we now have one more requirement: Person class has one more region field, the region field can only be one of China, China-Taiwan, China-Hong Kong.
The first step is to create a note:
@Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = RegionValidator.class) @Documented public @interface Region { String message() default "Region Value is not in the optional range"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
The second step requires you to implement the ConstraintValidator interface and override the isValid method:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; 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 comment:
@Region private String region;
Case 2: Verify phone number
Verify that our phone numbers are legitimate. This can be done with regular expressions, which can be searched online and even for specific operator segments.
PhoneNumber.java
import javax.validation.Constraint; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME; @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
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber,String> { @Override public boolean isValid(String phoneField, ConstraintValidatorContext context) { if (phoneField == null) { // can be null return true; } return phoneField.matches("^1(3[0-9]|4[57]|5[0-35-9]|8[0-9]|70)\\d{8}$") && phoneField.length() > 8 && phoneField.length() < 14; } }
Okay, we can use this comment now.
@PhoneNumber(message = "phoneNumber Incorrect format") @NotNull(message = "phoneNumber Cannot be empty") private String phoneNumber;
Use validation group
In some scenarios, we need to use validation groups, which may be unclear. To put it simply, there are different validation rules for different methods of manipulating objects. Examples are as follows (This is less used for projects I'm currently experiencing, because it's more cumbersome to understand at the code level, and more cumbersome to write about).
Create two interfaces first:
public interface AddPersonGroup { } public interface DeletePersonGroup { }
We can use validation groups like this
@NotNull(groups = DeletePersonGroup.class) @Null(groups = AddPersonGroup.class) private String group;
@Service @Validated public class PersonService { public void validatePerson(@Valid Person person) { // do something } @Validated(AddPersonGroup.class) public void validatePersonGroupForAdd(@Valid Person person) { // do something } @Validated(DeletePersonGroup.class) public void validatePersonGroupForDelete(@Valid Person person) { // do something } }
Verified by test:
@Test(expected = ConstraintViolationException.class) public void should_check_person_with_groups() { Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); person.setGroup("group1"); service.validatePersonGroupForAdd(person); } @Test(expected = ConstraintViolationException.class) public void should_check_person_with_groups2() { Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); service.validatePersonGroupForDelete(person); }
Be careful when using validation groups, which are an antipattern and can cause code to become less logical.
Code address: https://github.com/Snailclimb/springboot-guide/tree/master/source-code/advanced/bean-validation-demo
@NotNull vs @Column (nullable = false) (Important)
When using JPA to manipulate data, you often encounter @Column(nullable = false) as a type of constraint, so what's the difference between it and @NotNull?It's important to know this!
- @NotNull is a JSR 303 Bean validation annotation and is independent of the database constraint itself.
- @Column(nullable = false): is a JPA declaration column method that is not empty.
In summary, the former is used for validation, while the latter is used to indicate constraints on tables when the database creates them.
TODO
- Principle Analysis
Reference resources
- https://reflectoring.io/bean-validation-with-spring-boot/
- https://www.cnkirito.moe/spring-validation//
Public Number
If you want to keep an eye on my updated articles and shared dried goods in real time, you can keep an eye on my public number.
Java Interview Strike: Java Interview Strike V2.0 PDF version derived from this document for interviews Public Number Background reply "Java interview surprise" is free to receive!
Java engineers must learn resources: some Java engineers commonly use the public number of learning resources to respond to the keyword "1" for free, no-routine access.