知識庫 / Spring / Spring MVC RSS 訂閱

Spring MVC 自定義驗證

Spring MVC
HongKong
5
02:36 PM · Dec 06 ,2025

1. 概述

通常,當我們需要驗證用户輸入時,Spring MVC 提供標準預定義的驗證器。

但是,當我們需要驗證更特定類型的輸入時,我們有能力創建自己的自定義驗證邏輯。

在本教程中,我們將做到這一點;我們將創建一個自定義驗證器來驗證包含電話號碼字段的表單,然後我們將展示自定義驗證器對多個字段的用法。

本教程專注於 Spring MVC。我們的文章《Spring Boot 中的驗證》描述瞭如何在 Spring Boot 中創建自定義驗證。

2. 設置

為了充分利用 API,我們將添加依賴項到我們的 <em pom.xml</em> 文件中:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>

最新版本的依賴項可以在這裏進行檢查:這裏

如果使用 Spring Boot,則添加 spring-boot-starter-validation,它也會引入 hibernate-validator 依賴項。

3. 自定義驗證

創建自定義驗證器意味着自己定義標註並將其用於模型中,以強制執行驗證規則。

讓我們創建一個 自定義驗證器,用於檢查電話號碼。電話號碼必須是一個至少包含八位數字,最多包含 eleven 位數字的數字。

4. 新註解

讓我們創建一個新的 `@interface</em/> 來定義我們的註解:

@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

使用 @Constraint 註解,我們定義了用於驗證字段的類。message() 是在用户界面上顯示的錯誤消息。最後,大部分代碼是用於符合 Spring 標準的樣板代碼。

5. 創建驗證器

現在,讓我們創建一個驗證器類,該類會強制執行我們定義的驗證規則:

public class ContactNumberValidator implements 
  ConstraintValidator<ContactNumberConstraint, String> {

    @Override
    public void initialize(ContactNumberConstraint contactNumber) {
    }

    @Override
    public boolean isValid(String contactField,
      ConstraintValidatorContext cxt) {
        return contactField != null && contactField.matches("[0-9]+")
          && (contactField.length() > 8) && (contactField.length() < 14);
    }

}

驗證類實現了 ConstraintValidator 接口,並且必須也實現 isValid 方法;在該方法中,我們定義了我們的驗證規則。

自然地,我們在這裏採用一個簡單的驗證規則,以便展示驗證器的工作原理。

ConstraintValidator 定義了為給定對象驗證給定約束的邏輯。 實現必須符合以下限制:

  • 對象必須解析為非參數化的類型
  • 對象中的泛型參數必須是未限定通配符類型

6. 應用驗證標註

在我們的例子中,我們創建了一個簡單的類,其中包含一個字段來應用驗證規則。這裏我們正在設置我們的標註字段進行驗證:

@ContactNumberConstraint
private String phone;

我們定義了一個字符串字段,並使用自定義註解 @ContactNumberConstraint 進行了標註。在我們的控制器中,我們創建了映射,並處理了任何錯誤:

@Controller
public class ValidatedPhoneController {
 
    @GetMapping("/validatePhone")
    public String loadFormPage(Model m) {
        m.addAttribute("validatedPhone", new ValidatedPhone());
        return "phoneHome";
    }
    
    @PostMapping("/addValidatePhone")
    public String submitForm(@Valid ValidatedPhone validatedPhone,
      BindingResult result, Model m) {
        if(result.hasErrors()) {
            return "phoneHome";
        }
        m.addAttribute("message", "Successfully saved phone: "
          + validatedPhone.toString());
        return "phoneHome";
    }   
}

我們定義了這個簡單的控制器,它包含一個 JSP 頁面,並使用 submitForm 方法來驗證我們的電話號碼。

7. 視圖

我們的視圖是一個基本的 JSP 頁面,包含一個表單,該表單只有一個字段。當用户提交表單時,字段將由我們的自定義驗證器進行驗證,並重定向到同一頁面,帶有成功或驗證失敗的消息:

<form:form 
  action="/${pageContext.request.contextPath}/addValidatePhone"
  modelAttribute="validatedPhone">
    <label for="phoneInput">Phone: </label>
    <form:input path="phone" id="phoneInput" />
    <form:errors path="phone" cssClass="error" />
    <input type="submit" value="Submit" />
</form:form>

8. 測試

現在讓我們測試我們的控制器,以檢查它是否返回了適當的響應和視圖:

@Test
public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){
    this.mockMvc.
      perform(get("/validatePhone")).andExpect(view().name("phoneHome"));
}

讓我們也測試一下我們的字段是否基於用户輸入進行驗證:

@Test
public void 
  givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() {
 
    this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone").
      accept(MediaType.TEXT_HTML).
      param("phoneInput", "123")).
      andExpect(model().attributeHasFieldErrorCode(
          "validatedPhone","phone","ContactNumberConstraint")).
      andExpect(view().name("phoneHome")).
      andExpect(status().isOk()).
      andDo(print());
}

在測試中,我們向用户提供輸入“123”,正如我們所期望的,一切正常,並且我們觀察到錯誤發生在客户端

9. 自定義類級別驗證

還可以定義自定義驗證註解,用於驗證類的多個屬性。

這種場景的常見用例是驗證類中兩個字段是否具有匹配的值。

9.1. 創建標註

讓我們添加一個名為 FieldsValueMatch 的新標註,該標註可稍後應用於類。該標註將具有兩個參數,fieldfieldMatch,它們分別代表用於比較的字段名稱:

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {

    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        FieldsValueMatch[] value();
    }
}

我們還可以看到我們的自定義標註也包含一個 List 子接口,用於定義類上的多個 FieldsValueMatch 標註。

9.2. 創建驗證器

接下來,我們需要添加 FieldsValueMatchValidator 類,該類將包含實際的驗證邏輯:

public class FieldsValueMatchValidator 
  implements ConstraintValidator<FieldsValueMatch, Object> {

    private String field;
    private String fieldMatch;

    public void initialize(FieldsValueMatch constraintAnnotation) {
        this.field = constraintAnnotation.field();
        this.fieldMatch = constraintAnnotation.fieldMatch();
    }

    public boolean isValid(Object value, 
      ConstraintValidatorContext context) {

        Object fieldValue = new BeanWrapperImpl(value)
          .getPropertyValue(field);
        Object fieldMatchValue = new BeanWrapperImpl(value)
          .getPropertyValue(fieldMatch);
        
        if (fieldValue != null) {
            return fieldValue.equals(fieldMatchValue);
        } else {
            return fieldMatchValue == null;
        }
    }
}

isValid() 方法檢索兩個字段的值並檢查它們是否相等。

9.3. 應用標註

讓我們創建一個名為 NewUserForm 的模型類,用於用户註冊所需的數據。它將包含兩個 emailpassword 屬性,以及兩個 verifyEmailverifyPassword 屬性,用於重新輸入這兩個值。

由於我們有兩個字段需要與對應的值進行驗證,因此我們在 NewUserForm 類上添加了兩個 @FieldsValueMatch 註解,一個用於 email 值,另一個用於 password 值:

@FieldsValueMatch.List({ 
    @FieldsValueMatch(
      field = "password", 
      fieldMatch = "verifyPassword", 
      message = "Passwords do not match!"
    ), 
    @FieldsValueMatch(
      field = "email", 
      fieldMatch = "verifyEmail", 
      message = "Email addresses do not match!"
    )
})
public class NewUserForm {
    private String email;
    private String verifyEmail;
    private String password;
    private String verifyPassword;

    // standard constructor, getters, setters
}

為了在 Spring MVC 中驗證模型,我們創建一個帶有 /user POST 映射的控制器,該控制器接收一個帶有 NewUserForm 對象的請求,並驗證是否存在任何驗證錯誤:

@Controller
public class NewUserController {

    @GetMapping("/user")
    public String loadFormPage(Model model) {
        model.addAttribute("newUserForm", new NewUserForm());
        return "userHome";
    }

    @PostMapping("/user")
    public String submitForm(@Valid NewUserForm newUserForm, 
      BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "userHome";
        }
        model.addAttribute("message", "Valid form");
        return "userHome";
    }
}

9.4. 驗證標註

為了驗證我們自定義的類級別標註,我們編寫一個 JUnit 測試,將匹配的信息發送到 /user 端點,然後驗證響應中是否包含任何錯誤:

public class ClassValidationMvcTest {
  private MockMvc mockMvc;
    
    @Before
    public void setup(){
        this.mockMvc = MockMvcBuilders
          .standaloneSetup(new NewUserController()).build();
    }
    
    @Test
    public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk() 
      throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders
          .post("/user")
          .accept(MediaType.TEXT_HTML).
          .param("email", "[email protected]")
          .param("verifyEmail", "[email protected]")
          .param("password", "pass")
          .param("verifyPassword", "pass"))
          .andExpect(model().errorCount(0))
          .andExpect(status().isOk());
    }
}

然後,我們還將添加一個使用 JUnit 的測試,該測試將發送不匹配的信息到 /user 端點,並斷言結果將包含兩個錯誤:

@Test
public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk() 
  throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders
      .post("/user")
      .accept(MediaType.TEXT_HTML)
      .param("email", "[email protected]")
      .param("verifyEmail", "[email protected]")
      .param("password", "pass")
      .param("verifyPassword", "passsss"))
      .andExpect(model().errorCount(2))
      .andExpect(status().isOk());
    }

10. 總結

在本文中,我們學習瞭如何創建自定義驗證器來驗證字段或類,並將它們連接到 Spring MVC。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.