Avaje Validator
バリデーションライブラリ
Avaje Validator
概要
Avaje Validatorは、アノテーションプロセッシングを使用したPOJOバリデーションライブラリです。aptソースコード生成によるリフレクションフリーのバリデーションを提供し、Hibernate Validationの軽量(〜120kb + 生成コード)なソースコード生成スタイルの代替として機能します。Java 17以上で動作し、コンパイル時にバリデーションアダプターを生成することで、実行時のリフレクションを回避します。
詳細
Avaje Validatorは、@Validアノテーションが付けられた各タイプに対して、avaje-validator-generatorアノテーションプロセッサがバリデーションアダプタークラスをJavaソースコードとして生成します。これらは自動的にサービスローダーメカニズムを使用してValidatorに登録されます。
主要な特徴:
- コンパイル時コード生成: リフレクションを使用しない高速なバリデーション
- 軽量: 約120kbと生成コードのみの小さなフットプリント
- 複数の制約アノテーション対応: Avaje/Jakarta/Javax制約アノテーションをサポート
- コンテナ要素制約: List、Set、Mapなど標準Javaコンテナの要素バリデーション
- メソッドパラメータバリデーション: DIコンテナと連携したメソッドレベルのバリデーション
- グループバリデーション: 条件に応じた部分的なバリデーション実行
- カスタムバリデータ: 独自のバリデーションロジックの実装が可能
- エラーメッセージのカスタマイズ: 詳細なエラーメッセージの制御
Avajeエコシステムの一部として設計され、マイクロサービスアーキテクチャに最適化されています。
メリット・デメリット
メリット
- 高速: リフレクションフリーによる優れたパフォーマンス
- 軽量: 小さなライブラリサイズとメモリフットプリント
- コンパイル時の安全性: 生成コードによる型安全性の向上
- GraalVM対応: ネイティブイメージビルドに最適
- シンプルなAPI: 直感的で使いやすいバリデーションAPI
- 標準互換: Jakarta/Javax Validationアノテーションとの互換性
- 依存性が少ない: 最小限の外部依存
デメリット
- コード生成: ビルドプロセスでのアノテーションプロセッサ設定が必要
- Java 17要件: 比較的新しいJavaバージョンが必要
- エコシステム: Hibernate Validatorと比較して統合やプラグインが少ない
- ドキュメント: 日本語ドキュメントが限定的
参考ページ
書き方の例
基本的なバリデーション
import io.avaje.validation.constraints.*;
@Valid
public record Customer(
@NotBlank(message = "名前は必須です")
String name,
@Email(message = "有効なメールアドレスを入力してください")
@NotNull
String email,
@Size(min = 10, max = 200, message = "住所は10文字以上200文字以下で入力してください")
String address,
@Min(value = 0, message = "年齢は0以上である必要があります")
@Max(value = 150, message = "年齢は150以下である必要があります")
Integer age
) {}
// バリデーションの実行
Validator validator = Validator.builder().build();
Customer customer = new Customer("", "invalid-email", "東京", -5);
try {
validator.validate(customer);
} catch (ConstraintViolationException e) {
e.violations().forEach(violation -> {
System.out.println(violation.path() + ": " + violation.message());
});
}
カスタムバリデータの作成
// カスタムアノテーション
import io.avaje.validation.constraints.Constraint;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = JapanesePhoneValidator.class)
public @interface JapanesePhone {
String message() default "有効な日本の電話番号を入力してください";
Class<?>[] groups() default {};
}
// バリデータ実装
import io.avaje.validation.adapter.ConstraintAdapter;
import io.avaje.validation.adapter.ValidationContext;
public class JapanesePhoneValidator implements ConstraintAdapter<JapanesePhone, String> {
@Override
public boolean isValid(String value, JapanesePhone annotation, ValidationContext ctx) {
if (value == null || value.isEmpty()) {
return true; // @NotNullと組み合わせて使用
}
// 日本の電話番号形式をチェック
return value.matches("^0\\d{1,4}-\\d{1,4}-\\d{4}$") ||
value.matches("^\\+81-\\d{1,4}-\\d{1,4}-\\d{4}$") ||
value.matches("^0\\d{9,10}$");
}
}
// 使用例
@Valid
public class ContactInfo {
@JapanesePhone
@NotNull(message = "電話番号は必須です")
private String phoneNumber;
// constructors, getters, setters
}
グループバリデーション
// バリデーショングループの定義
public interface BasicValidation {}
public interface DetailedValidation {}
@Valid
public class Product {
@NotNull(groups = BasicValidation.class)
@Size(min = 3, max = 100, groups = BasicValidation.class)
private String name;
@NotNull(groups = BasicValidation.class)
@Positive(groups = BasicValidation.class)
private BigDecimal price;
@NotNull(groups = DetailedValidation.class)
@Pattern(regexp = "^[A-Z]{2}\\d{6}$",
message = "製品コードは大文字2文字と6桁の数字です",
groups = DetailedValidation.class)
private String productCode;
@Size(min = 20, max = 1000, groups = DetailedValidation.class)
private String description;
}
// 使用例
Validator validator = Validator.builder().build();
Product product = new Product();
// 基本バリデーションのみ実行
validator.validate(product, BasicValidation.class);
// 詳細バリデーションのみ実行
validator.validate(product, DetailedValidation.class);
// すべてのバリデーションを実行
validator.validate(product, BasicValidation.class, DetailedValidation.class);
コレクションとマップのバリデーション
@Valid
public class ShoppingCart {
@NotEmpty(message = "カートは空にできません")
@Valid
private List<@Valid CartItem> items;
@Size(max = 10, message = "クーポンは最大10個まで適用できます")
private Set<@NotBlank String> couponCodes;
@Valid
private Map<@NotNull String, @Valid @NotNull Address> deliveryAddresses;
}
@Valid
public record CartItem(
@NotNull @Positive Long productId,
@Min(1) @Max(99) Integer quantity,
@NotNull @DecimalMin("0.01") BigDecimal unitPrice
) {}
@Valid
public record Address(
@NotBlank String street,
@Pattern(regexp = "\\d{3}-\\d{4}") String postalCode,
@NotBlank String city
) {}
Dependency Injectionとの統合
// Avaje Injectとの統合例
import io.avaje.inject.Component;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
public class OrderService {
private final Validator validator;
@Inject
public OrderService(Validator validator) {
this.validator = validator;
}
public Order createOrder(@Valid Order order) {
// DIコンテナがメソッドバリデーションを自動実行
return orderRepository.save(order);
}
}
// バリデータの設定
@Component
public class ValidatorProvider {
@Singleton
public Validator provideValidator() {
return Validator.builder()
.addLocales(Locale.JAPANESE, Locale.ENGLISH)
.failFast(false) // すべてのエラーを収集
.build();
}
}
エラーハンドリング
public class ValidationExample {
private final Validator validator = Validator.builder().build();
public void processCustomer(Customer customer) {
try {
validator.validate(customer);
// バリデーション成功時の処理
saveCustomer(customer);
} catch (ConstraintViolationException e) {
// エラー情報の取得
Set<ConstraintViolation> violations = e.violations();
violations.forEach(violation -> {
String propertyPath = violation.path();
String message = violation.message();
Object invalidValue = violation.invalidValue();
System.err.printf("エラー: %s - %s (値: %s)%n",
propertyPath, message, invalidValue);
});
// エラーレスポンスの作成
Map<String, List<String>> errors = violations.stream()
.collect(Collectors.groupingBy(
ConstraintViolation::path,
Collectors.mapping(ConstraintViolation::message,
Collectors.toList())
));
throw new ValidationException(errors);
}
}
}
メソッドバリデーション
// メソッドアダプタの生成を有効化
@Valid
@AdaptMethod
public interface PaymentService {
void processPayment(
@NotNull @Valid CreditCard card,
@Positive @Max(1000000) BigDecimal amount,
@NotBlank String currency
);
@NotNull
@Valid
Receipt completeTransaction(@NotBlank String transactionId);
}
// 実装クラス
@Component
public class PaymentServiceImpl implements PaymentService {
@Override
public void processPayment(CreditCard card, BigDecimal amount, String currency) {
// バリデーションはAOPで自動実行される
// ビジネスロジックの実装
}
@Override
public Receipt completeTransaction(String transactionId) {
// 戻り値も自動的にバリデーションされる
return new Receipt(transactionId, Instant.now());
}
}