UPD。 中央のMavenリポジトリに投稿されました
<dependency> <groupId>net.sf.brunneng.jom</groupId> <artifactId>java-object-merger</artifactId> <version>0.8.5.1</version> </dependency>
UPD2 。 バージョン0.8.4
オブジェクトマッパーとは何ですか?
簡単な答えは、あるオブジェクトから別のオブジェクトにデータを自動的にコピーすることです。 しかし、あなたは尋ねるかもしれません:なぜこのコピーが必要なのですか? これが非常に頻繁に必要であることを疑うことができます。 そのため、より詳細な回答が必要です。
エンタープライズアプリケーションの世界では、データベースアクセス層、ビジネス、プレゼンテーション/ Webサービスなどの内部構造を層に分割するのが一般的です。 通常、データベースアクセス層には、データベース内のテーブルにマッピングされるオブジェクトが存在します。 それらをDTOと呼ぶことに同意しましょう(データ転送オブジェクトから)。 良い方法では、テーブルからデータを転送するだけで、ビジネスロジックは含まれません。 プレゼンテーション/ Webサービスレイヤーには、クライアント(ブラウザー/ Webサービスクライアント)にデータを配信するオブジェクトがあります。 それらをVO(Viewオブジェクトから)と呼びましょう。 VOは、DTOにあるデータの一部のみを必要とするか、複数のDTOからのデータを集約します。 さらに、データのローカライズまたは変換を行って、プレゼンテーションに便利な形式にすることができます。 したがって、DTOをビューに直接渡すのは適切ではありません。 また、ビジネスオブジェクトBO(ビジネスオブジェクト)がビジネスレイヤーで強調表示されることがあります。 これらはDTOのラッパーであり、それらを操作するビジネスロジック(保存、変更、ビジネスオペレーション)が含まれています。 この背景に対して、異なるレイヤーのオブジェクト間でデータを転送するというタスクが発生します。 たとえば、データの一部をDTOからVOにマッピングします。 または、VOからBOに移行して、発生した内容を保存します。
問題を真正面から解決すると、次のような「ダム」コードが得られます。
… employeeVO.setPositionName(employee.getPositionName()); employeeVO.setPerson(new PersonVO()); PersionVO personVO = employeeVO.getPerson(); PersonDTD person = employee.getPerson(); personVO.setFirstName(person.getFirstName()); personVO.setMiddleName(person.getMiddleName()); personVO.setLastName(person.getLastName()); ...
それはおなじみですか? :)はいの場合、私はあなたを喜ばせることができます。 この問題については、すでに解決策が考え出されています。
オブジェクトのマッパー
もちろん、それらは私が発明したものではありません。 Javaには多くの実装があります。 たとえば、 ここで見つけることができます。
つまり、マッパーのタスクは、あるオブジェクトのすべてのプロパティを別のオブジェクトにコピーし、必要に応じて必要な型変換を行うプロセスで、すべての子オブジェクトに対して同じことを再帰的に行うことです。
上記のリストのマッパーはすべて異なっており、多かれ少なかれプリミティブです。 おそらく最も強力なブルドーザー 、私は約2年間それで作業しており、いくつかのものが彼に合うのをやめました。 そして、ドーザーのさらなる開発の低迷のペースは、あなたの「バイク」を書くよう促されました(はい、私は他のマッパーに会いました-私たちの要件のために、彼らはさらに悪いです)。
悪いドーザーとは
- 注釈による不十分な構成サポート(
@Mapping
のみがあり@Mapping
)。 - 複数のフィールドから1つのフィールドにマップすることはできません(たとえば、名前、姓、および愛称から完全な名前を収集すること)。
- 汎用プロパティのマッピングに関する問題。 親抽象クラスにジェネリック型Tを返すゲッターがあり、ここ
, T , T. IEntity, , ..
プロパティクラスは文字列として内部ドーザーキャッシュに格納され、特別なローダークラスを使用してクラスを取得します。 この問題は、ドーザーが1つのバンドル内にあり、別のバンドル内の目的のBeanクラスに最初からアクセスできない場合に、osgi環境で発生します。 標準的な方法ではありますが、問題を克服しました-目的のローダークラスをスリップすることにより、実装自体:クラスを文字列として保存する-奇妙に見えます。 おそらく、これはperm genスペースメモリアルを作成しないためです。 しかし、まだあまり明確ではありません。
何かが突然マッピングされない場合、それを把握するのは非常に困難です。 ドーザーをデブートすると、その理由がわかります。 ある種の... OOPパターンのクレイジーな山があります-すべてが紛らわしく、明示的ではありません。 しかし、これは私の好みのためだけです。
マッパーにはどのような品質が必要ですか?
- 注釈による広範な構成サポート。
- 完全な汎用サポート。
- 誰もが脳を壊すことなくリスクを負うことができる、わかりやすくわかりやすいコード。
- デフォルトでは、追加の設定なしで、開発者が最も期待する方法でマップする必要があります。
- 微調整することが可能であるべきです(ドーザーより悪くない)。
マッパーではなく合併する理由
java-object-mergerは、1つの機能が他のマッパーと異なります。 基本的な考え方は、ある時点でスナップショットを有効にし、それらを比較して、2つのテキスト間の差分を見つける方法と同様の違い( diff )を見つけることでした。 さらに、スナップショットと差分を人間が読めるテキスト形式で表示できるようにする必要があります。 差分をすぐに見ると、差分を適用した後にターゲットがどのように変更されるかだけでなく、すべての違いが明確になります。 したがって、プロセスの完全な透明性を実現します。 魔法もブラックボックスもありません! スナップショットを作成すると、別の興味深いシナリオが開きます。 オブジェクトのスナップショットを作成してから、何らかの方法で変更し、新しいスナップショットを作成します。変更内容を確認し、必要に応じて変更をロールバックします。 ちなみに、特別な訪問者はdiffをバイパスし、適用したい変更のみをマークし、残りは無視できます。
したがって、合併は単なるマッパー以上のものであると言えます。
使用する
「Hello world」プログラムは次のようになります。
import net.sf.brunneng.jom.IMergingContext; import net.sf.brunneng.jom.MergingContext; public class Main { public static class A1 { private String field1; public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } } public static class A2 { private String field1; public A2(String field1) { this.field1 = field1; } public String getField1() { return field1; } } public static void main(String[] args) { IMergingContext context = new MergingContext(); A2 a2 = new A2("Hello world!"); A1 a1 = context.map(a2, A1.class); System.out.println(a1.getField1()); } }
まず、マッピングには、プロパティが両方のオブジェクトにゲッターを持っている必要があることがわかります。 これは値を比較するためのものです。 そして、新しい値を書き込むターゲットのセッター。 プロパティ自体には同じ名前を付ける必要があります。
mapメソッドの実装方法を見てみましょう。 これは、ライブラリに関する多くのことを理解するのに役立ちます。
@Override public <T> T map(Object source, Class<T> destinationClass) { Snapshot sourceSnapshot = createSnapshot(source); Snapshot destSnapshot = null; if (sourceSnapshot.getRoot().getType().equals(DataNodeType.BEAN)) { Object identifier = ((BeanDataNode)sourceSnapshot.getRoot()).getIdentifier(); if (identifier != null) { destSnapshot = createSnapshot(destinationClass, identifier); } } if (destSnapshot == null) { destSnapshot = createSnapshot(destinationClass); } Diff diff = destSnapshot.findDiffFrom(sourceSnapshot); diff.apply(); return (T)destSnapshot.getRoot().getObject(); }
ソーススナップショットがビンであり、識別子がある場合、IBeanFinder [ここでcreateSnapshot(destinationClass, identifier);
を使用して、destinationClassクラスのターゲットビンを見つけようとしますcreateSnapshot(destinationClass, identifier);
]。 そのようなものは登録しませんでしたし、識別子も登録しませんでした。 それ以外の場合、Beanは適切なIObjectCreator [ここではcreateSnapshot(destinationClass)
]を使用して作成されます。 そのようなものも登録しませんでしたが、標準の配信では、デフォルトのコンストラクターによってオブジェクトの作成者が存在します-これは使用されます。 次に、ソースのスナップショットからの差分がターゲットスナップショットから取得され、ターゲットに適用されます。 それだけです
ちなみに、この単純な場合のdiffは次のようになります。
MODIFY { dest object : Main$A1@28a38b58 src object : Main$A2@76f8d6a6 ADD { dest property : String field1 = null src property : String field1 = "Hello world!" } }
キー注釈
これらはパッケージnet.sf.brunneng.jom.annotations
ます。
-
@Mapping
関連付けのもう一方の端にあるマッピングフィールドへのパスを設定します(たとえば、“employee.person.firstName”
)。 ターゲットまたはソースオブジェクトのクラスで示される場合があります。 -
@Skip
フィールドはスナップショットに該当せず、比較もマップもされません。 -
@Identifier
-Beanの識別子と見なされるフィールドをマークします。 したがって、コレクションを比較するとき、どのオブジェクトをどのオブジェクトと比較する必要があるかがわかります。 つまり、一致する識別子を持つオブジェクトが比較されます。 また、diffを適用するプロセスでBeanを作成する必要があり、同時に識別子がわかっている場合は、登録済みのIBeanFinder
を使用して最初にこのBeanを見つけようとします。 そのため、IBeanFInder
の実装は、たとえばデータベース内でIBeanFInder
を検索できます。 -
@MapFromMany
と同じは、ターゲットオブジェクトのクラスでのみ示され、ターゲットオブジェクトのフィールドにマッピングされるソースオブジェクトのプロパティの配列を指定できます。 -
@Converter
クラスPropertyConverter
継承PropertyConverter
を設定できます。 -彼はプロパティ間の変換を実行します。 複数のフィールドを1つにマッピングする場合、プロパティコンバーターが必要です。 彼はソースからすべての値を一緒に収集し、それらから1つの値を形成する必要があります。 -
@OnPropertyChange, @OnBeanMappingStarted, @OnBeanMappingFinished
このBeanで発生するマッピングライフサイクルの対応するイベントをリッスンするメソッドをマークできます。 - その他。
型変換
IMergingContextでは、1つの型から別の型へのカスタム型コンバーターを登録できます(TypeConverter
インターフェイス)。 コンバーターの標準セットには、変換が含まれます。
- ラッパーのプリミティブ型とその逆
- 日付変換
- 行のオブジェクト
- 列挙から列挙へ、および列挙から定数への文字列
オブジェクトのカテゴリ
マッパーは、すべてのオブジェクトを次のようなカテゴリに分類します。
- 値オブジェクト:プリミティブ型、
java.lang
オブジェクト、日付、値オブジェクトの配列。 値と見なされるクラスのリストは、IMergingConext
介して展開できます。 - コレクションは配列であり、すべて
java.util.Collection
継承しています。 - マップはすべて
java.util.Map
から継承されます。 - 豆がすべてです。
性能
正直なところ、ライブラリを書いている間、パフォーマンスについてはあまり考えませんでした。 はい、当初は高性能という目標はありませんでした。 ただし、マッピング時間をテストオブジェクトごとにN回測定することにしました。 テストのソースコード 。 オブジェクトは非常に複雑で、値フィールド、子ビン、コレクション、マップがあります。 比較のために、ドーザーは現時点で最新バージョン5.4.0を使用しています。 彼は、投与者がチャンスを残さないことを期待していました。 しかし、それはまったく逆になりました! dozerは、32秒で5000個のテストオブジェクトをマップし、java-object-mergerは8秒で50,000個のオブジェクトをマップしました。 違いはいくつかの野生です-40回...
申込み
java-object-mergerは、私の主な仕事(osgi、spring、hibernate、数百のマッピングクラス)以降、現在のプロジェクトでテストされています。 それを交換するために、ドーザーは完全に1日未満かかりました。 途中でいくつかの明らかな学校がありましたが、修正後、すべての主要なシナリオは正常に機能しました。
遅延スナップショット
マッパーを実際のプロジェクトにねじ込むときに見つかった明らかな問題の1つは、他のエンティティの遅延リストを持つDTOでスナップショットを作成し、他のエンティティが3番目のものを参照する場合など、誤って1つのスナップショットを作成できることです。ベースの床を収縮させます。 したがって、スナップショットのすべてのプロパティをデフォルトで遅延させることにしました。 これは、差分を取るときに対応するプロパティと比較されるまでオブジェクトから引き出されないことを意味します。 または、スナップショットでloadLazyProperties()
メソッドを明示的に呼び出すまで。 そして、プロパティを引き出すと、スナップショットが自動的に完了します-再びプロパティがロードされるまで待機する遅延プロパティがあります。
おわりに
興味がある場合-ソースコードとドキュメントを含むプロジェクトはこちらです。 ライブラリのすべての主な機能は単体テストでカバーされているため、バカげた些細なエラーが表示されないことを確認できます。 ほとんどすべてのクラスとメソッドはjavadocによって文書化されています。
ダウンロードして、レビューを書いてみてください:)。 私は迅速に対応し、あなたの願いに耳を傾けることを約束します。