Java Jakarta Validation 实战指南:从参数校验到分组、自定义注解和全局异常处理

简介

Jakarta Validation 是 Java 生态里用于数据校验的标准规范。

它最常见的使用方式,就是在请求对象、实体对象、方法参数上加注解:

java 复制代码
public record CreateUserRequest(
        @NotBlank(message = "用户名不能为空")
        String username,

        @Email(message = "邮箱格式不正确")
        String email,

        @Min(value = 18, message = "年龄不能小于18岁")
        Integer age
) {
}

然后在接口入口处触发校验:

java 复制代码
@PostMapping("/users")
public Long create(@Valid @RequestBody CreateUserRequest request) {
    return userService.create(request);
}

请求参数不符合规则时,Spring 会在进入业务方法前抛出校验异常。

它的价值很直接:把参数规则写在模型上,把错误返回集中处理,让业务代码少写一堆重复的 if 判断。

Jakarta Validation、Hibernate Validator、Spring Boot 的关系

这几个名字经常一起出现,但角色不一样。

名称 角色 说明
Jakarta Validation 规范 定义注解、API、校验模型
Hibernate Validator 实现 Jakarta Validation 的常见参考实现
Spring Boot 集成方 自动配置 Validator,并在 Web、Service 等场景触发校验

简单理解:

text 复制代码
Jakarta Validation 定规则
Hibernate Validator 负责执行规则
Spring Boot 负责把校验接到 Web 请求、方法调用和异常处理流程里

包名也有一个历史变化:

时代 包名
Java EE / Bean Validation 老项目 javax.validation.*
Jakarta EE / Spring Boot 3+ jakarta.validation.*

Spring Boot 3 开始整体切到 Jakarta 命名空间,所以代码里通常使用:

java 复制代码
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

Maven 依赖

Spring Boot 项目通常直接引入 validation starter:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

它会带上 Jakarta Validation API 和 Hibernate Validator。

如果是普通 Java 项目,可以直接引入 Hibernate Validator:

xml 复制代码
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>${hibernate-validator.version}</version>
</dependency>

部分 Java SE 场景还需要表达式语言实现,用于处理更复杂的消息插值:

xml 复制代码
<dependency>
    <groupId>org.glassfish.expressly</groupId>
    <artifactId>expressly</artifactId>
    <version>${expressly.version}</version>
</dependency>

Spring Boot 项目优先交给 Boot 的依赖管理,不建议在业务项目里随意手写 Hibernate Validator 版本。

第一个 Spring Boot Demo

先准备一个创建用户请求对象。

java 复制代码
package com.example.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record CreateUserRequest(

        @NotBlank(message = "用户名不能为空")
        @Size(min = 2, max = 20, message = "用户名长度需要在2到20之间")
        String username,

        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        String email,

        @NotNull(message = "年龄不能为空")
        @Min(value = 18, message = "年龄不能小于18岁")
        @Max(value = 100, message = "年龄不能大于100岁")
        Integer age,

        @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
        String phone
) {
}

Controller:

java 复制代码
package com.example.user.controller;

import com.example.user.dto.CreateUserRequest;
import com.example.user.dto.UserCreateResponse;
import com.example.user.service.UserService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public UserCreateResponse create(@Valid @RequestBody CreateUserRequest request) {
        Long userId = userService.create(request);
        return new UserCreateResponse(userId);
    }
}

响应对象:

java 复制代码
package com.example.user.dto;

public record UserCreateResponse(Long userId) {
}

Service:

java 复制代码
package com.example.user.service;

import com.example.user.dto.CreateUserRequest;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public Long create(CreateUserRequest request) {
        return System.currentTimeMillis();
    }
}

请求参数不合法时,Controller 方法不会继续执行,Spring 会抛出绑定异常。

常用注解

常用约束可以按类型记。

注解 适用场景 说明
@NotNull 任意对象 不能为 null
@NotEmpty 字符串、集合、数组、Map 不能为 null,长度或大小不能为 0
@NotBlank 字符串 不能为 null,去掉空白后不能是空字符串
@Size 字符串、集合、数组、Map 限制长度或大小
@Min / @Max 整数、长整数等数字 限制最小值和最大值
@DecimalMin / @DecimalMax BigDecimal 等数字 适合金额、比例
@Positive 数字 需要大于 0
@PositiveOrZero 数字 需要大于等于 0
@Negative 数字 需要小于 0
@Digits 数字 限制整数位和小数位
@Email 字符串 邮箱格式
@Pattern 字符串 正则表达式
@Past 日期时间 应为过去时间
@PastOrPresent 日期时间 应为过去或当前时间
@Future 日期时间 应为未来时间
@FutureOrPresent 日期时间 应为未来或当前时间
@AssertTrue 布尔值 应为 true
@AssertFalse 布尔值 应为 false

几个注解很容易混:

注解 null 空字符串 "" 空白字符串 " "
@NotNull 不通过 通过 通过
@NotEmpty 不通过 不通过 通过
@NotBlank 不通过 不通过 不通过

用户名、标题、备注这类字符串必填字段,通常用 @NotBlank

集合必填并且至少有一个元素,通常用 @NotEmpty

数字、日期、对象必填,通常用 @NotNull

@Valid 和 @Validated 的区别

@Valid 来自 Jakarta Validation:

java 复制代码
import jakarta.validation.Valid;

@Validated 来自 Spring:

java 复制代码
import org.springframework.validation.annotation.Validated;

常见区别如下:

注解 来源 常见用途
@Valid Jakarta Validation 触发对象校验、级联校验
@Validated Spring 触发分组校验、方法参数校验

JSON 请求体校验通常这样写:

java 复制代码
@PostMapping
public UserCreateResponse create(@Valid @RequestBody CreateUserRequest request) {
    return new UserCreateResponse(1001L);
}

如果要使用分组校验,通常用 @Validated

java 复制代码
@PostMapping
public UserCreateResponse create(
        @Validated(CreateGroup.class) @RequestBody UserRequest request
) {
    return new UserCreateResponse(1001L);
}

如果要校验 @RequestParam@PathVariable 这种普通参数,Controller 类或方法通常需要加 @Validated

java 复制代码
package com.example.user.controller;

import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequestMapping("/api/users")
public class UserQueryController {

    @GetMapping("/{id}")
    public String getById(@PathVariable @Min(value = 1, message = "用户ID需要大于0") Long id) {
        return "user-" + id;
    }
}

全局异常处理

参数校验失败后,默认错误响应通常不适合直接给前端使用。

可以用 @RestControllerAdvice 统一包装错误结果。

先定义统一错误对象:

java 复制代码
package com.example.common;

import java.util.List;

public record ApiErrorResponse(
        String code,
        String message,
        List<FieldErrorItem> errors
) {
}

字段错误对象:

java 复制代码
package com.example.common;

public record FieldErrorItem(
        String field,
        String message
) {
}

全局异常处理器:

java 复制代码
package com.example.common;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;

import java.util.ArrayList;
import java.util.List;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiErrorResponse handleRequestBodyValidException(MethodArgumentNotValidException ex) {
        List<FieldErrorItem> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(this::toFieldErrorItem)
                .toList();

        return new ApiErrorResponse("VALIDATION_FAILED", "参数校验失败", errors);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiErrorResponse handleConstraintViolationException(ConstraintViolationException ex) {
        List<FieldErrorItem> errors = ex.getConstraintViolations()
                .stream()
                .map(this::toFieldErrorItem)
                .toList();

        return new ApiErrorResponse("VALIDATION_FAILED", "参数校验失败", errors);
    }

    @ExceptionHandler(HandlerMethodValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiErrorResponse handleHandlerMethodValidationException(HandlerMethodValidationException ex) {
        List<FieldErrorItem> errors = new ArrayList<>();

        ex.visitResults(new HandlerMethodValidationException.Visitor() {
            @Override
            public void other(org.springframework.validation.method.ParameterValidationResult result) {
                result.getResolvableErrors().forEach(error -> {
                    String field = result.getMethodParameter().getParameterName();
                    errors.add(new FieldErrorItem(field, error.getDefaultMessage()));
                });
            }
        });

        return new ApiErrorResponse("VALIDATION_FAILED", "参数校验失败", errors);
    }

    private FieldErrorItem toFieldErrorItem(FieldError error) {
        return new FieldErrorItem(error.getField(), error.getDefaultMessage());
    }

    private FieldErrorItem toFieldErrorItem(ConstraintViolation<?> violation) {
        return new FieldErrorItem(
                violation.getPropertyPath().toString(),
                violation.getMessage()
        );
    }
}

常见异常来源:

异常 常见来源
MethodArgumentNotValidException @RequestBody 对象校验失败
ConstraintViolationException Service 方法参数校验、部分普通参数校验
HandlerMethodValidationException Spring MVC 方法参数校验失败

不同 Spring 版本和注解位置可能抛出不同异常,统一处理时可以一起覆盖。

分组校验

新增用户和修改用户经常有不同规则。

比如新增时没有 id,修改时需要传 id

先定义两个分组接口:

java 复制代码
package com.example.user.validation;

public interface CreateGroup {
}
java 复制代码
package com.example.user.validation;

public interface UpdateGroup {
}

请求对象:

java 复制代码
package com.example.user.dto;

import com.example.user.validation.CreateGroup;
import com.example.user.validation.UpdateGroup;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record UserRequest(

        @NotNull(message = "用户ID不能为空", groups = UpdateGroup.class)
        Long id,

        @NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
        @Size(min = 2, max = 20, message = "用户名长度需要在2到20之间", groups = {CreateGroup.class, UpdateGroup.class})
        String username,

        @NotBlank(message = "邮箱不能为空", groups = CreateGroup.class)
        @Email(message = "邮箱格式不正确", groups = {CreateGroup.class, UpdateGroup.class})
        String email
) {
}

Controller:

java 复制代码
package com.example.user.controller;

import com.example.user.dto.UserRequest;
import com.example.user.validation.CreateGroup;
import com.example.user.validation.UpdateGroup;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserGroupController {

    @PostMapping
    public String create(@Validated(CreateGroup.class) @RequestBody UserRequest request) {
        return "创建成功";
    }

    @PutMapping
    public String update(@Validated(UpdateGroup.class) @RequestBody UserRequest request) {
        return "修改成功";
    }
}

分组适合规则差异比较明确的场景。

如果新增和修改字段差异很大,拆成 CreateUserRequestUpdateUserRequest 两个 DTO 通常更清楚。

级联校验

对象里嵌套对象时,需要在嵌套字段上加 @Valid

地址对象:

java 复制代码
package com.example.order.dto;

import jakarta.validation.constraints.NotBlank;

public record AddressRequest(

        @NotBlank(message = "省份不能为空")
        String province,

        @NotBlank(message = "城市不能为空")
        String city,

        @NotBlank(message = "详细地址不能为空")
        String detail
) {
}

订单对象:

java 复制代码
package com.example.order.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record CreateOrderRequest(

        @NotBlank(message = "订单号不能为空")
        String orderNo,

        @Valid
        @NotNull(message = "收货地址不能为空")
        AddressRequest address
) {
}

没有 @Valid 时,只会校验 address 是否为 null,不会继续校验 AddressRequest 里面的 provincecitydetail

集合和泛型元素校验

Jakarta Validation 支持容器元素校验。

比如订单明细列表:

java 复制代码
package com.example.order.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import java.util.List;

public record SubmitOrderRequest(

        @NotEmpty(message = "订单明细不能为空")
        List<@Valid OrderItemRequest> items,

        List<@NotNull(message = "优惠券ID不能为空") Long> couponIds
) {
}

明细对象:

java 复制代码
package com.example.order.dto;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record OrderItemRequest(

        @NotNull(message = "商品ID不能为空")
        Long skuId,

        @NotNull(message = "购买数量不能为空")
        @Positive(message = "购买数量需要大于0")
        Integer count
) {
}

几个细节:

写法 含义
@NotEmpty List<OrderItemRequest> items 列表本身不能为空,且至少有一个元素
List<@Valid OrderItemRequest> items 校验列表里每个对象
List<@NotNull Long> couponIds 校验列表里每个 ID 不能为 null

Service 方法参数校验

校验不只发生在 Controller。

Service 方法参数也可以校验,常见于内部服务、定时任务、消息消费入口。

java 复制代码
package com.example.user.service;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

@Validated
@Service
public class UserQueryService {

    public String getUsername(@Min(value = 1, message = "用户ID需要大于0") Long userId) {
        return "user-" + userId;
    }

    public void changeNickname(
            @Min(value = 1, message = "用户ID需要大于0") Long userId,
            @NotBlank(message = "昵称不能为空") String nickname
    ) {
        // 更新昵称
    }

    public void create(@Valid CreateUserCommand command) {
        // 创建用户
    }
}

命令对象:

java 复制代码
package com.example.user.service;

import jakarta.validation.constraints.NotBlank;

public record CreateUserCommand(
        @NotBlank(message = "用户名不能为空")
        String username
) {
}

Service 类上加 @Validated 后,Spring 会通过代理触发方法校验。

同一个类内部直接调用本类方法时,可能绕过代理,方法校验不会触发。需要把被校验的方法放到另一个 Spring Bean,或者通过代理对象调用。

返回值校验

方法返回值也可以加约束。

java 复制代码
package com.example.user.service;

import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

@Validated
@Service
public class UserProfileService {

    @NotNull(message = "用户资料不能为空")
    public UserProfile findProfile(Long userId) {
        return new UserProfile(userId, "Tom");
    }
}

返回对象:

java 复制代码
package com.example.user.service;

public record UserProfile(
        Long userId,
        String username
) {
}

返回值校验更适合内部服务契约,不适合滥用。很多业务场景下,返回为空本身可能就是合法结果。

自定义校验注解

内置注解覆盖不了所有业务规则。

比如手机号格式,可以封装成一个注解。

先定义注解:

java 复制代码
package com.example.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {

    String message() default "手机号格式不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

再写校验器:

java 复制代码
package com.example.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.regex.Pattern;

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isBlank()) {
            return true;
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

使用:

java 复制代码
package com.example.user.dto;

import com.example.validation.PhoneNumber;
import jakarta.validation.constraints.NotBlank;

public record BindPhoneRequest(

        @NotBlank(message = "手机号不能为空")
        @PhoneNumber
        String phone
) {
}

自定义校验器里通常把 null 当作通过。

原因是是否必填应该交给 @NotNull@NotBlank 这类注解表达。这样一个 @PhoneNumber 既可以用于必填手机号,也可以用于非必填手机号。

类级别校验

有些规则需要同时看多个字段。

比如开始时间不能晚于结束时间。

先定义注解:

java 复制代码
package com.example.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {

    String message() default "开始时间不能晚于结束时间";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

请求对象:

java 复制代码
package com.example.activity.dto;

import com.example.validation.ValidDateRange;
import jakarta.validation.constraints.FutureOrPresent;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;

@ValidDateRange
public record ActivityRequest(

        @NotNull(message = "开始时间不能为空")
        @FutureOrPresent(message = "开始时间不能早于当前时间")
        LocalDateTime startTime,

        @NotNull(message = "结束时间不能为空")
        @FutureOrPresent(message = "结束时间不能早于当前时间")
        LocalDateTime endTime
) {
}

校验器:

java 复制代码
package com.example.validation;

import com.example.activity.dto.ActivityRequest;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class DateRangeValidator implements ConstraintValidator<ValidDateRange, ActivityRequest> {

    @Override
    public boolean isValid(ActivityRequest value, ConstraintValidatorContext context) {
        if (value == null || value.startTime() == null || value.endTime() == null) {
            return true;
        }
        return !value.startTime().isAfter(value.endTime());
    }
}

类级别校验适合跨字段规则。单字段规则仍然放在字段注解上更直观。

手动校验

脱离 Spring Web 时,也可以手动使用 Validator

java 复制代码
package com.example.validation;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;

import java.util.Set;

public class ManualValidationDemo {

    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        RegisterCommand command = new RegisterCommand("", "bad-email");

        Set<ConstraintViolation<RegisterCommand>> violations = validator.validate(command);

        for (ConstraintViolation<RegisterCommand> violation : violations) {
            System.out.println(violation.getPropertyPath() + " -> " + violation.getMessage());
        }
    }
}

命令对象:

java 复制代码
package com.example.validation;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record RegisterCommand(

        @NotBlank(message = "用户名不能为空")
        String username,

        @Email(message = "邮箱格式不正确")
        String email
) {
}

输出大概是:

text 复制代码
username -> 用户名不能为空
email -> 邮箱格式不正确

错误消息和国际化

注解里的 message 可以直接写中文:

java 复制代码
@NotBlank(message = "用户名不能为空")
private String username;

也可以写消息 key:

java 复制代码
@NotBlank(message = "{user.username.notBlank}")
private String username;

然后在 src/main/resources/ValidationMessages.properties 中配置:

properties 复制代码
user.username.notBlank=用户名不能为空
user.email.invalid=邮箱格式不正确

Spring Boot 也会结合应用的 MessageSource 解析消息。项目已经有 messages.propertiesmessages_zh_CN.properties 这类国际化文件时,可以把校验文案纳入统一管理。

Fail Fast:只返回第一个错误

默认情况下,一个对象里多个字段不合法,会返回多个错误。

有些接口只想返回第一个错误,可以开启 Hibernate Validator 的 fail fast。

Spring Boot 里可以注册一个配置:

java 复制代码
package com.example.config;

import org.hibernate.validator.HibernateValidatorConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ValidationConfig {

    @Bean
    public ValidationConfigurationCustomizer validationConfigurationCustomizer() {
        return configuration -> {
            if (configuration instanceof HibernateValidatorConfiguration hibernateConfiguration) {
                hibernateConfiguration.failFast(true);
            }
        };
    }
}

多错误返回适合表单场景,一次性展示所有字段问题。

单错误返回适合移动端弹窗、命令式接口、对响应体大小比较敏感的场景。

和 JPA 实体校验的关系

Jakarta Validation 可以放在 JPA 实体上。

java 复制代码
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotBlank;

@Entity
public class UserEntity {

    @Id
    @GeneratedValue
    private Long id;

    @NotBlank(message = "用户名不能为空")
    private String username;
}

Hibernate ORM 在持久化生命周期中也可以触发校验。

不过 Web 接口里更推荐使用专门的请求 DTO:

text 复制代码
CreateUserRequest 负责接口入参
UserEntity 负责数据库映射

接口字段和数据库字段经常不完全一致。把校验规则全部堆到实体上,后期容易被不同接口场景互相影响。

常见问题

引入了注解但校验没有生效

常见原因:

原因 处理方式
缺少 spring-boot-starter-validation 添加 validation starter
@RequestBody 前没有 @Valid@Validated 在参数前添加触发注解
普通参数校验缺少 @Validated 在 Controller 或 Service 类上添加 @Validated
Service 内部调用本类方法 通过 Spring 代理调用,或拆到另一个 Bean
嵌套对象缺少 @Valid 在嵌套字段或集合元素上添加 @Valid

int 加了 @NotNull 仍然没效果

int 是基本类型,默认值是 0,不会是 null

如果要表达必填,使用包装类型:

java 复制代码
@NotNull(message = "年龄不能为空")
private Integer age;

@NotBlank 用在 Integer 上报错

@NotBlank 只能用于字符串。

数字必填用 @NotNull,数值范围用 @Min@Max@Positive 等注解。

java 复制代码
@NotNull(message = "数量不能为空")
@Positive(message = "数量需要大于0")
private Integer count;

@Valid 和 BindingResult 的位置

Spring MVC 支持在被校验参数后面紧跟 BindingResult 手动处理错误。

java 复制代码
@PostMapping
public String create(@Valid @RequestBody CreateUserRequest request,
                     BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return bindingResult.getFieldError().getDefaultMessage();
    }
    return "创建成功";
}

这种写法适合少量特殊接口。

大多数 REST API 更适合用全局异常处理器统一返回错误结构。

实践建议

场景 建议
Web JSON 入参 DTO 上写约束,Controller 参数使用 @Valid
普通参数 Controller 类或 Service 类使用 @Validated
新增和修改规则差异小 使用分组校验
新增和修改字段差异大 拆成不同请求 DTO
嵌套对象 嵌套字段上加 @Valid
集合元素 使用 List<@Valid Item>List<@NotNull Long>
业务格式规则 封装自定义注解
错误返回 使用 @RestControllerAdvice 统一处理
JPA 实体 避免把所有接口规则都压到实体上

小结

Jakarta Validation 的核心是声明式校验。

简单字段规则用内置注解,跨字段规则用类级别自定义注解,业务格式规则用自定义约束,接口错误返回交给全局异常处理器。

Spring Boot 项目中,常见组合是:

text 复制代码
spring-boot-starter-validation
DTO 约束注解
Controller @Valid / @Validated
Service @Validated
@RestControllerAdvice

这样参数校验、业务逻辑和错误返回会分得比较清楚,代码也更容易维护。

相关推荐
jvmind_dev2 小时前
第一章:AI Agent 如何重新定义 Java 性能分析
java·agent
从此以后自律3 小时前
Java 并发编程完整讲解
java·开发语言
Sylvia33.3 小时前
足球数据接口开发实战:如何用火星数据API盘活赛事应用
java·服务器·开发语言·数据库·python
小小放舟、3 小时前
PaiCLI-Demo:从零实现 ReAct Agent + Tool Call
java·后端·intellij-idea·springboot·agent·react·tool call
ch.ju4 小时前
Java程序设计(第3版)第四章——匿名内部类
java·开发语言
C++、Java和Python的菜鸟5 小时前
第2章 前端Web基础(js、vue+Ajax)
java
harmful_sheep5 小时前
idea相关设置
java·ide·intellij-idea
奋发向前wcx5 小时前
y1,y2总复习笔记2 2026.7.15
java·笔记·算法
han_hanker5 小时前
mybatis-plus 常用方法
java·tomcat·mybatis