はじめまして
この記事はMicroAd Advent Calendar 2017の9日目の記事です。
初 Qiita の試みということで、自分メモとしても記録として残そうと思いました。
誤った情報や脱字など、ご指摘がありましたらご教示ください。
ゴール
簡単だけど地味に使い回せそうなツールを作ってみる。独自アノテーションはその工夫の小さな一歩です。
そして少しだけ中身を知る。
サービスロジックで十分なんじゃ
そりゃそうだ
心惹かれるモノは試したくなる。なぜか @ この記号は昔から魅力的に思えます。
また、今後同じチェックロジックが必要になった時、アノテーションをつけるだけで済み、どんなチェックが行われるのかぱっと見わかりやすい。
アノテーション (@interface)
一応 java におけるアノテーションは位置づけとしてはインタフェースとされています。
リンク先の引用によると
The motivation for the introduction of annotations into the Java programming
language was the need for semantic information for a given piece of code
java プログラミングにおいて所定のコードの「意味」を扱う必要性から出てきたものと記述されています。
以下は標準 API として提供されている代表的なアノテーションのオーバーライドです。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
アノテーションについているアノテーションは
java.lang.annotation パッケージにて提供しているメタアノテーションです。
@Target
アノテーションをつける対象です。{}
で囲んで複数指定できます。
対象の種類として
- TYPE(クラス・インタフェース・アノテーション)
- FIELD(フィールド・ENUM)
- CONSTRUCTOR(コンストラクタ)
- などなど...
いくつかありますが、@Override の場合 METHOD を対象としています。
実際 @Override はメソッド以外の要素につけることができません。
@Retention
アノテーションが影響する範囲を記述しています。
Retention は RetentionPolicy をリターンし、RetentionPolicy の種類は三つのみです。
-
SOURCE : このアノテーションはコンパイル時に破棄される
-
CLASS(デフォルト) : class ファイルに記録はされるが、実行時保持はされない
-
RUNTIME : class ファイルに記録され、かつ、実行時に参照できる == 実行時 JVM にこのアノテーションの情報が読み込まれる
@Override の場合 RetentionPolicy が SOURCE なので、
コンパイル終了時は何の意味も持たない == バイトコード変換されない ことになります。
Target も Retention も、場合によっては可読性を考慮して import static
してシンプルに書いたりもします。
今回の例
今回欲しかったのは半角と全角を分けてカウントして最大長さを制限するためのアノテーションだったので、
ひな型は概ね javax.validation.constraints パッケージを参考 (@Size とか @NotNull とかありますよね)しました。
というか、今回の独自アノテーション、つまりは制約アノテーション(Constraint Annotation) には決まったひな形(message(), groups(), payload() の設定が必須)があるので、それにしたがって作成します。
名前は適当につけますね。
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CustomSizeValidator.class})
public @interface CustomSize {
String message() default "{validation.CustomSize.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int max();
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@interface List {
CustomSize[] value();
}
}
@Documented
このアノテーションを使うにあたって、javadoc などによる文章化が必要であることを示します。
正直今回は必要なかったかもしれません。
@Constraint
このアノテーションで制約(チェック)したい具体的なロジックを記述したクラスを指定します。
以下が、今回のアノテーションのバリデーション実装クラスです。
public class CustomSizeValidator implements ConstraintValidator<CustomSize, String> {
private int max;
@Override
public void initialize(CustomSize customSize) {
max = customSize.max();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// CustomSizeUtil はチェックロジックだけを記述したクラス
// getLength の戻り値は int
return CustomSizeUtil.getLength(value) <= max;
}
}
initialize はisValid
が呼ばれるための初期化処理です。
isValid は実際のバリデーションロジックを実装します。パラメーターとして渡される <T> value
が実際の検証対象のオブジェクトになります。
今回は、入力された文字列に対する長さのチェックなので、String 型の文字列がここに該当します。
不正な値の入力によりvalue
がチェックを通れなかった場合は、false がリターンされます。
message()
不正入力時に警告(?)として出したいメッセージです。
メッセージプロパティ(hibernate の ValidationMessages.properties とか)に定義する文言が入ります。
また、キーは完全修飾クラス名で記述するように、となっています。
たとえばこのような形になると思います。
validation.CustomSize.message = {0} を超えないように入力してください
groups()
特定のバリデーショングループがカスタマイズできるような設定です。
空の Class> 型で初期化されている必要があります。
グループ化とは、制約に順序を持たせるためのものです。
@GroupSequence({CheckA.class, CheckB.class}) // A のチェックの後 B
public interface GroupedChecks {
}
payload()
チェック対象のオブジェクトになんらかのメタ情報を与えるためだけの宣言です。
実際 javax.validation.Payload
インタフェースの中身は空っぽで、マーカーもしくはカテゴライズするためのものだと思います。
たとえば
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CustomSizeValidator.class})
public @interface CustomSize {
String message() default "{validation.CustomSize.message}";
Class<?>[] groups() default {};
class Text implements Payload{} // Add Payload
// omit
}
public class SampleModel {
@CustomSizeValidator(max = 20, payload = {CustomSizeValidator.Text.class})
private String memo;
// omit
}
正しい言い方なのかはいまいちですが、Text
といったカテゴライズとして識別されるよう、memo
フィールドに意味を与えることができます。
特にない場合は空のままにします。
max()
今回は「最大長さ」に制約をかけるバリデーションなので、max という名前を設定しました。
@CustomSizeValidator(max = 20) // ここに入る名前
private String memo;
List { A[] value() }
ConstraintValidator の実装に寄ってチェック可能な対象(複数可能)を定義します。
@interface List {
CustomSizeValidator value();
AnotherValidator aaa();
}
特別な理由がない限りは要素は1つで十分かもしれません。
使う
他の入力チェックと同じですが、コントローラーのパラメータとして渡される上位モデルには @Valid をつけて、すぐ隣に並べて BindingResult を定義します。
@RequestMapping(value = "/sample", method = RequestMethod.POST)
public String samplePost(@ModelAttribute @Valid BigModel model, BindingResult result) {
// omit
}
入れ子モデルの場合、上位モデルにも @Valid をつけます。
public class BigModel {
@Valid
private SmallModel smallModel;
// omit
}
今回のカスタムアノテーションは @Target({ElementType.FIELD}) なので、フィールドを対象としています。
実際チェックの対象とするフィールドにつけます。
public class SmallModel {
@CustomSize(max = 20)
private String input;
// omit
}
テスト
-
アプリ稼働して直接試すテスト (
BindingResult
にてエラーが格納される) -
テストコードでテスト(いくつか値を与えて本当にその通りに計算されるか)
決まった制限がある文字数カウントなので、境界値分析でテストしました。
アノテーション制約の実装クラスのisValid()
をテストしています。
// omit
when:
def result = validator.isValid("", null)
then:
assert result == excepted
where:
testCase | maxLen | doubleLen || excepted
"Boundary value, small" | 5 | 4.5 || true
"Boundary value, same" | 5 | 5 || true
"Boundary value, big" | 5 | 5.5 || false
おわりまして
こういうのをやってみたら、楽だったよ。くらいで終わらせるつもりでしたが、
やはり中身を知ることは非常に興味深く、とてもいい勉強のきっかけになりました。
参考
Hibernate Community Documentation
以上