后端接口接收的参数不校验,脏数据直接进业务逻辑,轻则报500,重则把数据库刷坏。Spring Boot里做参数校验的标准方案是Bean Validation(JSR-380)加全局异常处理。本文覆盖从基础注解到自定义校验的完整链路,给出可直接复制的代码。
Spring Boot集成Bean Validation:基础注解使用
引入依赖后,在Controller参数对象上声明校验规则:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// 请求对象
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度需在2-20之间")
private String username;
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄最小为1")
@Max(value = 120, message = "年龄最大为120")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
// Controller中开启校验
@PostMapping("/users")
public Result<Long> create(@Validated @RequestBody CreateUserRequest req) {
return Result.ok(userService.create(req));
}
@Validated触发校验,校验失败时Spring抛出MethodArgumentNotValidException。对方法参数校验,需要在类级别加@Validated,方法参数上写@Valid。
分组校验:同一DTO复用不同规则
同一个对象在创建和更新场景规则不同(比如ID在更新时必填),用分组:
public class UserDTO {
public interface CreateGroup {}
public interface UpdateGroup {}
@Null(groups = CreateGroup.class, message = "创建时ID必须为空")
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
}
// 接口声明对应分组
@PostMapping
public Result<Long> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }
@PutMapping("/{id}")
public Result<Long> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { ... }
注意:指定分组后,未声明分组属性的校验规则(如username的@NotBlank)默认不生效,需要把通用规则也放进分组或用@GroupSequence组合,这是容易踩坑的地方。
自定义校验注解:跨字段与业务规则校验
框架自带的@Email、@Pattern不够用时(比如开始时间不能晚于结束时间),写自定义注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TimeRangeValidator.class)
public @interface TimeRange {
String message() default "开始时间不能晚于结束时间";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class TimeRangeValidator implements ConstraintValidator<TimeRange, OrderDTO> {
@Override
public boolean isValid(OrderDTO dto, ConstraintValidatorContext ctx) {
if (dto.getStartTime() == null || dto.getEndTime() == null) {
return true; // 空值交给@NotNull处理
}
return !dto.getStartTime().isAfter(dto.getEndTime());
}
}
跨字段校验放在类级别(@Constraint标注在类上),单字段的复杂业务规则可以用@AssertTrue或自定义注解在字段上。
校验异常统一处理:全局异常处理器
校验失败信息要统一格式返回前端,用@RestControllerAdvice统一拦截:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Map<String, String>> handleValid(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
for (FieldError fe : e.getBindingResult().getFieldErrors()) {
errors.put(fe.getField(), fe.getDefaultMessage());
}
return Result.fail(400, errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<String> handleConstraint(ConstraintViolationException e) {
return Result.fail(400, e.getMessage());
}
}
两类异常要覆盖:@RequestBody校验抛MethodArgumentNotValidException,方法参数校验抛ConstraintViolationException,都要处理。参数校验前置在Controller层,业务层再校验一遍(比如查库后的存在性校验),异常处理职责清晰,不会把校验逻辑散落到服务方法里。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-can-shu-jiao-yan-shi-zhan-beanvalidation-yu-tong/