spring boot 数据校验validation
背景
在日常的开发中,后端经常需要都请求参数进行校验。比如注册用户时,用户名不能为空,密码长度要在6-16之间,邮箱必须符合格式
等等。如果不做校验,脏数据就可能进入数据库,造成业务问题;如果校验方式不合理。代码会变得臃肿
下面将介绍一下Spring Boot
提供的 Validation
(基于 JSR 303/380 规范)让我们能通过注解的方式优雅地完成参数校验,极大地提升了开发效率和代码可读性
没有使用 Validation 的传统写法
场景:创建用户接口
-
定义接受参数对象UserDto
// UserDTO实体类 class UserDto { private String name; private Integer age; private String email; // getter和setter省略 }
-
要求:用户名不能为空,长度5-10;邮箱格式必须正确;年龄在18-60之间
@RestController @RequestMapping("/user") public class UserController { @PostMapping("/add") public String addUser(UserDto user) { // 手动校验参数 if (user.getName() == null || user.getName().trim().isEmpty()) { return "用户名不能为空"; } if (user.getName().length() < 5 || user.getName().length() > 10) { return "用户名长度必须在5-10之间"; } if (user.getAge() == null) { return "年龄不能为空"; } if (user.getAge() < 18 || user.getAge() > 60) { return "年龄必须在18-60之间"; } if (user.getEmail() == null || user.getEmail().trim().isEmpty()) { return "邮箱不能为空"; } if (!user.getEmail().matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) { return "邮箱格式不正确"; } // 业务逻辑处理 return "用户添加成功"; } }
-
总结
- 代码冗长,不利于维护
- 每个接口都要写重复的校验逻辑
- 校验逻辑和业务逻辑耦合,不够优雅
使用 Validation 的优雅写法
可以在实体类上加注解,把校验规则声明在模型上,让 Spring 自动完成校验
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
UserDto对象加上注解
import javax.validation.constraints.*;
public class UserDto {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 10, message = "用户名长度必须在{min}-{max}之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确") // 自带邮箱格式校验,无需自己写正则!
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄最小为{value}")
@Max(value = 150, message = "年龄最大为{value}")
private Integer age;
// 省略 Getter 和 Setter...
}
在Controller参数前加@Valid或@Validated注解
import javax.validation.Valid;
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/add")
// 关键一步:在 @RequestBody 前加上 @Valid 注解
public String addUser(@Valid @RequestBody UserDto user) {
// 只需关注核心业务
System.out.println("用户创建成功: " + user);
return "success";
}
}
通过上述使用 validation 改造,Spring 会自动对 UserDto 的字段进行校验,当请求参数不满足规则时,Spring Boot 会自动抛出 MethodArgumentNotValidException 异常,不会进入这个方法体。但我们不能直接给用户返回异常栈,需要统一处理
全局异常处理(友好返回错误信息)
刚才我们已经说过了参数校验不满足规则,系统会抛出MethodArgumentNotValidException
,那么我们就可以通过 @RestControllerAdvice
捕获 MethodArgumentNotValidException
,来实现统一返回错误信息
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理实体校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, Object> handleValidException(MethodArgumentNotValidException e) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("code", 400);
errorResult.put("message", "参数校验失败");
// 从异常对象中拿到具体的错误信息
// 这里只取第一个错误信息,也可以全部返回
String defaultMessage = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
errorResult.put("data", defaultMessage);
return errorResult;
}
}
常用注解
注解 | 功能说明 | |
---|---|---|
@NotNull | 值不能为null | |
@NotBlank | 字符串不能为空(trim后长度>0) | |
@NotEmpty | 集合、数组、Map、String不能为空 | |
@Size(min=, max=) | 检查字符串、集合、数组大小 | |
@Min(value) | 数字最小值 | |
@Max(value) | 数字最大值 | |
校验邮箱格式 | ||
@Pattern(regexp=) | 正则表达式匹配 | |
@Positive | 正数 | |
@Future | 日期必须在未来 | |
@Past | 日期必须在过去 |
分组校验
当同一个实体类在不同场景下有不同的校验规则时,比如新增时ID应为空,而更新时ID不能为空,这时就需要分组校验
定义分组接口(标记接口)
public interface CreateGroup {} // 创建分组
public interface UpdateGroup {} // 更新分组
在实体上指定分组
继续改造一下我们的UserDto,这时候需要增加id字段
public class UserDto {
@Null(groups = CreateGroup.class, message = "创建时ID必须为空")
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
private String username;
// ... 其他字段
}
在Controller中使用@Validated指定分组
@PostMapping("/create")
public String create(@Validated(CreateGroup.class) @RequestBody UserDto user) {
// ... 创建逻辑
}
@PostMapping("/update")
public String update(@Validated(UpdateGroup.class) @RequestBody UserDto user) {
// ... 更新逻辑
}