表單數據校驗是系統健壯性的第一道防線。將校驗邏輯與業務代碼解耦,利用 Spring MVC + Hibernate Validator(JSR-303 實現)可實現聲明式校驗,大幅簡化 Controller 代碼。
@NotNull、@NotEmpty、@NotBlank@Min、@Max、@DecimalMin、@DecimalMax、@Digits@Pattern(正則)、@Email、@Length、@Size@Past、@Future@AssertTrue、@AssertFalse@NotNull(message = "基礎數量不能為空")
@Min(value = 0, message = "基礎數量不合法")
private Integer baseQty;
屬性為自定義對象/集合時,添加 @Valid 觸發遞歸校驗:
@Valid
@NotNull(message = "價格策略內容不能為空")
private List<ProductPricePolicyItem> policyItems;
場景:密碼/確認密碼一致、最小數量 ≤ 最大數量等多字段組合校驗。
@CrossFieldMatch@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = CrossFieldMatchValidator.class)
@Documented
public @interface CrossFieldMatch {
String message() default "{constraints.crossfieldmatch}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first(); // 第一字段名
String second(); // 第二字段名
CrossFieldOperator operator(); // 操作符:EQ, GT, GE, LT, LE
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List { CrossFieldMatch[] value(); }
}
public class CrossFieldMatchValidator
implements ConstraintValidator<CrossFieldMatch, Object> {
private String firstFieldName, secondFieldName;
private CrossFieldOperator operator;
@Override
public void initialize(CrossFieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
operator = constraintAnnotation.operator();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
Class<?> clazz = value.getClass();
Field firstField = clazz.getDeclaredField(firstFieldName);
Field secondField = clazz.getDeclaredField(secondFieldName);
firstField.setAccessible(true);
secondField.setAccessible(true);
Object firstVal = firstField.get(value);
Object secondVal = secondField.get(value);
if (firstVal == null || secondVal == null) return false;
if (!firstVal.getClass().equals(secondVal.getClass())) return false;
// 根據操作符比較(支持 Long/Integer/Short/Double)
switch (operator) {
case EQ: return firstVal.equals(secondVal);
case GT: return compare(firstVal, secondVal) > 0;
case GE: return compare(firstVal, secondVal) >= 0;
case LT: return compare(firstVal, secondVal) < 0;
case LE: return compare(firstVal, secondVal) <= 0;
}
} catch (Exception e) { /* ignore */ }
return false;
}
// compare 方法省略:按類型轉換後比較
}
@CrossFieldMatch.List({
@CrossFieldMatch(first = "minQty", second = "maxQty",
operator = CrossFieldOperator.LE,
message = "最小數量必須小於等於最大數量")
})
public class ProductPriceQtyRange implements Serializable {
@Min(value = 0, message = "最小數量不合法")
private int minQty;
@Min(value = 0, message = "最大數量不合法")
private int maxQty;
// getter/setter
}
<mvc:annotation-driven validator="validator">
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass"
value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="useCodeAsDefaultMessage" value="false"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
@PostMapping("/save")
public Result save(@Valid @RequestBody ProductPriceQtyRange dto, BindingResult result) {
if (result.hasErrors()) {
return Result.error(result.getFieldError().getDefaultMessage());
}
// 業務邏輯
}
聲明式校驗將數據完整性約束內聚到 DTO 類,業務代碼專注核心邏輯。內置注解覆蓋 80%+ 場景,跨字段校驗通過自定義 @Constraint 實現,配合 @Valid 遞歸校驗嵌套對象,構建完整校驗體系。