Spring中优雅的处理数据校验
Spring中优雅的处理数据校验
JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解。
问题
使用 spring boot 开发 web 项目时,基本上每一个接口都要做异常捕捉处理和入参校验,如果每个都硬编码在每个接口中,会导致程序中存在大量重复的代码。
反例
让我们先看一个反例
@PostMapping("/some/valid")
public ResponseEntity handleSometing(@RequestBody User user) {
try {
//校验入参
Assert.notNull(dto.getName(), "name 不能为空");
return ResponseEntity.ok(dto);
} catch (Exception e) {
//记录日志
throw new RuntimeException(e);
}
这里存在两个需要优化的点:
- 如何抽离入参校验
- 如何抽离异常捕获
如何解决问题
-
配置Spring的全局异常处理器
-
使用 validation 校验入参(@Validated 和 @Valid),@Validated 没法作用在类成员对象上,而 @Valid 可以,在实际业务中经常会遇到类成员变量为其他业务对象的情况,所以使用 @Valid 替代 @Validation
示例代码
POM
<properties>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>
</dependencies>
全局异常处理类
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* body json校验错误拦截处理
*
* @param e 错误信息集合
* @return 错误信息
*/
@ExceptionHandler (MethodArgumentNotValidException.class)
@ResponseBody
public Map<String, Object> validationBodyException(MethodArgumentNotValidException e) {
//一般项目中会有统一的返回类,此处直接用map
Map<String, Object> map = new HashMap<>();
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
log.error("捕获异常:", message);
map.put("message", message);
return map;
}
/**
* form校验异常捕获
* @param e 异常
* @return 错误信息
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public Map<String, Object> bindExceptionHandler(BindException e) {
//一般项目中会有统一的返回类,此处直接用map
Map<String, Object> map = new HashMap<>();
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
log.error("捕获异常:", message);
map.put("message", message);
return map;
}
@ExceptionHandler (RuntimeException.class)
@ResponseBody
public Map<String, Object> runtimeException(RuntimeException e) {
//一般项目中会有统一的返回类,此处直接用map
Map<String, Object> map = new HashMap<>();
String message = e.getMessage();
log.error("捕获异常:", message);
map.put("message", message);
return map;
}
}
Controller
@Slf4j
@RestController
@RequestMapping ("test")
public class TestController {
@RequestMapping("/valid")
// 主要是这里添加类@valid注解, 用于处理数据校验工作, 如果校验不通过, 则会抛出MethodArgsNotValidException异常. 这里的异常捕获在 全局状态捕获那里进行异常的处理. 所以实际上在我们的业务逻辑里面是不需要处理异常的.
public ResponseEntity<UserDTO> valid(@RequestBody @Valid UserDTO dto){
return ResponseEntity.ok(dto);
}
}
DTO
public class UserDTO {
@NotNull(message = "用户id不能为空")
private Integer id;
@NotEmpty(message = "用户名不能为空")
@Size(min = 4, max = 30, message = "用户名只能在4~30位之间")
private String name;
@NotEmpty(message = "密码不能为空")
private String password;
@Min(message = "年龄最小为18岁", value = 18)
@Max(message = "年龄最大为80岁", value = 80)
@NotNull(message = "年龄不能为空")
private int age;
@Pattern(regexp = "^1[35678]\\d{9}$", message = "手机号格式不正确")
@NotBlank(message = "手机号不能为空")
private String phone;
@NotBlank(message = "邮箱不能为空")
@Email(message = "请输入正确的邮箱")
private String email;
// 注意这个注解, 用于表示子类的值也是需要校验的.
@Valid
private InnerClass innerClass;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public InnerClass getInnerClass() {
return innerClass;
}
public void setInnerClass(InnerClass innerClass) {
this.innerClass = innerClass;
}
public static class InnerClass {
@NotBlank(message = "innerField不能为空")
private String innerField;
public String getInnerField() {
return innerField;
}
public void setInnerField(String innerField) {
this.innerField = innerField;
}
}
}