JSR 380 に何度も苦しめられてきた私は、巨大な依存関係を引きずり込まないバリデーションフレームワークを高く評価しています。Solon のバリデーションシステムは solon-security-validation に含まれ — 単一のプラグインで 20 以上のアノテーション、エンティティバリデーション、カスタムバリデータを提供し、javax.validationjakarta.validation を一切引き込みません。

役に立った点を順に紹介します。

2 フェーズ・バリデーションモデル

Solon はバリデーションを 2 つのフェーズに分けています。

  1. コンテキストバリデーション(注入前) — メソッドパラメータが解決される前に実行。メソッド自体に付けたアノテーションでヘッダー、クッキー、IP、リクエストレベルの制約を検査します。
  2. パラメータバリデーション(注入後) — パラメータ解決後に実行。個別のパラメータやエンティティフィールドに付けたアノテーションで検証します。

これらは 2 つの実装メカニズムに対応します。

  • コンテキストバリデーションは内部で @Addition(Filter.class) を利用
  • パラメータバリデーションは @Around(Interceptor.class) を利用

通常はこれらを意識する必要はありませんが、一部のアノテーションがメソッドに付き、他がパラメータに付く理由を説明しています。

はじめに:コントローラでの @Valid の利用

コントローラクラス(またはその基底クラス)に @Valid を付け、パラメータにアノテーションを追加します。

@Valid
@Controller
public class UserController {

    @Mapping("/user/add")
    public void addUser(
            @NotNull String name,
            @Email String email,
            @Pattern("^https?://") String avatarUrl,
            @Length(min = 6, max = 20) String password) {
        // 全パラメータはこのメソッド実行前に検証済み
    }
}

Enter fullscreen mode Exit fullscreen mode

これだけです。 ValidatorFactory の設定や Bean の宣言は不要です。クラスに付けた @Valid により、全てのハンドラメソッドでバリデーションが有効になります。

エンティティ(ネスト)バリデーションのための @Validated

パラメータが独自のバリデーションルールを持つ DTO の場合、@Validated を使用します。

@Valid
@Controller
public class UserController {

    @Mapping("/user/register")
    public void register(@Validated RegisterRequest req) {
        // req のフィールドが検証される
    }
}

@Data
public class RegisterRequest {
    @NotNull
    @Length(min = 2, max = 50)
    private String name;

    @Email
    @NotNull
    private String email;

    @Validated  // ネストしたエンティティのバリデーション
    @NotNull
    @Size(min = 1)
    private List<Order> orderList;
}

Enter fullscreen mode Exit fullscreen mode

グループバリデーション

更新と作成のシナリオを分ける場合、グループを利用します。

public interface UpdateGroup {}

@Data
public class User {
    @NotNull(groups = UpdateGroup.class)  // 更新時のみ必須
    private Long id;

    @NotNull
    private String name;
}

// コントローラ
@Valid
@Controller
public class UserController {
    @Mapping("/user/update")
    public void update(@Validated(UpdateGroup.class) User user) {
        // UpdateGroup に属するフィールドのみ検証
    }
}

Enter fullscreen mode Exit fullscreen mode

20 以上のアノテーション一覧表

Solon のバリデーションプラグインには包括的なアノテーションセットが同梱されています。完全なリストは以下の通りです。

アノテーション 適用範囲 目的
@Valid コントローラクラス バリデーションの有効化
@Validated パラメータ/フィールド エンティティフィールドの検証
@NotNull メソッド/パラメータ/フィールド null でないこと
@Null メソッド/パラメータ/フィールド null であること
@NotBlank メソッド/パラメータ/フィールド 空白でない文字列
@NotEmpty メソッド/パラメータ/フィールド 空でない文字列
@NotZero メソッド/パラメータ/フィールド ゼロでないこと
@Min(value) パラメータ/フィールド >= value
@Max(value) パラメータ/フィールド <= value
@DecimalMin(value) パラメータ/フィールド >= 10 進数値
@DecimalMax(value) パラメータ/フィールド <= 10 進数値
@Length(min, max) パラメータ/フィールド 文字列長の範囲
@Size パラメータ/フィールド コレクションサイズの範囲
@Email パラメータ/フィールド メール形式
@Pattern(value) パラメータ/フィールド 正規表現一致
@Date パラメータ/フィールド 日付形式
@Numeric パラメータ/フィールド 数値形式
@Logined コントローラ/メソッド ログイン済み
@NoRepeatSubmit コントローラ/メソッド 重複送信の禁止
@Whitelist コントローラ/メソッド ホワイトリスト内の IP
@NotBlacklist コントローラ/メソッド ブラックリスト外の IP

このうち @Logined@NoRepeatSubmit@Whitelist@NotBlacklist は「ビジネス指向」のアノテーションで、Checker インターフェースの実装が必要です。詳細は後述します。

非 Web コンポーネントでのバリデーション

驚いた点として、Solon のバリデーションはコントローラだけでなく、通常の @Component クラスでも動作します。

@Valid
@Component
public class UserService {
    public void addUser(@NotNull String name, @Email String email) {
        // ここでもバリデーションが有効
    }
}

Enter fullscreen mode Exit fullscreen mode

ValidUtils による手動バリデーション

任意の場所でプログラム的にバリデーションを実行できます。

User user = new User();
user.setName(null);
ValidUtils.validateEntity(user);  // 無効なら ValidatorException をスロー

Enter fullscreen mode Exit fullscreen mode

バリデーションエラーの処理

フィルターで ValidatorException をキャッチします。

@Component
public class ValidationFilter implements Filter {
    @Override
    public void doFilter(Context ctx, FilterChain chain) throws Throwable {
        try {
            chain.doFilter(ctx);
        } catch (ValidatorException e) {
            ctx.render(Result.failure(e.getCode(), e.getMessage()));
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

デフォルトでは最初のエラーで検証を停止します。全てのエラーを収集したい場合は以下を設定します。

solon.validation.validateAll: true

Enter fullscreen mode Exit fullscreen mode

ビジネス指向バリデータ(4 つの Checker)

4 つのアノテーションは Checker 実装の提供を必要とします。ここで Solon の設計が光ります — フレームワークがインターセプトを処理し、ビジネスロジックだけを組み込めばよいのです。

@NoRepeatSubmit

@Component
public class NoRepeatSubmitCheckerImpl implements NoRepeatSubmitChecker {
    @Override
    public boolean check(NoRepeatSubmit anno, Context ctx,
                         String submitHash, int limitSeconds) {
        return LockUtils.tryLock(Solon.cfg().appName(), submitHash, limitSeconds);
    }
}

Enter fullscreen mode Exit fullscreen mode

@Whitelist

@Component
public class WhitelistCheckerImpl implements WhitelistChecker {
    @Override
    public boolean check(Whitelist anno, Context ctx) {
        String ip = ctx.realIp();
        return CloudClient.list().inListOfIp("whitelist", ip);
    }

Enter fullscreen mode Exit fullscreen mode

@NotBlacklist

@Component
public class NotBlacklistCheckerImpl implements NotBlacklistChecker {
    @Override
    public boolean check(NotBlacklist anno, Context ctx) {
        String ip = ctx.realIp();
        return !CloudClient.list().inListOfIp("blacklist", ip);
    }

Enter fullscreen mode Exit fullscreen mode

@Logined

@Component
public class LoginedCheckerImpl implements LoginedChecker {
    @Override
    public boolean check(Logined anno, Context ctx, String userKeyName) {
        return ctx.sessionAsLong("userId") > 0;
    }

Enter fullscreen mode Exit fullscreen mode

カスタムバリデータ:独自アノテーションの作成

フレームワークは拡張可能に設計されています。以下は @Phone バリデータの完全な作成例です。

手順 1:アノテーションの定義

@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
}

Enter fullscreen mode Exit fullscreen mode

手順 2:バリデータの実装

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

    @Override
    public String message(Phone anno) {
        return anno.message();
    }

    @Override
    public Class<?>[] groups(Phone anno) {
        return anno.groups();
    }

    @Override
    public Result validateOfValue(Phone anno, Object val, StringBuilder tmp) {
        if (val == null) return Result.succeed();
        if (val instanceof String == false) return Result.failure();
        if (PHONE_PATTERN.matcher((String) val).matches()) {
            return Result.succeed();
        }
        return Result.failure();
    }

    @Override
    public Result validateOfContext(Context ctx, Phone anno,
                                    String name, StringBuilder tmp) {
        String val = ctx.param(name);
        if (val == null) return Result.succeed();
        if (PHONE_PATTERN.matcher(val).matches()) {
            return Result.succeed();
        }
        return Result.failure(name);
    }
}

Enter fullscreen mode Exit fullscreen mode

手順 3:登録

@Configuration
public class ValidationConfig {
    @Bean
    public void registerValidators() {
        ValidatorManager.register(Phone.class, new PhoneValidator());
    }
}

Enter fullscreen mode Exit fullscreen mode

手順 4:使用

@Valid
@Controller
public class UserController {
    @Mapping("/user/phone")
    public void setPhone(@Phone String phone) {
        // 検証済み
    }
}

Enter fullscreen mode Exit fullscreen mode

改善してほしい点

2 点あります。

  1. 組み込みの @Phone@URL がない — よく使われるものが用意されていません。自分で書くのは簡単ですが、最初から用意されていると便利です。
  2. @Validated アノテーション名の重複 — JSR 380 の @Valid や Spring の @Validated と異なる動作をします。Solon では @Validated はパラメータに対するエンティティフィールドバリデーションのトリガー専用です。理解すれば問題ありませんが、JSR 380 との名前重複で一瞬混乱しました。

結論

Solon のバリデーションは自己完結型の完全なシステムです。20 以上のアノテーション、エンティティバリデーション、グループバリデーション、カスタムバリデータのための拡張ポイントを、javax.validationjakarta.validation の依存なしに利用できます。

特に優れているのはビジネス指向のアノテーション(@Whitelist@NoRepeatSubmit@Logined)です。セキュリティや冪等性の制約をハンドラ内に散在させるのではなく、バリデーションレイヤで強制できます。

依存グラフを小さく保ちたいプロジェクトにとって、Solon のバリデーションは有力な選択肢です。