首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring MVC Validator 完整實戰指南:JSR-303 標準註解、自定義驗證器、分組驗證與 AJAX 整合

     編輯:Java 相關知識

前言

資料驗證是 Web 應用安全的第一道防線。Spring MVC 整合 **Bean Validation (JSR-303/JSR-349/JSR-380)**,提供聲明式驗證、程式化驗證、分組驗證、自定義約束完整體系。本文從標準註解、自定義驗證器、控制器整合、前端 AJAX 協作四個維度系統闡述。

一、JSR-303 標準驗證註解速查表

註解適用類型語義範例
@NotNull任意不為 null@NotNull(message = "姓名必填")
@NotEmpty集合、陣列、Map、String不為 null 且 size > 0@NotEmpty List tags
@NotBlankString不為 null 且 trim 後長度 > 0@NotBlank String username
@Size集合、陣列、Map、String大小在 [min, max]@Size(min=6, max=20) String password
@Min / @Max數值類型數值範圍@Min(18) @Max(100) int age
@DecimalMin / @DecimalMaxBigDecimal、String精確數值範圍@DecimalMin("0.01") BigDecimal price
@Digits數值、String整數位、小數位限制@Digits(integer=10, fraction=2) BigDecimal amount
@PatternString正則匹配@Pattern(regexp="^1[3-9]\\d{9}$") String mobile
@EmailString郵箱格式@Email String email
@Past / @FutureDate、Calendar、Instant過去/未來時間@Future LocalDateTime expireTime
@AssertTrue / @AssertFalseBoolean必為 true/false@AssertTrue Boolean agreed
@Valid巢狀物件遞歸驗證@Valid Address address

二、實體類驗證聲明


public class UserForm {
    @NotBlank(message = "用戶名不能為空")
    @Size(min = 3, max = 20, message = "用戶名長度 3-20 位")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用戶名只能包含字母、數字、下劃線")
    private String username;

    @NotBlank(message = "密碼不能為空")
    @Size(min = 8, max = 32, message = "密碼長度 8-32 位")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$", message = "密碼需含大小寫字母和數字")
    private String password;

    @Email(message = "郵箱格式不正確")
    private String email;

    @Min(value = 18, message = "年齡不能小於 18 歲")
    @Max(value = 100, message = "年齡不能大於 100 歲")
    private Integer age;

    @Valid // 巢狀驗證
    private AddressForm address;

    // Getter/Setter...
}

三、控制器整合:@Validated + BindingResult

3.1 基礎用法


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

    @PostMapping
    public Result register(@Validated @RequestBody UserForm form, BindingResult bindingResult) {
        // 1. 有錯誤直接返回
        if (bindingResult.hasErrors()) {
            return Result.error(extractErrors(bindingResult));
        }
        
        // 2. 業務邏輯
        userService.register(form);
        return Result.success();
    }

    private Map extractErrors(BindingResult br) {
        Map errors = new LinkedHashMap<>();
        br.getFieldErrors().forEach(e -> 
            errors.put(e.getField(), e.getDefaultMessage())
        );
        return errors;
    }
}

3.2 關鍵規則

  • `@Validated` 啟用驗證(`@Valid` 亦可,但 `@Validated` 支援分組)
  • `BindingResult` **必須緊跟** 被驗證參數之後,中間不能有其他參數
  • `@RequestBody` 針對 JSON 請求體,表單提交用 `@ModelAttribute`

四、分組驗證:同一實體不同場景不同規則

4.1 定義分組介面


public interface RegisterGroup {} // 註冊場景
public interface LoginGroup {}    // 登入場景
public interface UpdateGroup {}   // 更新場景

4.2 實體標註分組


public class UserForm {
    @NotBlank(groups = {RegisterGroup.class, UpdateGroup.class})
    private String username;

    @NotBlank(groups = RegisterGroup.class)
    @Size(min = 8, groups = RegisterGroup.class)
    private String password;

    @Email(groups = {RegisterGroup.class, UpdateGroup.class})
    private String email;
}

4.3 控制器指定分組


@PostMapping("/register")
public Result register(@Validated(RegisterGroup.class) @RequestBody UserForm form, BindingResult br) { ... }

@PostMapping("/login")
public Result login(@Validated(LoginGroup.class) @RequestBody UserForm form, BindingResult br) { ... }

@PutMapping("/{id}")
public Result update(@PathVariable Long id, @Validated(UpdateGroup.class) @RequestBody UserForm form, BindingResult br) { ... }

五、自定義驗證註解與驗證器

5.1 定義註解


@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
    String message() default "手機號格式不正確";
    Class[] groups() default {};
    Class[] payload() default {};
    boolean required() default true; // 自定義屬性
}

5.2 實現驗證器


public class PhoneValidator implements ConstraintValidator {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
    private boolean required;

    @Override
    public void initialize(Phone constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) {
            return !required; // 不必填時允許為空
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

5.3 使用自定義註解


public class UserForm {
    @Phone(required = true, message = "請輸入正確的手機號")
    private String phone;
}

5.4 進階:跨欄位驗證(類級別註解)


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordMatch {
    String message() default "兩次密碼不一致";
    Class[] groups() default {};
    Class[] payload() default {};
    String first() default "password";
    String second() default "confirmPassword";
}

public class PasswordMatchValidator implements ConstraintValidator {
    private String firstField;
    private String secondField;

    @Override
    public void initialize(PasswordMatch constraintAnnotation) {
        firstField = constraintAnnotation.first();
        secondField = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(Object obj, ConstraintValidatorContext context) {
        try {
            Object first = PropertyUtils.getProperty(obj, firstField);
            Object second = PropertyUtils.getProperty(obj, secondField);
            return Objects.equals(first, second);
        } catch (Exception e) {
            return false;
        }
    }
}

// 使用:加在類上
@PasswordMatch(first = "password", second = "confirmPassword")
public class RegisterForm {
    private String password;
    private String confirmPassword;
}

六、程式化驗證:Service 層/非 Controller 場景


@Service
public class UserService {
    @Autowired
    private Validator validator; // JSR-380 Validator 介面

    public void register(UserForm form) {
        // 驗證並拋出異常
        Set> violations = validator.validate(form, RegisterGroup.class);
        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
        // 或收集錯誤
        Map errors = violations.stream()
            .collect(Collectors.toMap(
                v -> v.getPropertyPath().toString(),
                ConstraintViolation::getMessage,
                (a, b) -> a,
                LinkedHashMap::new
            ));
        // 業務邏輯...
    }
}

七、全域異常處理:統一錯誤響應格式


@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationError(MethodArgumentNotValidException e) {
        BindingResult br = e.getBindingResult();
        Map errors = br.getFieldErrors().stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage,
                (a, b) -> a,
                LinkedHashMap::new
            ));
        return Result.error("驗證失敗", errors);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public Result handleConstraintViolation(ConstraintViolationException e) {
        Map errors = e.getConstraintViolations().stream()
            .collect(Collectors.toMap(
                v -> v.getPropertyPath().toString(),
                ConstraintViolation::getMessage,
                (a, b) -> a,
                LinkedHashMap::new
            ));
        return Result.error("驗證失敗", errors);
    }
}

八、AJAX 前端整合最佳實踐

8.1 前端錯誤顯示邏輯


// axios 攔截器
axios.interceptors.response.use(
    response => response.data,
    error => {
        if (error.response?.status === 400 && error.response.data.errors) {
            const errors = error.response.data.errors; // {field: message}
            Object.entries(errors).forEach(([field, msg]) => {
                const input = document.querySelector(`[name="${field}"]`);
                if (input) {
                    showFieldError(input, msg);
                }
            });
        }
        return Promise.reject(error);
    }
);

function showFieldError(input, message) {
    clearFieldError(input);
    input.classList.add('is-invalid');
    const feedback = document.createElement('div');
    feedback.className = 'invalid-feedback';
    feedback.textContent = message;
    input.parentNode.appendChild(feedback);
}

8.2 後端響應結構標準化


public class Result {
    private int code; // 200 成功, 400 驗證失敗, 500 系統錯誤
    private String message;
    private T data;
    private Map errors; // 驗證錯誤詳情

    public static  Result success(T data) { ... }
    public static  Result error(String message, Map errors) { ... }
}

九、效能與最佳實踐總結

  1. 聲明式優於程式化:Controller 用 `@Validated`,Service 複雜邏輯用 `Validator`
  2. 分組驗證解決場景衝突:同一實體不同操作不同規則
  3. 自定義約束封裝業務規則:手機號、身份證、密碼強度、跨欄位比對
  4. 全域異常處理統一響應**:前端只需處理標準錯誤格式
  5. 驗證錯誤碼國際化**:`message = "{user.username.notblank}"` + `ValidationMessages.properties`
  6. 避免過度驗證**:Controller 層做基礎格式驗證,Service 層做業務規則驗證

總結

Spring MVC Validator 體系 = **JSR-303 標準註解 + 分組驗證 + 自定義 ConstraintValidator + 程式化 Validator + 全域異常處理**。掌握聲明式與程式化雙模式、分組與巢狀驗證、自定義約束開發、前後端錯誤協作協議,才能構建既安全又優雅的資料驗證體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved