JSR 380 に何度も苦しめられてきた私は、巨大な依存関係を引きずり込まないバリデーションフレームワークを高く評価しています。Solon のバリデーションシステムは solon-security-validation に含まれ — 単一のプラグインで 20 以上のアノテーション、エンティティバリデーション、カスタムバリデータを提供し、javax.validation や jakarta.validation を一切引き込みません。
役に立った点を順に紹介します。
2 フェーズ・バリデーションモデル
Solon はバリデーションを 2 つのフェーズに分けています。
- コンテキストバリデーション(注入前) — メソッドパラメータが解決される前に実行。メソッド自体に付けたアノテーションでヘッダー、クッキー、IP、リクエストレベルの制約を検査します。
- パラメータバリデーション(注入後) — パラメータ解決後に実行。個別のパラメータやエンティティフィールドに付けたアノテーションで検証します。
これらは 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 点あります。
-
組み込みの
@Phoneや@URLがない — よく使われるものが用意されていません。自分で書くのは簡単ですが、最初から用意されていると便利です。 -
@Validatedアノテーション名の重複 — JSR 380 の@Validや Spring の@Validatedと異なる動作をします。Solon では@Validatedはパラメータに対するエンティティフィールドバリデーションのトリガー専用です。理解すれば問題ありませんが、JSR 380 との名前重複で一瞬混乱しました。
結論
Solon のバリデーションは自己完結型の完全なシステムです。20 以上のアノテーション、エンティティバリデーション、グループバリデーション、カスタムバリデータのための拡張ポイントを、javax.validation や jakarta.validation の依存なしに利用できます。
特に優れているのはビジネス指向のアノテーション(@Whitelist、@NoRepeatSubmit、@Logined)です。セキュリティや冪等性の制約をハンドラ内に散在させるのではなく、バリデーションレイヤで強制できます。
依存グラフを小さく保ちたいプロジェクトにとって、Solon のバリデーションは有力な選択肢です。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.