This article has been included in SpringBootGuide (SpringBoot2.0+ from entry to actual combat!)
- Github address: https://github.com/CodingDocs/springboot-guide
- Code cloud address: https://gitee.com/SnailClimb/springboot-guide (Github is not accessible or the access speed is relatively slow, and the corresponding content on the code cloud can be viewed)
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 stringstring$
: 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。