Java Jakarta Validation 實(shí)戰(zhàn)指南
簡介
Jakarta Validation 是 Java 生態(tài)里用于數(shù)據(jù)校驗(yàn)的標(biāo)準(zhǔn)規(guī)范。
它最常見的使用方式,就是在請求對象、實(shí)體對象、方法參數(shù)上加注解:
public record CreateUserRequest(
@NotBlank(message = "用戶名不能為空")
String username,
@Email(message = "郵箱格式不正確")
String email,
@Min(value = 18, message = "年齡不能小于18歲")
Integer age
) {
}
然后在接口入口處觸發(fā)校驗(yàn):
@PostMapping("/users")
public Long create(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
請求參數(shù)不符合規(guī)則時,Spring 會在進(jìn)入業(yè)務(wù)方法前拋出校驗(yàn)異常。
它的價值很直接:把參數(shù)規(guī)則寫在模型上,把錯誤返回集中處理,讓業(yè)務(wù)代碼少寫一堆重復(fù)的 if 判斷。
Jakarta Validation、Hibernate Validator、Spring Boot 的關(guān)系
這幾個名字經(jīng)常一起出現(xiàn),但角色不一樣。
| 名稱 | 角色 | 說明 |
|---|---|---|
| Jakarta Validation | 規(guī)范 | 定義注解、API、校驗(yàn)?zāi)P?/td> |
| Hibernate Validator | 實(shí)現(xiàn) | Jakarta Validation 的常見參考實(shí)現(xiàn) |
| Spring Boot | 集成方 | 自動配置 Validator,并在 Web、Service 等場景觸發(fā)校驗(yàn) |
簡單理解:
Jakarta Validation 定規(guī)則 Hibernate Validator 負(fù)責(zé)執(zhí)行規(guī)則 Spring Boot 負(fù)責(zé)把校驗(yàn)接到 Web 請求、方法調(diào)用和異常處理流程里
包名也有一個歷史變化:
| 時代 | 包名 |
|---|---|
| Java EE / Bean Validation 老項(xiàng)目 | javax.validation.* |
| Jakarta EE / Spring Boot 3+ | jakarta.validation.* |
Spring Boot 3 開始整體切到 Jakarta 命名空間,所以代碼里通常使用:
import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank;
Maven 依賴
Spring Boot 項(xiàng)目通常直接引入 validation starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>它會帶上 Jakarta Validation API 和 Hibernate Validator。
如果是普通 Java 項(xiàng)目,可以直接引入 Hibernate Validator:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>部分 Java SE 場景還需要表達(dá)式語言實(shí)現(xiàn),用于處理更復(fù)雜的消息插值:
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
<version>${expressly.version}</version>
</dependency>Spring Boot 項(xiàng)目優(yōu)先交給 Boot 的依賴管理,不建議在業(yè)務(wù)項(xiàng)目里隨意手寫 Hibernate Validator 版本。
第一個 Spring Boot Demo
先準(zhǔn)備一個創(chuàng)建用戶請求對象。
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 = "手機(jī)號格式不正確")
String phone
) {
}
Controller:
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);
}
}
響應(yīng)對象:
package com.example.user.dto;
public record UserCreateResponse(Long userId) {
}
Service:
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();
}
}
請求參數(shù)不合法時,Controller 方法不會繼續(xù)執(zhí)行,Spring 會拋出綁定異常。
常用注解
常用約束可以按類型記。
| 注解 | 適用場景 | 說明 |
|---|---|---|
| @NotNull | 任意對象 | 不能為 null |
| @NotEmpty | 字符串、集合、數(shù)組、Map | 不能為 null,長度或大小不能為 0 |
| @NotBlank | 字符串 | 不能為 null,去掉空白后不能是空字符串 |
| @Size | 字符串、集合、數(shù)組、Map | 限制長度或大小 |
| @Min / @Max | 整數(shù)、長整數(shù)等數(shù)字 | 限制最小值和最大值 |
| @DecimalMin / @DecimalMax | BigDecimal 等數(shù)字 | 適合金額、比例 |
| @Positive | 數(shù)字 | 需要大于 0 |
| @PositiveOrZero | 數(shù)字 | 需要大于等于 0 |
| @Negative | 數(shù)字 | 需要小于 0 |
| @Digits | 數(shù)字 | 限制整數(shù)位和小數(shù)位 |
| 字符串 | 郵箱格式 | |
| @Pattern | 字符串 | 正則表達(dá)式 |
| @Past | 日期時間 | 應(yīng)為過去時間 |
| @PastOrPresent | 日期時間 | 應(yīng)為過去或當(dāng)前時間 |
| @Future | 日期時間 | 應(yīng)為未來時間 |
| @FutureOrPresent | 日期時間 | 應(yīng)為未來或當(dāng)前時間 |
| @AssertTrue | 布爾值 | 應(yīng)為 true |
| @AssertFalse | 布爾值 | 應(yīng)為 false |
幾個注解很容易混:
| 注解 | null | 空字符串 "" | 空白字符串 " " |
|---|---|---|---|
| @NotNull | 不通過 | 通過 | 通過 |
| @NotEmpty | 不通過 | 不通過 | 通過 |
| @NotBlank | 不通過 | 不通過 | 不通過 |
用戶名、標(biāo)題、備注這類字符串必填字段,通常用 @NotBlank。
集合必填并且至少有一個元素,通常用 @NotEmpty。
數(shù)字、日期、對象必填,通常用 @NotNull。
@Valid 和 @Validated 的區(qū)別
@Valid 來自 Jakarta Validation:
import jakarta.validation.Valid;
@Validated 來自 Spring:
import org.springframework.validation.annotation.Validated;
常見區(qū)別如下:
| 注解 | 來源 | 常見用途 |
|---|---|---|
| @Valid | Jakarta Validation | 觸發(fā)對象校驗(yàn)、級聯(lián)校驗(yàn) |
| @Validated | Spring | 觸發(fā)分組校驗(yàn)、方法參數(shù)校驗(yàn) |
JSON 請求體校驗(yàn)通常這樣寫:
@PostMapping
public UserCreateResponse create(@Valid @RequestBody CreateUserRequest request) {
return new UserCreateResponse(1001L);
}
如果要使用分組校驗(yàn),通常用 @Validated:
@PostMapping
public UserCreateResponse create(
@Validated(CreateGroup.class) @RequestBody UserRequest request
) {
return new UserCreateResponse(1001L);
}
如果要校驗(yàn) @RequestParam、@PathVariable 這種普通參數(shù),Controller 類或方法通常需要加 @Validated:
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;
}
}
全局異常處理
參數(shù)校驗(yàn)失敗后,默認(rèn)錯誤響應(yīng)通常不適合直接給前端使用。
可以用 @RestControllerAdvice 統(tǒng)一包裝錯誤結(jié)果。
先定義統(tǒng)一錯誤對象:
package com.example.common;
import java.util.List;
public record ApiErrorResponse(
String code,
String message,
List<FieldErrorItem> errors
) {
}
字段錯誤對象:
package com.example.common;
public record FieldErrorItem(
String field,
String message
) {
}
全局異常處理器:
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", "參數(shù)校驗(yàn)失敗", 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", "參數(shù)校驗(yàn)失敗", 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", "參數(shù)校驗(yàn)失敗", 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 對象校驗(yàn)失敗 |
| ConstraintViolationException | Service 方法參數(shù)校驗(yàn)、部分普通參數(shù)校驗(yàn) |
| HandlerMethodValidationException | Spring MVC 方法參數(shù)校驗(yàn)失敗 |
不同 Spring 版本和注解位置可能拋出不同異常,統(tǒng)一處理時可以一起覆蓋。
分組校驗(yàn)
新增用戶和修改用戶經(jīng)常有不同規(guī)則。
比如新增時沒有 id,修改時需要傳 id。
先定義兩個分組接口:
package com.example.user.validation;
public interface CreateGroup {
}
package com.example.user.validation;
public interface UpdateGroup {
}
請求對象:
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:
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 "創(chuàng)建成功";
}
@PutMapping
public String update(@Validated(UpdateGroup.class) @RequestBody UserRequest request) {
return "修改成功";
}
}
分組適合規(guī)則差異比較明確的場景。
如果新增和修改字段差異很大,拆成 CreateUserRequest 和 UpdateUserRequest 兩個 DTO 通常更清楚。
級聯(lián)校驗(yàn)
對象里嵌套對象時,需要在嵌套字段上加 @Valid。
地址對象:
package com.example.order.dto;
import jakarta.validation.constraints.NotBlank;
public record AddressRequest(
@NotBlank(message = "省份不能為空")
String province,
@NotBlank(message = "城市不能為空")
String city,
@NotBlank(message = "詳細(xì)地址不能為空")
String detail
) {
}
訂單對象:
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 時,只會校驗(yàn) address 是否為 null,不會繼續(xù)校驗(yàn) AddressRequest 里面的 province、city、detail。
集合和泛型元素校驗(yàn)
Jakarta Validation 支持容器元素校驗(yàn)。
比如訂單明細(xì)列表:
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 = "訂單明細(xì)不能為空")
List<@Valid OrderItemRequest> items,
List<@NotNull(message = "優(yōu)惠券ID不能為空") Long> couponIds
) {
}
明細(xì)對象:
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 = "購買數(shù)量不能為空")
@Positive(message = "購買數(shù)量需要大于0")
Integer count
) {
}
幾個細(xì)節(jié):
| 寫法 | 含義 |
|---|---|
| @NotEmpty List<OrderItemRequest> items | 列表本身不能為空,且至少有一個元素 |
| List<@Valid OrderItemRequest> items | 校驗(yàn)列表里每個對象 |
| List<@NotNull Long> couponIds | 校驗(yàn)列表里每個 ID 不能為 null |
Service 方法參數(shù)校驗(yàn)
校驗(yàn)不只發(fā)生在 Controller。
Service 方法參數(shù)也可以校驗(yàn),常見于內(nèi)部服務(wù)、定時任務(wù)、消息消費(fèi)入口。
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) {
// 創(chuàng)建用戶
}
}
命令對象:
package com.example.user.service;
import jakarta.validation.constraints.NotBlank;
public record CreateUserCommand(
@NotBlank(message = "用戶名不能為空")
String username
) {
}
Service 類上加 @Validated 后,Spring 會通過代理觸發(fā)方法校驗(yàn)。
同一個類內(nèi)部直接調(diào)用本類方法時,可能繞過代理,方法校驗(yàn)不會觸發(fā)。需要把被校驗(yàn)的方法放到另一個 Spring Bean,或者通過代理對象調(diào)用。
返回值校驗(yàn)
方法返回值也可以加約束。
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");
}
}
返回對象:
package com.example.user.service;
public record UserProfile(
Long userId,
String username
) {
}
返回值校驗(yàn)更適合內(nèi)部服務(wù)契約,不適合濫用。很多業(yè)務(wù)場景下,返回為空本身可能就是合法結(jié)果。
自定義校驗(yàn)注解
內(nèi)置注解覆蓋不了所有業(yè)務(wù)規(guī)則。
比如手機(jī)號格式,可以封裝成一個注解。
先定義注解:
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 "手機(jī)號格式不正確";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
再寫校驗(yàn)器:
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();
}
}
使用:
package com.example.user.dto;
import com.example.validation.PhoneNumber;
import jakarta.validation.constraints.NotBlank;
public record BindPhoneRequest(
@NotBlank(message = "手機(jī)號不能為空")
@PhoneNumber
String phone
) {
}
自定義校驗(yàn)器里通常把 null 當(dāng)作通過。
原因是是否必填應(yīng)該交給 @NotNull、@NotBlank 這類注解表達(dá)。這樣一個 @PhoneNumber 既可以用于必填手機(jī)號,也可以用于非必填手機(jī)號。
類級別校驗(yàn)
有些規(guī)則需要同時看多個字段。
比如開始時間不能晚于結(jié)束時間。
先定義注解:
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 "開始時間不能晚于結(jié)束時間";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
請求對象:
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 = "開始時間不能早于當(dāng)前時間")
LocalDateTime startTime,
@NotNull(message = "結(jié)束時間不能為空")
@FutureOrPresent(message = "結(jié)束時間不能早于當(dāng)前時間")
LocalDateTime endTime
) {
}
校驗(yàn)器:
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());
}
}
類級別校驗(yàn)適合跨字段規(guī)則。單字段規(guī)則仍然放在字段注解上更直觀。
手動校驗(yàn)
脫離 Spring Web 時,也可以手動使用 Validator。
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());
}
}
}
命令對象:
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
) {
}
輸出大概是:
username -> 用戶名不能為空
email -> 郵箱格式不正確
錯誤消息和國際化
注解里的 message 可以直接寫中文:
@NotBlank(message = "用戶名不能為空") private String username;
也可以寫消息 key:
@NotBlank(message = "{user.username.notBlank}")
private String username;
然后在 src/main/resources/ValidationMessages.properties 中配置:
user.username.notBlank=用戶名不能為空 user.email.invalid=郵箱格式不正確
Spring Boot 也會結(jié)合應(yīng)用的 MessageSource 解析消息。項(xiàng)目已經(jīng)有 messages.properties、messages_zh_CN.properties 這類國際化文件時,可以把校驗(yàn)文案納入統(tǒng)一管理。
Fail Fast:只返回第一個錯誤
默認(rèn)情況下,一個對象里多個字段不合法,會返回多個錯誤。
有些接口只想返回第一個錯誤,可以開啟 Hibernate Validator 的 fail fast。
Spring Boot 里可以注冊一個配置:
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);
}
};
}
}
多錯誤返回適合表單場景,一次性展示所有字段問題。
單錯誤返回適合移動端彈窗、命令式接口、對響應(yīng)體大小比較敏感的場景。
和 JPA 實(shí)體校驗(yàn)的關(guān)系
Jakarta Validation 可以放在 JPA 實(shí)體上。
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 在持久化生命周期中也可以觸發(fā)校驗(yàn)。
不過 Web 接口里更推薦使用專門的請求 DTO:
CreateUserRequest 負(fù)責(zé)接口入?yún)?br />UserEntity 負(fù)責(zé)數(shù)據(jù)庫映射
接口字段和數(shù)據(jù)庫字段經(jīng)常不完全一致。把校驗(yàn)規(guī)則全部堆到實(shí)體上,后期容易被不同接口場景互相影響。
常見問題
引入了注解但校驗(yàn)沒有生效
常見原因:
| 原因 | 處理方式 |
|---|---|
| 缺少 spring-boot-starter-validation | 添加 validation starter |
| @RequestBody 前沒有 @Valid 或 @Validated | 在參數(shù)前添加觸發(fā)注解 |
| 普通參數(shù)校驗(yàn)缺少 @Validated | 在 Controller 或 Service 類上添加 @Validated |
| Service 內(nèi)部調(diào)用本類方法 | 通過 Spring 代理調(diào)用,或拆到另一個 Bean |
| 嵌套對象缺少 @Valid | 在嵌套字段或集合元素上添加 @Valid |
int 加了 @NotNull 仍然沒效果
int 是基本類型,默認(rèn)值是 0,不會是 null。
如果要表達(dá)必填,使用包裝類型:
@NotNull(message = "年齡不能為空") private Integer age;
@NotBlank 用在 Integer 上報錯
@NotBlank 只能用于字符串。
數(shù)字必填用 @NotNull,數(shù)值范圍用 @Min、@Max、@Positive 等注解。
@NotNull(message = "數(shù)量不能為空") @Positive(message = "數(shù)量需要大于0") private Integer count;
@Valid 和 BindingResult 的位置
Spring MVC 支持在被校驗(yàn)參數(shù)后面緊跟 BindingResult 手動處理錯誤。
@PostMapping
public String create(@Valid @RequestBody CreateUserRequest request,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getFieldError().getDefaultMessage();
}
return "創(chuàng)建成功";
}
這種寫法適合少量特殊接口。
大多數(shù) REST API 更適合用全局異常處理器統(tǒng)一返回錯誤結(jié)構(gòu)。
實(shí)踐建議
| 場景 | 建議 |
|---|---|
| Web JSON 入?yún)?/td> | DTO 上寫約束,Controller 參數(shù)使用 @Valid |
| 普通參數(shù) | Controller 類或 Service 類使用 @Validated |
| 新增和修改規(guī)則差異小 | 使用分組校驗(yàn) |
| 新增和修改字段差異大 | 拆成不同請求 DTO |
| 嵌套對象 | 嵌套字段上加 @Valid |
| 集合元素 | 使用 List<@Valid Item> 或 List<@NotNull Long> |
| 業(yè)務(wù)格式規(guī)則 | 封裝自定義注解 |
| 錯誤返回 | 使用 @RestControllerAdvice 統(tǒng)一處理 |
| JPA 實(shí)體 | 避免把所有接口規(guī)則都壓到實(shí)體上 |
小結(jié)
Jakarta Validation 的核心是聲明式校驗(yàn)。
簡單字段規(guī)則用內(nèi)置注解,跨字段規(guī)則用類級別自定義注解,業(yè)務(wù)格式規(guī)則用自定義約束,接口錯誤返回交給全局異常處理器。
Spring Boot 項(xiàng)目中,常見組合是:
spring-boot-starter-validation DTO 約束注解 Controller @Valid / @Validated Service @Validated @RestControllerAdvice
這樣參數(shù)校驗(yàn)、業(yè)務(wù)邏輯和錯誤返回會分得比較清楚,代碼也更容易維護(hù)。
到此這篇關(guān)于Java Jakarta Validation 實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)Java Jakarta Validation 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
visual studio2022 JNI開發(fā)流程的實(shí)現(xiàn)
本文主要介紹了通過IDEA創(chuàng)建Maven項(xiàng)目并生成JNI頭文件,使用Visual Studio 2022構(gòu)建DLL,最后在Java中加載并調(diào)用C++實(shí)現(xiàn),具有一定的參考價值,感興趣的可以了解一下2025-07-07
Java設(shè)計(jì)模式之動態(tài)代理模式實(shí)例分析
這篇文章主要介紹了Java設(shè)計(jì)模式之動態(tài)代理模式,結(jié)合實(shí)例形式分析了動態(tài)代理模式的概念、功能、組成、定義與使用方法,需要的朋友可以參考下2018-04-04
springboot使用shiro-整合redis作為緩存的操作
這篇文章主要介紹了springboot使用shiro-整合redis作為緩存的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
java集合_淺談Iterable和Iterator的區(qū)別
下面小編就為大家?guī)硪黄猨ava集合_淺談Iterable和Iterator的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-09-09
Spring Boot如何優(yōu)雅的使用多線程實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot如何優(yōu)雅的使用多線程的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
Java數(shù)據(jù)結(jié)構(gòu)二叉樹難點(diǎn)解析
樹是一種重要的非線性數(shù)據(jù)結(jié)構(gòu),直觀地看,它是數(shù)據(jù)元素(在樹中稱為結(jié)點(diǎn))按分支關(guān)系組織起來的結(jié)構(gòu),很象自然界中的樹那樣。樹結(jié)構(gòu)在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機(jī)構(gòu)都可用樹形象表示2021-10-10
解決IDEA報錯java無效的目標(biāo)發(fā)行版:22
在使用IDEA編譯項(xiàng)目時,可能會遇到JDK版本不一致的錯誤,這篇文章主要介紹了解決IDEA報錯java無效的目標(biāo)發(fā)行版:22的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-10-10

