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との統合も簡単で、エンタープライズアプリケーションでの使用に適しています。