这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战”
添加相关依赖
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-starter-web
依赖即可
但是 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 层的验证
验证请求体
验证被 @RequstBody
注解标记的方法参数。
在需要验证的参数前加上 @Valid
注解,验证失败则抛出 MethodArgumentNotValidException
异常,Spring 会将其转换成 HTTP Status 400 「错误请求」。
PersonController
@RestController
@RequestMapping("/api/person")
public class PersonController {
@PostMapping
public ResponseEntity<PersonRequest> save(@Valid @RequestBody PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
}
PersonRequest
使用注解对请求参数校验
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PersonRequest {
@NotNull(message = "id 不能为空")
private String id;
@NotNull(message = "classId 不能为空")
private String classId;
@NotNull(message = "name 不能为空")
private String name;
}
**自定义异常处理器捕获异常 **
增加对于 PersonController
的异常处理方法,捕获验证失败抛出的 MethodArgumentNotValidException
,将校验注解对应字端名和 message 信息封装返回。
@ResponseBody
@ControllerAdvice(assignableTypes = PersonController.class)
public class GlobalExceptionHandler {
// 处理 MethodArgumentNotValidException 的异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String ,String>> handleVaildationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getAllErrors().forEach(error -> {
String field = ((FieldError) error).getField();
String defaultMessage = error.getDefaultMessage();
errors.put(field, defaultMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
通过测试验证
接下来通过 MockMvc
模拟请求 Controller
的方式来验证是否生效。
@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("11111").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("classId").value("classId 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"));
}
}
Postman
验证
验证请求参数
常见的请求除了请求体外更多的是对于 @PathVariable
和 @RequestParam
标记的请求参数。
PersonController
注意:必须要在类上加上 @Validated
注解,这样 Spring 才会去校验参数
@Validated
@RestController
@RequestMapping("/api/person")
public class PersonController {
@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Max(value = 5, message = "超过 id 的范围了") @PathVariable(name = "id") Integer id) {
return ResponseEntity.ok().body(id);
}
@GetMapping("/name")
public ResponseEntity<String> getPersonByName(@Size(max = 6, message = "超过 name 的范围了") @RequestParam(name = "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_value() 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_param_value() throws Exception {
mockMvc.perform(get("/api/person/name")
.param("name", "1234567")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByName.name: 超过 name 的范围了"));
}
使用 Postman
验证
Service 中方法参数的校验
事实上请求参数的校验不仅仅可以用在 Controller
层,在 Service
依旧适用。
PersonService
同样不要忘记在类上加上 @Validated
注解
@Validated
@Service
public class PersonService {
public void validatePersonRequest(@Valid PersonRequest personRequest) {
// do something
}
public void validateId(@Max(value = 5, message = "超过 id 的范围了") Integer id) {
// do something
}
}
通过测试验证
@SpringBootTest
class PersonServiceTest {
@Autowired
PersonService personService;
@Test
void should_person_check() {
try {
PersonRequest personRequest = PersonRequest.builder().sex("22").person(Person.builder().address("aaaa").build()).build();
personService.validatePersonRequest(personRequest);
} catch (ConstraintViolationException ex) {
ex.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
}
@Test
void should_id_check() {
try {
personService.validateId(6);
} catch (ConstraintViolationException ex) {
ex.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
}
}
输出结果如下
name 不能为空
classId 不能为空
id 不能为空
超过 id 的范围了
嵌套验证
绝大部分情况请求体中都会出现实体嵌套的现象,比如:PersonRequest
中包含了 Person
类,这种情况可以使用 @Valid
注解标记 Person
属性。
PersonRequest
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PersonRequest {
@NotNull(message = "classId 不能为空")
private String classId;
@NotNull(message = "name 不能为空")
private String name;
@Valid
@NotNull(message = "person 不能为空")
private Person person;
}
Person
注意:如果没有添加 @NotNull(message = "person 不能为空")
注解,那么请求参数中没有包含 Person
不会去验证 Person
中的参数
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Person {
@NotNull(message = "id 不能为空")
private String id;
@NotNull(message = "address 不能为空")
private String address;
}
通过测试验证
@Test
public void should_check_person_value() throws Exception {
PersonRequest personRequest = PersonRequest.builder().sex("11111").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("classId").value("classId 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("person").value("person 不能为空"));
}
@Test
public void should_check_person_nested_value() throws Exception{
PersonRequest personRequest2 = PersonRequest.builder().sex("11111").person(Person.builder().address("add").build()).build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest2)))
.andExpect(MockMvcResultMatchers.jsonPath("classId").value("classId 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("$['person.id']").value("id 不能为空"));
}
注意:验证的时候有个小坑 jsonPath
会将 person.id
转换成 {"person":{"id":"id 不能为空"}}
的形式,但是实际后台异常抛出的就是{"person.id":"id 不能为空"}
所以此处要使用$['person.id']
来解析 JSON。
详细信息可以看看 github 中的介绍 github.com/json-path/J…
Postman
验证
Validator 手动验证参数
如果需要手动验证可以直接通过 Validator
工厂类获得的 Validator
示例。
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
如果在 Spring Bean 中的话,还可以通过 @Autowired
直接注入的方式。
@Autowired
Validator validate
使用:
@Test
void check_person_manually(){
PersonRequest personRequest = PersonRequest.builder().sex("22").person(Person.builder().address("aaaa").build()).build();
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Set<ConstraintViolation<PersonRequest>> validate = validatorFactory.getValidator().validate(personRequest);
validate.forEach(personRequestConstraintViolation -> {
System.out.println(personRequestConstraintViolation.getMessage());
});
}
输出结果如下:
name 不能为空
classId 不能为空
id 不能为空
自定义 Validator
自带的注解没有办法满足你的需求,可以通过自定义 Validator 实现
校验字端是否属于指定类型
当前 PersonRequest
中增加了 PhoneType
字端,PhoneType
只能是 IOS
、ANDROID
中的一个。
第一步,创建注解 PhoneType
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneTypeValidator.class)
@Documented
public @interface PhoneType {
String message() default "phoneType 不是规定类型";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第二步,实现 ConstraintValidator
接口,重写 isValid
方法
public class PhoneTypeValidator implements ConstraintValidator<PhoneType, String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
HashSet<String> phoneTypes = new HashSet<>();
phoneTypes.add("IOS");
phoneTypes.add("ANDROID");
return phoneTypes.contains(s);
}
}
使用注解标记属性
@PhoneType
private String phoneType;
通过测试验证
@Test
void check_phone_type() throws Exception {
PersonRequest personRequest = PersonRequest.builder().sex("11111").phoneType("window").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("classId").value("classId 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"))
.andExpect(MockMvcResultMatchers.jsonPath("phoneType").value("phoneType 不是规定类型"));
}
使用 Postman
验证
分组验证
对一个参数需要多种验证方式时,也可通过分配组验证达到目的。
- 创建两个接口
@GroupSequence({IGroupA.class, IGroupB.class})
public interface IGroup {
}
- 实体添加注释
@NotNull(message = "group 不能为空", groups = {AddGroup.class})
@Null(message = "group 必须为空", groups = {UpdateGroup.class})
private String group;
- 使用验证组
@Validated(AddGroup.class)
public void validateAddPerson(@Valid Person person) {
// do something
}
@Validated(UpdateGroup.class)
public void validateUpdatePerson(@Valid Person person) {
// do something
}
通过测试验证
@Test
void check_add_user() {
try {
Person person = Person.builder().address("address").build();
personService.validateAddPerson(person);
} catch (ConstraintViolationException ex) {
ex.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
}
@Test
void check_update_user() {
try {
Person person = Person.builder().address("address").group("group").build();
personService.validateUpdatePerson(person);
} catch (ConstraintViolationException ex) {
ex.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
}
注解总结
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=)
被注释的元素必须在合适的范围内