1. 概要

一般に、ユーザー入力を検証する必要がある場合、SpringMVCは標準の事前定義された検証ツールを提供します。

ただし、より特定のタイプの入力を検証する必要がある場合は、 独自のカスタム検証ロジックを作成することができます。 

このチュートリアルでは、まさにそれを行います。 電話番号フィールドを使用してフォームを検証するカスタムバリデーターを作成してから、複数のフィールドのカスタムバリデーターを表示します。

このチュートリアルは、SpringMVCに焦点を当てています。 Spring Bootでの検証というタイトルの記事では、SpringBootでカスタム検証を作成する方法について説明しています。

2. 設定

APIを活用するために、pom.xmlファイルに依存関係を追加します。

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

依存関係の最新バージョンはここで確認できます。

Spring Bootを使用している場合は、 spring-boot-starter-web、のみを追加できます。これにより、hibernate-validator依存関係も導入されます。

3. カスタム検証

カスタムバリデーターを作成するには、独自のアノテーションをロールアウトし、それをモデルで使用して検証ルールを適用する必要があります。

それでは、電話番号をチェックするカスタムバリデーターを作成しましょう。 電話番号は、8桁以上、11桁以下の番号である必要があります。

4. 新しい注釈

新しい@interfaceを作成して、アノテーションを定義しましょう。

@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. 検証アノテーションの適用

この例では、検証ルールを適用するための1つのフィールドを持つ単純なクラスを作成しました。 ここでは、検証する注釈付きフィールドを設定しています。

@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. カスタムクラスレベルの検証

クラスの複数の属性を検証するために、カスタム検証アノテーションをクラスレベルで定義することもできます。

このシナリオの一般的な使用例は、クラスの2つのフィールドに一致する値があるかどうかを確認することです。

9.1. 注釈の作成

後でクラスに適用できるFieldsValueMatchという新しいアノテーションを追加しましょう。 注釈には、比較するフィールドの名前を表す fieldfieldMatch、の2つのパラメーターがあります。

@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();
    }
}

カスタムアノテーションには、クラスで複数のFieldsValueMatchアノテーションを定義するためのListサブインターフェイスも含まれていることがわかります。

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()メソッドは、2つのフィールドの値を取得し、それらが等しいかどうかを確認します。

9.3. 注釈の適用

ユーザー登録に必要なデータを対象としたNewUserFormモデルクラスを作成してみましょう。 2つのemailおよびpassword属性に加えて、2つのverifyEmailおよびverifyPassword属性を使用して2つの値を再入力します。

対応する一致フィールドと照合するフィールドが2つあるので、NewUserFormクラスに2つの@FieldsValueMatchアノテーションを追加しましょう。1つはemail 値用、もう1つはパスワード値:

@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でモデルを検証するために、@Validで注釈が付けられたNewUserFormオブジェクトを受け取り、存在するかどうかを確認する / userPOSTマッピングを使用してコントローラーを作成しましょう。検証エラー:

@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", "john@yahoo.com")
          .param("verifyEmail", "john@yahoo.com")
          .param("password", "pass")
          .param("verifyPassword", "pass"))
          .andExpect(model().errorCount(0))
          .andExpect(status().isOk());
    }
}

次に、一致しない情報を / user エンドポイントに送信し、結果に2つのエラーが含まれることを表明するJUnitテストも追加します。

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

10. 概要

この短い記事では、フィールドまたはクラスを検証するためのカスタムバリデーターを作成し、それらをSpringMVCにワイヤリングする方法を学びました。

いつものように、この記事のコードはGithubから入手できます。