<!-- Spring Boot 2.x / Spring 5.x 自動傳遞 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 非 Boot 手動添加 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- 表達式語言支持(@Pattern 等需要) -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b11</version>
</dependency>
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setProviderClass(HibernateValidator.class);
// 綁定國際化消息源(可選)
validator.setValidationMessageSource(messageSource());
return validator;
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource();
ms.setBasenames("classpath:messages/ValidationMessages"); // 國際化資源文件
ms.setDefaultEncoding("UTF-8");
ms.setUseCodeAsDefaultMessage(true);
return ms;
}
}
<mvc:annotation-driven validator="validator"/>
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list><value>classpath:messages/ValidationMessages</value></list>
</property>
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="messageSource"/>
</bean>
public class UserDTO {
private Long id;
@Max(value = 20, message = "{val.age.message}")
private Integer age;
@NotBlank(message = "{username.not.null}")
@Length(min = 3, max = 6, message = "{username.length}")
private String username;
@NotBlank(message = "{pwd.not.null}")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,10}$", message = "密碼必須6-10位字母數字組合")
private String password;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手機號格式不正確")
private String phone;
@Email(message = "{email.format.error}")
private String email;
// getter/setter 省略
}
# classpath:messages/ValidationMessages.properties
val.age.message=年齡不能超過20歲
username.not.null=用戶名不能為空
pwd.not.null=密碼不能為空
username.length=用戶名長度必須在{min}到{max}之間
email.format.error=郵箱格式不正確
errors.put(e.getField(), e.getDefaultMessage())
);
return Result.error(400, "參數校驗失敗", errors);
}
// 2. 業務邏輯
userService.register(dto);
return Result.success();
}
// 獲取錯誤詳情工具方法
private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
result.getFieldErrors().forEach(e -> map.put(e.getField(), e.getDefaultMessage()));
return map;
}
}
| 註解 | 包 | 適用類型 | 說明 |
|---|---|---|---|
@NotNull | javax.validation | 任意 | 值不為 null |
@NotBlank | org.hibernate.validator | CharSequence | 非 null、trim 後長度 > 0 (推薦字符串用此) |
@NotEmpty | org.hibernate.validator | Collection/Map/Array/CharSequence | 非 null、size > 0 |
@Size(min=, max=) | javax.validation | Collection/Map/Array/CharSequence | 大小在範圍內 |
@Length(min=, max=) | org.hibernate.validator | CharSequence | 字符長度在範圍內 |
@Min/@Max | javax.validation | 數值類型 | 最小/最大值 |
@DecimalMin/@DecimalMax | javax.validation | BigDecimal/CharSequence | 數值範圍 |
@Email | org.hibernate.validator | CharSequence | 郵箱格式 |
@Pattern(regexp=) | javax.validation | CharSequence | 正則匹配 |
@Past/@Future | javax.validation | Date/Calendar/Instant | 過去/未來日期 |
@Range(min=, max=) | org.hibernate.validator | 數值類型 | 範圍(含邊界) |
@Valid | javax.validation | 對象 | 級聯校驗嵌套對象 |
public interface UpdateGroup {}
public interface CreateGroup {}
public class UserDTO {
@NotNull(groups = UpdateGroup.class, message = "更新時 ID 必填")
private Long id;
@NotBlank(groups = CreateGroup.class, message = "創建時用戶名必填")
private String username;
}
// Controller 指定分組
@PostMapping("/create")
public Result<Void> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }
@PutMapping("/update")
public Result<Void> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { ... }
// 1. 定義註解
@Target({FIELD, METHOD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface ValidPhone {
String message() default "手機號格式錯誤";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 實現驗證器
public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && PHONE_PATTERN.matcher(value).matches();
}
}
// 3. 使用
@ValidPhone
private String phone;
errors.put(fe.getField(), fe.getDefaultMessage())
);
return Result.error(400, "參數校驗失敗", errors);
}
@ExceptionHandler(ConstraintViolationException.class) // @Validated 方法參數校驗異常
public Result<Map<String, String>> handleConstraintViolation(ConstraintViolationException e) {
Map<String, String> errors = new HashMap<>();
e.getConstraintViolations().forEach(
cv -> errors.put(cv.getPropertyPath().toString(), cv.getMessage())
);
return Result.error(400, "參數校驗失敗", errors);
}
}
最佳實踐:
1. 字符串必用 @NotBlank(@NotNull 不檢查空串,@NotEmpty 不 trim)
2. 國際化鍵值分離:message="{key}" 配合 MessageSource,支持多語言
3. 分組校驗:創建/更新場景不同字段要求用 groups 隔離
4. 級聯校驗:嵌套對象加 @Valid 遞歸驗證
5. 全局異常處理:統一返回結構,前端統一提示
6. 正則預編譯:複雜 @Pattern 考慮自定義註解內部編譯 Pattern