Spring Boot参数校验实战:Bean Validation与统一异常处理

后端接口接收的参数不校验,脏数据直接进业务逻辑,轻则报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/

(0)
小编小编
上一篇 4小时前
下一篇 4小时前

相关推荐