Javaアプリケーションでの検証

このテキストは、データ検証のさまざまなアプローチに焦点を当てています。プロジェクトで遭遇する可能性のある落とし穴と、Javaアプリケーションでデータを検証する際に従うべきメソッドとテクノロジーです。







検証







作成者がわざわざデータ検証のアプローチを選択しなかったプロジェクトをよく見ました。 チームは、期限とあいまいな要件という形で信じられないほどのプレッシャーの下でプロジェクトに取り組みました。その結果、彼らは正確で一貫した検証の時間を持っていませんでした。 そのため、Javascriptスニペット、スクリーンコントローラー、ビジネスロジックビン、ドメインエンティティ、トリガー、データベース制約など、検証コードはあらゆる場所に散らばっています。 このコードはif-elseステートメントでいっぱいであり、多数の例外をスローし、そこで特定のデータが検証される場所を見つけようとします...その結果、プロジェクトの開発に伴い、要件に準拠することが難しくなり、コストがかかります(多くの場合、非常に混乱します)データ検証へのアプローチの均一性。







データを検証するためのシンプルでエレガントな方法はありますか? 読みにくいという罪から私たちを守る方法、検証のすべてのロジックをまとめる方法、そして人気のあるJavaフレームワークの開発者によってすでに作成された方法はありますか?







はい、そのような方法があります。







CUBAプラットフォームの開発者である私たちにとって、ベストプラクティスを使用できることが非常に重要です。 検証コードは次のことを行う必要があると考えています。







  1. 再利用可能であり、DRYの原則に従ってください。
  2. 自然で理解しやすいものにしてください。
  3. 開発者が見たい場所に配置。
  4. ユーザーインターフェイス、SOAP呼び出し、RESTなど、さまざまなソースからのデータを検証できるようにします。
  5. 問題なくマルチスレッド環境で動作します。
  6. チェックを手動で実行する必要なく、アプリケーション内で自動的に呼び出されます。
  7. 簡潔なダイアログボックスでユーザーに明確でローカライズされたメッセージを提供するため。
  8. 標準に従ってください。


CUBAプラットフォームフレームワークを使用して記述されたサンプルアプリケーションを使用して、これを実装する方法を見てみましょう。 ただし、CUBAはSpringとEclipseLinkに基づいているため、ここで使用される技術のほとんどは、JPAおよびBean Validation仕様をサポートする他のJavaプラットフォームで動作します。







データベース制約を使用した検証



おそらく、データを検証する最も一般的で明白な方法は、データベースレベルで制限を使用することです。たとえば、必須フラグ(値を空にすることはできないフィールド)、文字列の長さ、一意のインデックスなどです。 このタイプのソフトウェアは通常、データ処理に厳密に焦点を合わせているため、この方法はエンタープライズアプリケーションに最適です。 ただし、ここでも、アプリケーションの各レベルに個別に制限を設定することにより、開発者は間違いを犯します。 ほとんどの場合、その理由は開発者間の責任の分配にあります。







私たちのほとんどが知っている例を考えてみてください。私たち自身の経験からでも… RESTサービス、そして最後に、クライアント側のUI開発者。 次に、この要件が変更され、フィールドが15文字に増加します。 Devopsはデータベースの制約値を変更しますが、クライアント側の制限は同じであるため、ユーザーにとっては何も変わりません...







開発者はこの問題を回避する方法を知っています。検証は集中化する必要があります。 CUBAでは、そのような検証はJPAエンティティアノテーションにあります。 このメタ情報に基づいて、CUBA Studioは正しいDDLスクリプトを生成し、適切なクライアント側バリデーターを適用します。







制約の例







注釈が変更されると、CUBAはDDLスクリプトを更新し、移行スクリプトを生成します。そのため、次回プロジェクトを展開するときに、インターフェイスとアプリケーションデータベースの両方で新しいJPAベースの制限が有効になります。







この方法に絶対的な信頼性を与えるデータベースレベルでのシンプルさと実装にもかかわらず、JPAアノテーションの範囲は、DDL標準で表現できる最も単純なケースに限定され、データベーストリガーやストアドプロシージャは含まれません。 そのため、JPAベースの制約により、エンティティフィールドを一意または必須にするか、列の最大長を設定できます。 @UniqueConstraint



アノテーションを使用して、列の組み合わせに一意の制限を設定することもできます。 しかし、それがおそらくすべてです。







場合によっては、フィールドの最小/最大値の確認、正規表現による検証、アプリケーション固有のカスタムチェックの実行など、より複雑な検証ロジックが必要な場合は、 「Bean Validation」と呼ばれるアプローチが適用されます。







Bean検証



誰もが、ライフサイクルが長く、その効果が何千ものプロジェクトで証明されている標準に従うことをお勧めします。 Java Bean Validationは、 JSR 380、349 、および303とそのアプリケーション( Hibernate ValidatorおよびApache BVal)で文書化されたアプローチです







このアプローチは多くの開発者によく知られていますが、多くの場合過小評価されています。 これは、レガシープロジェクトにもデータ検証を埋め込む簡単な方法です。これにより、明確でシンプルで信頼性が高く、ビジネスロジックに可能な限り近いチェックを構築できます。







Bean Validationを使用すると、プロジェクトに多くの利点があります。









ユーザーが入力された情報を送信すると、 CUBAプラットフォームは (他のフレームワークと同様に) Bean Validationを自動的に開始するため、検証が失敗すると即座にエラーメッセージが表示され、ビンバリデーターを手動で実行する必要はありません。







パスポート番号を使用して例に戻りましょうが、今回はPersonエンティティのいくつかの制限を追加します。









これらすべてのチェックにより、Personクラスは次のようになります。







 @Listeners("passportnumber_PersonEntityListener") @NamePattern("%s|name") @Table(name = "PASSPORTNUMBER_PERSON") @Entity(name = "passportnumber$Person") @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @FraudDetectionFlag public class Person extends StandardEntity { private static final long serialVersionUID = -9150857881422152651L; @Pattern(message = "Bad formed person name: ${validatedValue}", regexp = "^[AZ][az]*(\\s(([az]{1,3})|(([az]+\\')?[AZ][az]*)))*$") @Length(min = 2) @NotNull @Column(name = "NAME", nullable = false) protected String name; @Email(message = "Email address has invalid format: ${validatedValue}", regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") @Column(name = "EMAIL", length = 120) protected String email; @DecimalMax(message = "Person height can not exceed 300 centimeters", value = "300") @DecimalMin(message = "Person height should be positive", value = "0", inclusive = false) @Column(name = "HEIGHT") protected BigDecimal height; @NotNull @Column(name = "COUNTRY", nullable = false) protected Integer country; @NotNull @Column(name = "PASSPORT_NUMBER", nullable = false, length = 15) protected String passportNumber; ... }
      
      





Person.java







@NotNull



@DecimalMin



@Length



@Pattern



@DecimalMin



などの注釈の使用は非常に明白であり、コメントを必要としないと信じています。 @ValidPassportNumber



アノテーションの実装を詳しく見てみましょう。







新しい@ValidPassportNumber



は、 Person#passportNumber



Person#country



フィールドで指定された各国の正規表現パターンと一致することを確認します。







まず、ドキュメントをご覧くださいCUBAまたはHibernateのマニュアルで問題ありません)、それに応じて、この新しい注釈でクラスをマークし、それにgroups



パラメーターを渡す必要がありますUiCrossFieldChecks.class



は、この検証がクロスで実行されることを意味します。検証-すべての個々のフィールドをチェックした後、 Default.class



はデフォルトの検証グループに制限を保存します。







注釈の説明は次のようになります。







 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = ValidPassportNumberValidator.class) public @interface ValidPassportNumber { String message() default "Passport number is not valid"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
      
      





ValidPassportNumber.java







ここで、 @Target(ElementType.TYPE)



は、このランタイムアノテーションの目的はクラスであり、 @Constraint(validatedBy = … )



は、 ConstraintValidator<...>



インターフェースを実装するValidPassportNumberValidator



クラスによって検証が実行されることを決定します。 検証コード自体はisValid(...)



メソッドにあり、実際の検証をかなり簡単な方法で実行します。







 public class ValidPassportNumberValidator implements ConstraintValidator<ValidPassportNumber, Person> { public void initialize(ValidPassportNumber constraint) { } public boolean isValid(Person person, ConstraintValidatorContext context) { if (person == null) return false; if (person.country == null || person.passportNumber == null) return false; return doPassportNumberFormatCheck(person.getCountry(), person.getPassportNumber()); } private boolean doPassportNumberFormatCheck(CountryCode country, String passportNumber) { ... } }
      
      





ValidPassportNumberValidator.java







以上です。 CUBAプラットフォームでは、カスタム検証を機能させ、ユーザーエラーメッセージを表示するコード行を記述するだけで済みます。

複雑なことはありませんか?







それでは、すべての仕組みを見てみましょう。 ここでCUBAには他のnishtyakiがあります:ユーザーにエラーメッセージを表示するだけでなく、Bean検証に合格しなかった赤いフィールドで強調表示します。







UI表現







それはエレガントな解決策ではありませんか? サブジェクト領域のエンティティにいくつかのJava注釈のみを追加することにより、UIに検証エラーの適切な表示が得られます。







セクションを要約して、エンティティに対するBean Validationの利点をもう一度簡単にリストしましょう。







  1. それは理解可能で読みやすいです。
  2. エンティティクラスで値の制約を直接定義できます。
  3. カスタマイズおよび補足できます。
  4. 一般的なORMに統合され、変更がデータベースに保存される前にチェックが自動的に開始されます。
  5. 一部のフレームワークは、ユーザーがUIにデータを送信するときにBean検証を自動的に実行します(そうでない場合は、 Validator



    インターフェイスを手動で簡単に呼び出すことができます)。
  6. Bean Validationは認識されている標準であり、インターネット上のドキュメントで一杯です。


しかし、外部システムからのデータを検証するために、メソッド、コンストラクター、またはRESTアドレスに制限を設定する必要がある場合はどうでしょうか? または、テストされた各メソッドで多くのif-else条件を持つ退屈なコードを記述せずに、メソッドパラメーターの値を宣言的にチェックする必要がある場合はどうでしょうか。







答えは簡単です:Bean Validationはメソッドにも適用されます!







契約による検証



データモデルの状態の検証を超える必要がある場合があります。 多くのメソッドは、パラメーターと戻り値の自動検証の恩恵を受けることができます。 これは、RESTまたはSOAPアドレスに送信されるデータをチェックするだけでなく、メソッド呼び出しの前提条件と事後条件を規定して、入力されたデータがメソッド本体の実行前に検証されたこと、または戻り値を確認したい場合にも必要になる場合がありますが予想される範囲内にあるか、たとえば、入力パラメーターの値の範囲を宣言的に記述するだけで、コードの可読性が向上します。







Bean検証を使用して、メソッドおよびコンストラクターの入力パラメーターと戻り値に制限を適用して、Javaクラスでの呼び出しの前提条件と事後条件を確認できます。 このパスには、パラメーターと戻り値の有効性をチェックする従来の方法に比べていくつかの利点があります。







  1. 命令型スタイルで手動でチェックを実行する必要はありません(たとえば、 IllegalArgumentException



    などをスローすることにより)。 制約を宣言的に定義し、コードをよりわかりやすく表現力豊かにすることができます。
  2. 制約の構成、再利用、構成が可能です。チェックごとに検証ロジックを記述する必要はありません。 コードが少ないほど、バグが少なくなります。
  3. クラス、メソッドの戻り値、またはパラメーターが@Validated



    アノテーションでマークされている場合、チェックはメソッドが呼び出されるたびにプラットフォームによって自動的に実行されます。
  4. 実行可能モジュールが@Documented



    アノテーションでマークされている@Documented



    、その事前条件と事後条件は生成されたJavaDocに含まれます。


「契約の検証」を使用して、明確でコンパクトな、保守が容易なコードを取得します。







例として、CUBAアプリケーションのRESTコントローラーのインターフェースを見てみましょう。 PersonApiService



インターフェイスを使用すると、 getPersons()



メソッドを使用してデータベースから人のリストを取得し、 addNewPerson(...)



呼び出しを使用して新しい人を追加できます。







そして、Bean検証が継承されることを忘れないでください! つまり、特定のクラス、フィールド、またはメソッドに注釈を付けた場合、このクラスを継承するか、このインターフェイスを実装するすべてのクラスは、同じ検証注釈の対象になります。







 @Validated public interface PersonApiService { String NAME = "passportnumber_PersonApiService"; @NotNull @Valid @RequiredView("_local") List<Person> getPersons(); void addNewPerson( @NotNull @Length(min = 2, max = 255) @Pattern(message = "Bad formed person name: ${validatedValue}", regexp = "^[AZ][az]*(\\s(([az]{1,3})|(([az]+\\')?[AZ][az]*)))*$") String name, @DecimalMax(message = "Person height can not exceed 300 cm", value = "300") @DecimalMin(message = "Person height should be positive", value = "0", inclusive = false) BigDecimal height, @NotNull CountryCode country, @NotNull String passportNumber ); }
      
      





PersonApiService.java







このコードは十分に明確ですか?

_(CUBAプラットフォームに固有のアノテーション@RequiredView(“_local”)



除き、返されたPerson



オブジェクトにPASSPORTNUMBER_PERSON



テーブルのすべてのフィールドが含まれていることを確認します)._







@Valid



は、 getPersons()



メソッドによって返される各コレクションオブジェクトも、 Person



クラスの制限に対して検証する必要があることを定義します。







CUBAアプリケーションでは、これらのメソッドは次のアドレスで利用できます。









Postmanアプリケーションを開き、検証が正常に機能することを確認します。







郵便配配アプリ







お気づきかもしれませんが、上記の例ではパスポート番号は検証されていません。 これは、 passportNumber



を検証するための正規表現テンプレートの選択がcountry



フィールドの値に依存するため、このフィールドはaddNewPerson



メソッドのパラメーターのクロスチェックを必要とするためです。 この相互検証は、クラスレベルのエンティティ制限の完全な類似物です!







パラメーターの相互検証はJSR 349および380でサポートされています。Hibernateのドキュメントを読んで、独自のクラス/インターフェースメソッドの相互検証を実装する方法を学ぶことができます。







外部Bean検証



世界には完璧なものがないため、Beanの検証には欠点と制限があります。







  1. データベースに変更を保存する前に、オブジェクトの複雑なグラフの状態を確認するだけでよい場合があります。 たとえば、顧客注文のすべての要素が1つのパッケージに入れられていることを確認する必要があります。 これはかなり難しい操作であり、ユーザーが注文に新しいアイテムを追加するたびに実行するのは得策ではありません。 したがって、このようなチェックが必要になるのは1回だけですOrderItem



    オブジェクトとそのOrderItem



    サブオブジェクトをデータベースに保存する前に。
  2. トランザクション内でいくつかのチェックを行う必要があります。 たとえば、電子ストアシステムは、データベースにコミットする前に注文を履行するために十分な商品のコピーがあるかどうかを確認する必要があります。 このようなチェックは、トランザクション内でのみ実行できます。 システムはマルチスレッドであり、在庫品の数量はいつでも変更される可能性があります。


CUBAプラットフォームは、 エンティティリスナートランザクションリスナーと呼ばれる2つの事前コミットデータ検証メカニズムを提供します。 それらをより詳細に検討しましょう。







エンティティリスト



CUBAのエンティティリスナーは 、JPAが開発者に提供するPreInsertEvent



PreUpdateEvent



およびPredDeleteEvent



リスナー
に非常に似ています。 どちらのメカニズムでも、エンティティオブジェクトをデータベースに保存する前後にチェックできます。







CUBAでは、エンティティリスナーを簡単に作成して接続できます。これには、次の2つが必要です。







  1. エンティティリスナーインターフェイスの1つを実装するマネージドBeanを作成します。 検証には3つのインターフェイスが重要です。

    BeforeDeleteEntityListener<T>





    BeforeInsertEntityListener<T>





    BeforeUpdateEntityListener<T>



  2. 追跡する予定のエンティティオブジェクトに@Listeners



    アノテーションを追加します。


そしてそれだけです。







JPA標準(JSR 338、セクション3.5)と比較すると、CUBAプラットフォームリスナーインターフェイスは型指定されているため、 Object



型の引数をエンティティ型にキャストして作業を開始する必要はありません。 CUBAプラットフォームは、関連するエンティティまたはEntityManagerの呼び出し元に、他のエンティティをロードおよび変更する機能を追加します。 これらの変更はすべて、対応するエンティティリスナーも呼び出します。







CUBAプラットフォームは、データベースから実際にレコードを削除する代わりに、削除済みとしてマークされ、通常の使用ではアクセスできなくなるアプローチである「ソフト削除」もサポートしています。 したがって、ソフト削除の場合、プラットフォームはBeforeDeleteEntityListener



/ AfterDeleteEntityListener



リスナーを呼び出しますが、標準の実装ではPreUpdate



/ PostUpdate



呼び出しPostUpdate









例を見てみましょう。 ここで、イベントリスナBeanは、1行のコードでエンティティクラスに接続します。 @Listeners



アノテーションは、リスナクラスの名前を@Listeners



します。







 @Listeners("passportnumber_PersonEntityListener") @NamePattern("%s|name") @Table(name = "PASSPORTNUMBER_PERSON") @Entity(name = "passportnumber$Person") @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @FraudDetectionFlag public class Person extends StandardEntity { ... }
      
      





Person.java







リスナーの実装自体は次のようになります。







 /** * Checks that there are no other persons with the same * passport number and country code * Ignores spaces in the passport number for the check. * So numbers "12 45 768007" and "1245 768007" and "1245768007" * are the same for the validation purposes. */ @Component("passportnumber_PersonEntityListener") public class PersonEntityListener implements BeforeDeleteEntityListener<Person>, BeforeInsertEntityListener<Person>, BeforeUpdateEntityListener<Person> { @Override public void onBeforeDelete(Person person, EntityManager entityManager) { if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) { throw new ValidationException( "Passport and country code combination isn't unique"); } } @Override public void onBeforeInsert(Person person, EntityManager entityManager) { // use entity argument to validate the Person object // entityManager could be used to access database // if you need to check the data // throw ValidationException object if validation check failed if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) throw new ValidationException( "Passport and country code combination isn't unique"); } @Override public void onBeforeUpdate(Person person, EntityManager entityManager) { if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) throw new ValidationException( "Passport and country code combination isn't unique"); } ... }
      
      





PersonEntityListener.java







エンティティリスナーは、次の場合に最適です。









トランザクションリスナー



CUBAトランザクションリスナートランザクションのコンテキストで動作しますが、エンティティリスナーと比較して、各データベーストランザクションに対して呼び出されます。







これらは彼らに超強力を与えます:









しかし、同じことが彼らの欠点によって決定されます:









したがって、トランザクションリスナは、同じアルゴリズムを使用してさまざまな種類のエンティティを検査する必要がある場合(たとえば、すべてのビジネスオブジェクトに対応する単一のサービスでサイバー詐欺のすべてのデータをチェックする場合)に適したソリューションです。







あなたは通り過ぎてはならない!







エンティティに@FraudDetectionFlag



注釈があるかどうかを確認し、存在する場合は不正検出を開始するサンプルを見てください。 繰り返しますが、このメソッドは各データベーストランザクションをコミットする前にシステムで呼び出されるので、コードはできるだけ少ないオブジェクトをチェックしようとします。







 @Component("passportnumber_ApplicationTransactionListener") public class ApplicationTransactionListener implements BeforeCommitTransactionListener { private Logger log = LoggerFactory.getLogger(ApplicationTransactionListener.class); @Override public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) { for (Entity entity : managedEntities) { if (entity instanceof StandardEntity && !((StandardEntity) entity).isDeleted() && entity.getClass().isAnnotationPresent(FraudDetectionFlag.class) && !fraudDetectorFeedAndFastCheck(entity)) { logFraudDetectionFailure(log, entity); String msg = String.format( "Fraud detection failure in '%s' with id = '%s'", entity.getClass().getSimpleName(), entity.getId()); throw new ValidationException(msg); } } } ... }
      
      





ApplicationTransactionListener.java







トランザクションリスナーになるには、マネージドBeanがBeforeCommitTransactionListener



インターフェイスとbeforeCommit



メソッドを実装する必要があります。 トランザクションリスナーは、アプリケーションの起動時に自動的にバインドします。 CUBAは、 BeforeCommitTransactionListener



またはAfterCompleteTransactionListener



を実装するすべてのクラスをトランザクションリスナーとして登録します。







おわりに



Bean検証(JPA 303、349、および980)は、企業プロジェクトで発生したデータ検証ケースの95%の信頼できる基礎として役立つアプローチです。 このアプローチの主な利点は、検証ロジックのほとんどがドメインモデルクラスに直接集中していることです。 したがって、見つけやすく、読みやすく、保守も簡単です。 Spring、CUBA、および他の多くのライブラリはこれらの標準をサポートし、UIレイヤーでデータを受信するとき、検証済みのメソッドを呼び出すとき、またはORMを介してデータを保存するときに検証チェックを自動的に実行します。







一部のソフトウェア開発者は、サブジェクトモデルのクラスレベルでの検証を不自然で複雑すぎると見なし、UIレベルでのデータ検証はかなり効果的な戦略であると言います。 ただし、コンポーネントおよびUIコントローラーの多数の検証ポイントは、最も合理的なアプローチではないと考えています。 , , , , , listener' .







, , :







  1. JPA , , DDL.
  2. Bean Validation — , , , . , .
  3. bean validation, . , , REST.
  4. Entity listeners: , Bean Validation, . , . Hibernate .
  5. Transaction listeners — , , . , , .


PS: , Java, , , .















All Articles