OVal

Java向けの実用的で拡張可能なオブジェクト検証フレームワーク

OValは、Javaオブジェクト(JavaBeansに限らず)のための実用的で拡張可能な検証フレームワークです。アノテーション、POJO、XMLなど複数の方法で制約を宣言でき、Programming by Contract(DbC)機能も提供します。

インストール

Maven

<dependency>
    <groupId>net.sf.oval</groupId>
    <artifactId>oval</artifactId>
    <version>3.2.1</version>
</dependency>

Gradle

implementation 'net.sf.oval:oval:3.2.1'

基本的な使い方

シンプルな検証

import net.sf.oval.Validator;
import net.sf.oval.ConstraintViolation;
import net.sf.oval.constraint.*;

public class User {
    @NotNull(message = "名前は必須です")
    @NotEmpty
    @Length(max = 50, message = "名前は50文字以内で入力してください")
    private String name;
    
    @NotNull
    @Email(message = "有効なメールアドレスを入力してください")
    private String email;
    
    @Min(value = 0, message = "年齢は0以上である必要があります")
    @Max(value = 150, message = "年齢は150以下である必要があります")
    private int age;
    
    // ゲッター・セッター
}

// 検証の実行
Validator validator = new Validator();
User user = new User();
user.setName("");  // 空文字列
user.setEmail("invalid-email");
user.setAge(-1);

List<ConstraintViolation> violations = validator.validate(user);
for (ConstraintViolation violation : violations) {
    System.out.println(violation.getMessage());
}

主要なアノテーション

基本的な検証アノテーション

public class Product {
    // null値チェック
    @NotNull(message = "商品名は必須です")
    private String name;
    
    // 文字列の長さ
    @Length(min = 5, max = 100, message = "説明は5〜100文字で入力してください")
    private String description;
    
    // 数値の範囲
    @Range(min = 0.0, max = 999999.99, message = "価格は0〜999,999.99の範囲で入力してください")
    private double price;
    
    // 正規表現パターン
    @MatchPattern(pattern = "^[A-Z]{2}\\d{6}$", message = "商品コードは無効です")
    private String productCode;
    
    // コレクションのサイズ
    @Size(min = 1, max = 10, message = "タグは1〜10個まで設定できます")
    private List<String> tags;
    
    // 日付の検証
    @Past(message = "製造日は過去の日付である必要があります")
    private Date manufacturingDate;
    
    @Future(message = "有効期限は未来の日付である必要があります")
    private Date expirationDate;
}

条件付き検証

public class Order {
    private boolean isExpress;
    
    // 条件付き必須フィールド
    @NotNull(when = "groovy:_this.isExpress", message = "速達の場合、配送時間は必須です")
    private String deliveryTime;
    
    // 複数条件の組み合わせ
    @Min(value = 100, when = "javascript:_this.customerType == 'PREMIUM'", 
         message = "プレミアム会員の最小注文額は100円です")
    private double totalAmount;
}

カスタム制約の作成

アノテーションベースのカスタム制約

import net.sf.oval.configuration.annotation.Constraint;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Constraint(checkWith = PhoneNumberValidator.class)
public @interface PhoneNumber {
    String message() default "無効な電話番号です";
}

// バリデータークラス
public class PhoneNumberValidator extends AbstractAnnotationCheck<PhoneNumber> {
    @Override
    public boolean isSatisfied(Object validatedObject, Object value, 
                              OValContext context, Validator validator) {
        if (value == null) return true;
        
        String phone = value.toString();
        // 日本の電話番号形式をチェック
        return phone.matches("^0\\d{1,4}-\\d{1,4}-\\d{4}$") ||
               phone.matches("^0\\d{9,10}$");
    }
}

使用例

public class Contact {
    @PhoneNumber
    private String phoneNumber;
    
    @PhoneNumber(message = "緊急連絡先の電話番号が無効です")
    private String emergencyContact;
}

Programming by Contract (DbC)

メソッドの事前条件・事後条件

import net.sf.oval.guard.*;

@Guarded
public class BankAccount {
    private double balance = 0.0;
    
    // 事前条件:入金額は正の値
    @Pre(expr = "_args[0] > 0", lang = "groovy", 
         message = "入金額は正の値である必要があります")
    public void deposit(double amount) {
        balance += amount;
    }
    
    // 事前条件と事後条件
    @Pre(expr = "_args[0] > 0 && _args[0] <= _this.balance", lang = "groovy",
         message = "引き出し額は正の値で、残高以下である必要があります")
    @Post(expr = "_this.balance >= 0", lang = "groovy",
          message = "残高は負になることはできません")
    public void withdraw(double amount) {
        balance -= amount;
    }
    
    // 戻り値の検証
    @Post(expr = "_result >= 0", lang = "groovy",
          message = "残高は常に0以上です")
    public double getBalance() {
        return balance;
    }
}

不変条件(Invariants)

@Guarded
public class Rectangle {
    @Min(0)
    private double width;
    
    @Min(0)
    private double height;
    
    // クラスの不変条件
    @Invariant(expr = "_this.width >= 0 && _this.height >= 0", lang = "groovy",
               message = "幅と高さは負の値になることはできません")
    public void setDimensions(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

Spring統合

Spring Validatorとしての使用

import net.sf.oval.integration.spring.SpringValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.Errors;

@Configuration
public class ValidationConfig {
    @Bean
    public Validator ovalSpringValidator() {
        return new SpringValidator(new net.sf.oval.Validator());
    }
}

// コントローラーでの使用
@RestController
public class UserController {
    @Autowired
    private Validator validator;
    
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@RequestBody User user, 
                                      BindingResult result) {
        validator.validate(user, result);
        
        if (result.hasErrors()) {
            return ResponseEntity.badRequest().body(result.getAllErrors());
        }
        
        // ユーザー作成処理
        return ResponseEntity.ok(user);
    }
}

Spring AOPを使用したメソッド検証

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    @Bean
    public GuardInterceptor guardInterceptor() {
        return new GuardInterceptor();
    }
    
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        return new DefaultAdvisorAutoProxyCreator();
    }
    
    @Bean
    public NameMatchMethodPointcutAdvisor guardedAdvisor() {
        NameMatchMethodPointcutAdvisor advisor = 
            new NameMatchMethodPointcutAdvisor();
        advisor.setMappedNames("*");
        advisor.setAdvice(guardInterceptor());
        return advisor;
    }
}

式言語の活用

複数の式言語サポート

public class ConditionalValidation {
    private String type;
    private String value;
    
    // JavaScript式
    @NotNull(when = "javascript:_this.type == 'REQUIRED'",
             message = "タイプがREQUIREDの場合、値は必須です")
    private String conditionalField;
    
    // Groovy式
    @Length(max = 10, when = "groovy:_this.type == 'LIMITED'",
            message = "タイプがLIMITEDの場合、最大10文字です")
    private String limitedField;
    
    // MVEL式
    @MatchPattern(pattern = "\\d+", 
                  when = "mvel:type == 'NUMERIC'",
                  message = "タイプがNUMERICの場合、数値のみ許可されます")
    private String numericField;
}

グループ検証

// 検証グループの定義
public interface Create {}
public interface Update {}

public class Article {
    @NotNull(groups = {Update.class})
    private Long id;
    
    @NotNull(groups = {Create.class, Update.class})
    @Length(max = 100, groups = {Create.class, Update.class})
    private String title;
    
    @NotNull(groups = {Create.class})
    private String author;
}

// グループを指定して検証
Validator validator = new Validator();
Article article = new Article();

// 作成時の検証
List<ConstraintViolation> createViolations = 
    validator.validate(article, Create.class);

// 更新時の検証
List<ConstraintViolation> updateViolations = 
    validator.validate(article, Update.class);

エラーメッセージの国際化

// messages.properties
user.name.required=名前は必須です
user.email.invalid=有効なメールアドレスを入力してください
user.age.range=年齢は{min}〜{max}の範囲で入力してください

// 使用例
public class User {
    @NotNull(message = "{user.name.required}")
    private String name;
    
    @Email(message = "{user.email.invalid}")
    private String email;
    
    @Range(min = 0, max = 150, message = "{user.age.range}")
    private int age;
}

パフォーマンスの最適化

キャッシングの活用

// バリデーターの設定をカスタマイズ
Validator validator = new Validator();
validator.getConfiguration().setCheckInvariantsForReturnValues(false);
validator.getConfiguration().setIgnoreFieldAnnotationsOfClassesDeclaredIn(
    Arrays.asList("com.example.legacy")
);

// 制約のキャッシング
validator.getConfiguration().setConstraintCachingEnabled(true);

まとめ

OValは、Javaアプリケーションに強力で柔軟な検証機能を提供します。アノテーションベースの宣言的な検証から、Programming by Contractによる契約プログラミング、さらには複数の式言語を使用した動的な条件付き検証まで、幅広い検証ニーズに対応できます。Spring Frameworkとの統合も簡単で、エンタープライズアプリケーションでの使用に適しています。