Avaje Validator

バリデーションライブラリJavaアノテーション軽量コード生成リフレクションフリー

バリデーションライブラリ

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());
    }
}