SpringValidation數(shù)據(jù)校驗(yàn)之約束注解與分組校驗(yàn)方式
引言
數(shù)據(jù)校驗(yàn)是企業(yè)級(jí)應(yīng)用中的核心需求,它確保了業(yè)務(wù)數(shù)據(jù)的準(zhǔn)確性和一致性。
Spring Validation提供了一套強(qiáng)大而靈活的數(shù)據(jù)校驗(yàn)框架,通過(guò)聲明式的約束注解和分組校驗(yàn)機(jī)制,優(yōu)雅地實(shí)現(xiàn)了復(fù)雜的驗(yàn)證邏輯。
一、Spring Validation基礎(chǔ)架構(gòu)
1.1 JSR-380標(biāo)準(zhǔn)與Spring整合
Spring Validation以JSR-380(Bean Validation 2.0)為基礎(chǔ),通過(guò)與Hibernate Validator的無(wú)縫整合,提供了全面的數(shù)據(jù)校驗(yàn)解決方案。
JSR-380定義了標(biāo)準(zhǔn)的約束注解和驗(yàn)證API,Spring擴(kuò)展了這一標(biāo)準(zhǔn)并提供了更豐富的功能支持。
這種整合使開(kāi)發(fā)者能夠以聲明式方式定義校驗(yàn)規(guī)則,大大簡(jiǎn)化了數(shù)據(jù)驗(yàn)證的復(fù)雜性。
// Spring Validation依賴配置
/*
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
*/
// 啟用驗(yàn)證的基本配置
@Configuration
public class ValidationConfig {
@Bean
public Validator validator() {
return Validation.buildDefaultValidatorFactory().getValidator();
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}1.2 校驗(yàn)處理流程
Spring Validation的校驗(yàn)流程由多個(gè)核心組件協(xié)同完成。當(dāng)一個(gè)標(biāo)記了約束注解的對(duì)象被提交驗(yàn)證時(shí),ValidatorFactory創(chuàng)建Validator實(shí)例,然后遍歷對(duì)象的所有屬性,檢查是否滿足約束條件。
對(duì)于不滿足條件的屬性,會(huì)生成對(duì)應(yīng)的ConstraintViolation,包含違反信息和元數(shù)據(jù)。這些違反信息可以被收集并轉(zhuǎn)化為用戶友好的錯(cuò)誤消息。
// 手動(dòng)校驗(yàn)示例
@Service
public class ValidationService {
@Autowired
private Validator validator;
public <T> ValidationResult validate(T object) {
ValidationResult result = new ValidationResult();
Set<ConstraintViolation<T>> violations = validator.validate(object);
if (!violations.isEmpty()) {
result.setValid(false);
Map<String, String> errorMap = violations.stream()
.collect(Collectors.toMap(
v -> v.getPropertyPath().toString(),
ConstraintViolation::getMessage,
(msg1, msg2) -> msg1 + "; " + msg2
));
result.setErrorMessages(errorMap);
}
return result;
}
}
// 校驗(yàn)結(jié)果封裝
public class ValidationResult {
private boolean valid = true;
private Map<String, String> errorMessages = new HashMap<>();
// Getters and setters
public boolean hasErrors() {
return !valid;
}
}二、約束注解詳解
2.1 常用內(nèi)置約束注解
Spring Validation提供了豐富的內(nèi)置約束注解,覆蓋了常見(jiàn)的校驗(yàn)場(chǎng)景。這些注解可以分為幾類:基本驗(yàn)證(如@NotNull、@NotEmpty)、數(shù)字驗(yàn)證(如@Min、@Max)、字符串驗(yàn)證(如@Size、@Pattern)和時(shí)間驗(yàn)證(如@Past、@Future)等。
每個(gè)注解都可以通過(guò)message屬性自定義錯(cuò)誤消息,提高用戶體驗(yàn)。此外,大多數(shù)注解還支持通過(guò)payload屬性關(guān)聯(lián)額外的元數(shù)據(jù)。
// 內(nèi)置約束注解使用示例
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "產(chǎn)品名稱不能為空")
@Size(min = 2, max = 50, message = "產(chǎn)品名稱長(zhǎng)度必須在2-50之間")
private String name;
@NotNull(message = "價(jià)格不能為空")
@Positive(message = "價(jià)格必須是正數(shù)")
@Digits(integer = 6, fraction = 2, message = "價(jià)格格式不正確")
private BigDecimal price;
@Min(value = 0, message = "庫(kù)存不能為負(fù)數(shù)")
private Integer stock;
@NotEmpty(message = "產(chǎn)品分類不能為空")
private List<@NotBlank(message = "分類名稱不能為空") String> categories;
@Pattern(regexp = "^[A-Z]{2}\\d{6}$", message = "產(chǎn)品編碼格式不正確,應(yīng)為2個(gè)大寫(xiě)字母+6位數(shù)字")
private String productCode;
@Email(message = "聯(lián)系郵箱格式不正確")
private String contactEmail;
@Past(message = "創(chuàng)建日期必須是過(guò)去的時(shí)間")
private LocalDate createdDate;
// Getters and setters
}2.2 自定義約束注解
當(dāng)內(nèi)置約束無(wú)法滿足特定業(yè)務(wù)需求時(shí),自定義約束注解是一個(gè)強(qiáng)大的解決方案。創(chuàng)建自定義約束需要兩個(gè)核心組件:約束注解定義和約束驗(yàn)證器實(shí)現(xiàn)。注解定義聲明元數(shù)據(jù),如默認(rèn)錯(cuò)誤消息和應(yīng)用目標(biāo);驗(yàn)證器實(shí)現(xiàn)則包含實(shí)際的驗(yàn)證邏輯。通過(guò)組合現(xiàn)有約束或?qū)崿F(xiàn)全新邏輯,可以構(gòu)建出適合任何業(yè)務(wù)場(chǎng)景的驗(yàn)證規(guī)則。
// 自定義約束注解示例 - 中國(guó)手機(jī)號(hào)驗(yàn)證
@Documented
@Constraint(validatedBy = ChinesePhoneValidator.class)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ChinesePhone {
String message() default "手機(jī)號(hào)格式不正確";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 約束驗(yàn)證器實(shí)現(xiàn)
public class ChinesePhoneValidator implements ConstraintValidator<ChinesePhone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public void initialize(ChinesePhone annotation) {
// 初始化邏輯,如果需要
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 如果需要非空校驗(yàn),應(yīng)該額外使用@NotNull
}
return PHONE_PATTERN.matcher(value).matches();
}
}
// 使用自定義約束
public class User {
@NotNull(message = "姓名不能為空")
private String name;
@ChinesePhone
private String phoneNumber;
// 其他字段和方法
}三、分組校驗(yàn)深入應(yīng)用
3.1 分組校驗(yàn)基本原理
分組校驗(yàn)是Spring Validation的一個(gè)強(qiáng)大特性,允許根據(jù)不同的業(yè)務(wù)場(chǎng)景應(yīng)用不同的校驗(yàn)規(guī)則。通過(guò)定義接口作為分組標(biāo)識(shí),并在約束注解中指定所屬分組,可以實(shí)現(xiàn)精細(xì)化的驗(yàn)證控制。分組校驗(yàn)解決了一個(gè)實(shí)體類在不同操作(如新增、修改、刪除)中面臨的差異化驗(yàn)證需求,避免了代碼重復(fù)和維護(hù)困難。
// 分組校驗(yàn)的基本使用
// 定義驗(yàn)證分組
public interface Create {}
public interface Update {}
public interface Delete {}
// 使用分組約束
@Entity
public class Customer {
@NotNull(groups = {Update.class, Delete.class}, message = "ID不能為空")
@Null(groups = Create.class, message = "創(chuàng)建時(shí)不應(yīng)指定ID")
private Long id;
@NotBlank(groups = {Create.class, Update.class}, message = "名稱不能為空")
private String name;
@NotBlank(groups = Create.class, message = "創(chuàng)建時(shí)密碼不能為空")
private String password;
@Email(groups = {Create.class, Update.class}, message = "郵箱格式不正確")
private String email;
// Getters and setters
}
// 在控制器中使用分組校驗(yàn)
@RestController
@RequestMapping("/customers")
public class CustomerController {
@PostMapping
public ResponseEntity<Customer> createCustomer(
@Validated(Create.class) @RequestBody Customer customer) {
// 創(chuàng)建客戶邏輯
return ResponseEntity.ok(customerService.create(customer));
}
@PutMapping("/{id}")
public ResponseEntity<Customer> updateCustomer(
@PathVariable Long id,
@Validated(Update.class) @RequestBody Customer customer) {
// 更新客戶邏輯
return ResponseEntity.ok(customerService.update(id, customer));
}
}3.2 分組序列與順序校驗(yàn)
對(duì)于某些復(fù)雜場(chǎng)景,可能需要按特定順序執(zhí)行分組校驗(yàn),確?;掘?yàn)證通過(guò)后才進(jìn)行更復(fù)雜的驗(yàn)證。Spring Validation通過(guò)分組序列(GroupSequence)支持這一需求,開(kāi)發(fā)者可以定義驗(yàn)證組的執(zhí)行順序,一旦某個(gè)組的驗(yàn)證失敗,后續(xù)組的驗(yàn)證將被跳過(guò)。這種機(jī)制有助于提升驗(yàn)證效率,并提供更清晰的錯(cuò)誤反饋。
// 分組序列示例
// 定義基礎(chǔ)分組
public interface BasicCheck {}
public interface AdvancedCheck {}
public interface BusinessCheck {}
// 定義分組序列
@GroupSequence({BasicCheck.class, AdvancedCheck.class, BusinessCheck.class})
public interface OrderedChecks {}
// 使用分組序列
@Entity
public class Order {
@NotNull(groups = BasicCheck.class, message = "訂單號(hào)不能為空")
private String orderNumber;
@NotEmpty(groups = BasicCheck.class, message = "訂單項(xiàng)不能為空")
private List<OrderItem> items;
@Valid // 級(jí)聯(lián)驗(yàn)證
private Customer customer;
@AssertTrue(groups = AdvancedCheck.class, message = "總價(jià)必須匹配訂單項(xiàng)金額")
public boolean isPriceValid() {
if (items == null || items.isEmpty()) {
return true; // 基礎(chǔ)檢查會(huì)捕獲此問(wèn)題
}
BigDecimal calculatedTotal = items.stream()
.map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
return totalPrice.compareTo(calculatedTotal) == 0;
}
@AssertTrue(groups = BusinessCheck.class, message = "庫(kù)存不足")
public boolean isStockSufficient() {
// 庫(kù)存檢查邏輯
return inventoryService.checkStock(this);
}
// 其他字段和方法
}
// 使用分組序列驗(yàn)證
@Service
public class OrderService {
@Autowired
private Validator validator;
public ValidationResult validateOrder(Order order) {
Set<ConstraintViolation<Order>> violations =
validator.validate(order, OrderedChecks.class);
// 處理驗(yàn)證結(jié)果
return processValidationResult(violations);
}
}3.3 跨字段校驗(yàn)與類級(jí)約束
有些驗(yàn)證規(guī)則涉及多個(gè)字段的組合邏輯,如密碼與確認(rèn)密碼匹配、起始日期早于結(jié)束日期等。Spring Validation通過(guò)類級(jí)約束解決這一問(wèn)題,允許在類層面定義驗(yàn)證邏輯,處理跨字段規(guī)則。這種方式比單獨(dú)驗(yàn)證各個(gè)字段更加靈活和強(qiáng)大,特別適合復(fù)雜的業(yè)務(wù)規(guī)則。
// 自定義類級(jí)約束注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {
String message() default "結(jié)束日期必須晚于開(kāi)始日期";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String startDateField();
String endDateField();
}
// 類級(jí)約束驗(yàn)證器
public class DateRangeValidator implements ConstraintValidator<ValidDateRange, Object> {
private String startDateField;
private String endDateField;
@Override
public void initialize(ValidDateRange constraintAnnotation) {
this.startDateField = constraintAnnotation.startDateField();
this.endDateField = constraintAnnotation.endDateField();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
LocalDate startDate = (LocalDate) BeanUtils.getPropertyValue(value, startDateField);
LocalDate endDate = (LocalDate) BeanUtils.getPropertyValue(value, endDateField);
if (startDate == null || endDate == null) {
return true; // 空值驗(yàn)證交給@NotNull處理
}
return !endDate.isBefore(startDate);
} catch (Exception e) {
return false;
}
}
}
// 應(yīng)用類級(jí)約束
@ValidDateRange(
startDateField = "startDate",
endDateField = "endDate",
groups = BusinessCheck.class
)
public class EventSchedule {
@NotNull(groups = BasicCheck.class)
private String eventName;
@NotNull(groups = BasicCheck.class)
private LocalDate startDate;
@NotNull(groups = BasicCheck.class)
private LocalDate endDate;
// 其他字段和方法
}四、實(shí)踐應(yīng)用與最佳實(shí)踐
4.1 控制器參數(shù)校驗(yàn)
Spring MVC與Spring Validation的集成提供了便捷的控制器參數(shù)校驗(yàn)。通過(guò)在Controller方法參數(shù)上添加@Valid或@Validated注解,Spring會(huì)自動(dòng)對(duì)請(qǐng)求數(shù)據(jù)進(jìn)行驗(yàn)證。結(jié)合BindingResult參數(shù),可以捕獲校驗(yàn)錯(cuò)誤并進(jìn)行自定義處理。對(duì)于RESTful API,可以使用全局異常處理器統(tǒng)一處理驗(yàn)證異常,返回標(biāo)準(zhǔn)化的錯(cuò)誤響應(yīng)。
// 控制器參數(shù)校驗(yàn)示例
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// 請(qǐng)求體驗(yàn)證
@PostMapping
public ResponseEntity<?> createProduct(
@Validated(Create.class) @RequestBody Product product,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = bindingResult.getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(msg1, msg2) -> msg1 + "; " + msg2
));
return ResponseEntity.badRequest().body(errors);
}
return ResponseEntity.ok(productService.createProduct(product));
}
// 路徑變量和請(qǐng)求參數(shù)驗(yàn)證
@GetMapping("/search")
public ResponseEntity<?> searchProducts(
@RequestParam @NotBlank String category,
@RequestParam @Positive Integer minPrice,
@RequestParam @Positive Integer maxPrice) {
return ResponseEntity.ok(
productService.searchProducts(category, minPrice, maxPrice)
);
}
}
// 全局異常處理
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(msg1, msg2) -> msg1 + "; " + msg2
));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ApiError("Validation Failed", errors));
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolation(
ConstraintViolationException ex) {
Map<String, String> errors = ex.getConstraintViolations().stream()
.collect(Collectors.toMap(
violation -> violation.getPropertyPath().toString(),
ConstraintViolation::getMessage,
(msg1, msg2) -> msg1 + "; " + msg2
));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ApiError("Validation Failed", errors));
}
}總結(jié)
Spring Validation通過(guò)標(biāo)準(zhǔn)化的約束注解和靈活的分組校驗(yàn)機(jī)制,為企業(yè)級(jí)應(yīng)用提供了強(qiáng)大的數(shù)據(jù)驗(yàn)證支持。
約束注解的聲明式特性簡(jiǎn)化了驗(yàn)證代碼,而自定義約束功能滿足了各種特定業(yè)務(wù)需求。分組校驗(yàn)和分組序列解決了不同場(chǎng)景下的差異化驗(yàn)證問(wèn)題,類級(jí)約束則實(shí)現(xiàn)了復(fù)雜的跨字段驗(yàn)證邏輯。
在實(shí)際應(yīng)用中,結(jié)合控制器參數(shù)校驗(yàn)和全局異常處理,可以構(gòu)建出既健壯又易用的驗(yàn)證體系。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springcloud微服務(wù)基于redis集群的單點(diǎn)登錄實(shí)現(xiàn)解析
這篇文章主要介紹了springcloud微服務(wù)基于redis集群的單點(diǎn)登錄實(shí)現(xiàn)解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
Apache?Commons?CLI構(gòu)建命令行應(yīng)用利器教程
這篇文章主要為大家介紹了構(gòu)建命令行應(yīng)用利器Apache?Commons?CLI的使用教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
14個(gè)編寫(xiě)Spring MVC控制器的實(shí)用小技巧(吐血整理)
這篇文章主要介紹了14個(gè)編寫(xiě)Spring MVC控制器的實(shí)用小技巧(吐血整理),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Mybatis-Plus3.2.0 MetaObjectHandler 無(wú)法進(jìn)行公共字段全局填充
這篇文章主要介紹了Mybatis-Plus3.2.0 MetaObjectHandler 無(wú)法進(jìn)行公共字段全局填充,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Java中Stream流中map和forEach的區(qū)別詳解
本文主要介紹了Java中Stream流中map和forEach的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Java 根據(jù)url下載網(wǎng)絡(luò)資源
這篇文章主要介紹了Java 根據(jù)url下載網(wǎng)絡(luò)資源的示例代碼,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-11-11
spring boot中的properties參數(shù)配置詳解
這篇文章主要介紹了spring boot中的properties參數(shù)配置,需要的朋友可以參考下2017-09-09

