Hibernate Validator を使ったメソッド引数のチェック - AspectJ による組み込み

JSR 303 - Bean Validation のリファレンス実装である Hibernate Validator と AspectJ を使ってメソッドの引数をチェックする処理を実装してみました。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20120128/

はじめに

JSR 303 - Bean Validation はフィールドやプロパティ等への制約をアノテーション(@Size や @Max 等)で付与するための仕様です。

Bean Validation 1.0 にメソッドレベルの検証機能 *1 は含まれていませんが、Hibernate Validator には Bean Validation 1.0 仕様書の 「appendix C. Proposal for method-level validation」 に近い形でメソッドレベルの検証機能が用意されているので、今回はこの機能を使ってみました。

なお、フレームワーク等の助けを借りずに Hibernate Validator を単体で使う場合、メソッド引数をチェックする処理(以下のメソッド)を自前で呼び出す事になりますが、チェック対象毎に呼び出し処理を実装するのは実用的では無いので、今回は AspectJ を使ってコンパイル時に組み込むようにしました。

メソッド引数をチェックするためのメソッド
public interface MethodValidator {
    ・・・
    <T> Set<MethodConstraintViolation<T>> validateAllParameters(T object, Method method, Object[] parameterValues, Class<?>... groups);
    ・・・
}

メソッド引数の制約定義とチェック方法

Bean Validation のアノテーションを使って引数に以下のような制約を指定してみます。

  • name は null では無く、0 〜 5文字
  • point >= 3
引数に制約を指定したメソッド例
public void test(
    @NotNull @Size(max = 5) String name,
    @Min(3) int point) {
    ・・・
}

ここで、@Size は対象の値が null では無いときに適用される点にご注意ください。
つまり、name に @NotNull を付けなかった場合、null が入力されると @Size による文字数のチェックは適用されません。


また、Hibernate Validator でメソッド引数をチェックする基本的な処理手順は以下のようになります。

  1. ValidatorFactory 取得
  2. ValidatorFactory から Validator 取得
  3. Validator を unwrap() してメソッド検証用の MethodValidator を取得
  4. MethodValidator の validateAllParameters() にチェック対象のオブジェクト・メソッド・引数を渡してチェックを実施

チェックの結果、問題があれば MethodConstraintViolation として返されます。(何も無ければ戻り値は空)

メソッド引数チェック例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

// MethodValidor の取得
MethodValidator mvalidator = validator.unwrap(MethodValidator.class);
・・・
//メソッド引数のチェック
Set<MethodConstraintViolation<Object>> violations = mvalidator.validateAllParameters(・・・);
・・・

アスペクト定義クラス

まずは汎用性を考慮して以下のようなマーキング用のアノテーションを用意しました。

ValidMethod.java
・・・
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

//AOPの適用対象マーキング用アノテーション
@Target(ElementType.METHOD)
public @interface ValidMethod {
}

次に @ValidMethod アノテーションを付与したメソッドにチェック処理(checkMethod の内容)を組み込むアスペクトを定義しました。

MethodValidateAspect.java
・・・
import javax.validation.Validator;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
・・・
import org.hibernate.validator.method.MethodValidator;
import org.hibernate.validator.method.MethodConstraintViolation;

@Aspect
public class MethodValidateAspect {
	//チェック処理
    @Before("execution(@ValidMethod * *.*(..))")
    public void checkMethod(JoinPoint jp) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        // MethodValidor の取得
        MethodValidator mvalidator = validator.unwrap(MethodValidator.class);

        //java.lang.reflect.Method を取得するため MethodSignature にキャスト
        MethodSignature msig = (MethodSignature)jp.getSignature();

        //メソッド引数のチェック
        Set<MethodConstraintViolation<Object>> violations = mvalidator.validateAllParameters(jp.getThis(), msig.getMethod(), jp.getArgs());

        //チェック結果出力
        for (MethodConstraintViolation<Object> vi : violations) {
            System.out.printf("*** invalid arg : %s, %s\n", vi.getParameterName(), vi.getMessage());
        }
    }
}

実行クラス

チェック対象のクラスと動作確認用の実行クラスを以下のように実装しました。MethodValidateAspect が適用されるように test メソッドに @ValidMethod を付与しています。

DataTester.java
package fits.sample;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class DataTester {
    @ValidMethod
    public void test(
        @NotNull @Size(max = 5) String name,
        @Min(3) int point) {

        System.out.printf("%s, %d\n", name, point);
    }
}
App.java (実行クラス)
package fits.sample;

public class App {
    public static void main(String[] args) throws Exception {
        DataTester tester = new DataTester();

        System.out.println("------ check : test, 6");
        tester.test("test", 6);

        System.out.println("------ check : test123, 2");
        tester.test("test123", 2);

        System.out.println("------ check : null, 3");
        tester.test(null, 3);

        System.out.println("------ check : テスト, 10");
        tester.test("テスト", 10);
    }
}

ビルドと実行

Maven でビルド・実行するために以下のような pom.xml ファイルを用意しました。

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  ・・・
  <build>
    <plugins>
      <!-- App を実行するための設定 -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <configuration>
          <executable>java</executable>
          <mainClass>fits.sample.App</mainClass>
        </configuration>
      </plugin>
      <!-- コンパイル時に AspectJ を適用するための設定 -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <complianceLevel>1.6</complianceLevel>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- Hibernate Validator の設定 -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>4.2.0.Final</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <version>1.6.4</version>
    </dependency>
    <!-- AspectJ の設定 -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.6.11</version>
    </dependency>
  </dependencies>
</project>

実行するとチェック処理が機能している事を確認できます。(*** invalid arg の行がチェックに引っかかった引数)

実行結果
> mvn compile exec:java
・・・
------ check : test, 6
test, 6
------ check : test123, 2
*** invalid arg : arg1, must be greater than or equal to 3
*** invalid arg : arg0, size must be between 0 and 5
test123, 2
------ check : null, 3
*** invalid arg : arg0, may not be null
null, 3
------ check : テスト, 10
テスト, 10
・・・

*1:メソッド引数や戻り値のチェック等