Dozerによるマッピングの手引き

1概要

Dozer Java BeanからJava Beanへのマッパー で、属性ごとにあるオブジェクトから別のオブジェクトにデータを再帰的にコピーします。

このライブラリは、Java Beansの属性名間のマッピングをサポートするだけでなく、タイプ間で** 自動的に変換されます。

ほとんどの変換シナリオはそのまま使用できますが、DozerではXMLを介してカスタム変換を指定することもできます。

2簡単な例

最初の例では、送信元と送信先のデータオブジェクトがすべて同じ共通の属性名を共有しているとします。

これはDozerでできる最も基本的なマッピングです。

public class Source {
    private String name;
    private int age;

    public Source() {}

    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }

   //standard getters and setters
}

それから私達の行き先ファイル、 Dest.java :

public class Dest {
    private String name;
    private int age;

    public Dest() {}

    public Dest(String name, int age) {
        this.name = name;
        this.age = age;
    }

   //standard getters and setters
}

Dozerはフードの下でリフレクションを使用しているので、私たちは デフォルトまたはゼロ引数のコンストラクタ を含めることを確認する必要があります。

そして、パフォーマンス上の理由から、マッパーをグローバルにして、テスト全体を通して使用する単一のオブジェクトを作成しましょう。

DozerBeanMapper mapper;

@Before
public void before() throws Exception {
    mapper = new DozerBeanMapper();
}

それでは、最初のテストを実行して、 Source オブジェクトを作成したら、それを Dest オブジェクトに直接マッピングできることを確認します。

@Test
public void givenSourceObjectAndDestClass__whenMapsSameNameFieldsCorrectly__
  thenCorrect() {
    Source source = new Source("Baeldung", 10);
    Dest dest = mapper.map(source, Dest.class);

    assertEquals(dest.getName(), "Baeldung");
    assertEquals(dest.getAge(), 10);
}

ご覧のとおり、Dozerマッピングの後、結果は Source オブジェクトと同じフィールド名を持つすべてのフィールドの値を含む Dest オブジェクトの新しいインスタンスになります。

別の方法として、 mapper Dest クラスに渡す代わりに、 Dest オブジェクトを作成してその参照を mapper に渡すだけで済みます。

@Test
public void givenSourceObjectAndDestObject__whenMapsSameNameFieldsCorrectly__
  thenCorrect() {
    Source source = new Source("Baeldung", 10);
    Dest dest = new Dest();
    mapper.map(source, dest);

    assertEquals(dest.getName(), "Baeldung");
    assertEquals(dest.getAge(), 10);
}

3 Mavenのセットアップ

Dozerのしくみについて基本的な理解ができたので、 pom.xml に次の依存関係を追加しましょう。

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

最新版はhttps://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22dozer%22[ここ]です。

4データ変換例

すでにご存知のように、Dozerは両方のクラスで同じ名前の属性を見つける限り、既存のオブジェクトを別のオブジェクトにマップできます。

しかし、必ずしもそうとは限りません。したがって、マップされた属性のいずれかが異なるデータ型の場合、Dozerマッピングエンジンは 自動的にデータ型変換 を実行します。

この新しい概念を実際に見てみましょう。

public class Source2 {
    private String id;
    private double points;

    public Source2() {}

    public Source2(String id, double points) {
        this.id = id;
        this.points = points;
    }

   //standard getters and setters
}

そして宛先クラス:

public class Dest2 {
    private int id;
    private int points;

    public Dest2() {}

    public Dest2(int id, int points) {
        super();
        this.id = id;
        this.points = points;
    }

   //standard getters and setters
}

属性名は同じですが** データ型が異なります。

ソースクラスでは、 id String で、 points double ですが、デスティネーションクラスでは、 id points はどちらも __integer __です。

Dozerがどのように変換を正しく処理するのかを見てみましょう。

@Test
public void givenSourceAndDestWithDifferentFieldTypes__
  whenMapsAndAutoConverts__thenCorrect() {
    Source2 source = new Source2("320", 15.2);
    Dest2 dest = mapper.map(source, Dest2.class);

    assertEquals(dest.getId(), 320);
    assertEquals(dest.getPoints(), 15);
}

“ 320” 15.2 String double をソースオブジェクトに渡した結果、デスティネーションオブジェクトには 320 15、 両方が __integer __がありました。

5 XML による基本的なカスタムマッピング

これまで見てきたすべての例では、送信元と送信先の両方のデータオブジェクトが同じフィールド名を持っているため、簡単にマッピングできます。

ただし、実際のアプリケーションでは、マッピングしている2つのデータオブジェクトに共通のプロパティ名を共有するフィールドがない場合が無数にあります。

これを解決するために、DozerはXMLでカスタムマッピング設定を作成するオプションを提供しています。

このXMLファイルでは、Dozerマッピングエンジンがどのソース属性をどの宛先属性にマップするかを決定するために使用するクラスマッピングエントリを定義できます。

例を見てみましょう。そして、フランスのプログラマーによって構築されたアプリケーションからのデータオブジェクトを、私たちのオブジェクトを命名する英語スタイルに非整列化することを試みましょう。

name nickname 、および age フィールドを持つ Person オブジェクトがあります。

public class Person {
    private String name;
    private String nickname;
    private int age;

    public Person() {}

    public Person(String name, String nickname, int age) {
        super();
        this.name = name;
        this.nickname = nickname;
        this.age = age;
    }

   //standard getters and setters
}

非整列化しているオブジェクトは Personne という名前で、 nom surnom 、および age というフィールドがあります。

public class Personne {
    private String nom;
    private String surnom;
    private int age;

    public Personne() {}

    public Personne(String nom, String surnom, int age) {
        super();
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }

   //standard getters and setters
}

これらのオブジェクトは実際に同じ目的を達成しますが、言語の壁があります。その障害を解決するために、Dozerを使用してフランス語の Personne オブジェクトを Person オブジェクトにマップできます。

Dozerがこれを行えるようにするためには、カスタムマッピングファイルを作成するだけです。これを dozer mapping.xml__と呼びます。

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net
      http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <mapping>
        <class-a>com.baeldung.dozer.Personne</class-a>
        <class-b>com.baeldung.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

これは私たちが持つことができるカスタムXMLマッピングファイルの最も簡単な例です。

今のところ、ルート要素として <mappings> があり、その子要素は <mapping> です。クラスの出現回数と同じだけ、これらの子を <mapping> の内側に配置できますカスタムマッピングが必要なペア

<mapping> </mapping> タグ内でソースクラスとデスティネーションクラスを指定する方法にも注意してください。これには、カスタムマッピングが必要な各ソースフィールドと宛先フィールドのペアごとに <field> </field> が続きます。

最後に、カスタムマッピングファイルにフィールド age が含まれていないことに注意してください。年齢のためのフランスの言葉はまだ年齢です、そしてそれは我々にドーザーのもう一つの重要な特徴をもたらします。

  • マッピングXMLファイルで同じ名前のプロパティを指定する必要はありません。 Dozerは、ソースオブジェクトの同じプロパティ名を持つすべてのフィールドを自動的にデスティネーションオブジェクトにマッピングします。

次に、カスタムXMLファイルを src フォルダーの直下のクラスパスに配置します。ただし、クラスパスのどこに配置しても、Dozerはクラスパス全体を検索して指定されたファイルを探します。

マッピングファイルを mapper に追加するためのヘルパーメソッドを作成しましょう。

public void configureMapper(String... mappingFileUrls) {
    mapper.setMappingFiles(Arrays.asList(mappingFileUrls));
}

それではコードをテストしましょう。

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper__
  whenMaps__thenCorrect() {
    configureMapper("dozer__mapping.xml");
    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

テストに示されているように、 DozerBeanMapper はカスタムXMLマッピングファイルのリストを受け入れ、実行時にいつそれらを使用するかを決定します。

これらのデータオブジェクトを英語のアプリとフランス語のアプリの間でやり取りするようになったとします。 XMLファイル内に別のマッピングを作成する必要はありません。** Dozerは、1つのマッピング設定のみで双方向にオブジェクトをマッピングするのに十分スマートです。

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper__
  whenMapsBidirectionally__thenCorrect() {
    configureMapper("dozer__mapping.xml");
    Person englishAppPerson = new Person("Dwayne Johnson", "The Rock", 44);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

そのため、このテスト例ではDozerのこの別の機能を使用しています。つまり、Dozerマッピングエンジンは双方向です。 XMLファイル

必要に応じて、リソース名に「 file: 」という接頭辞を使用して、クラスパスの外部からカスタムマッピングファイルをロードすることもできます。

Windows環境(下記のテストなど)では、もちろんWindows固有のファイル構文を使用します。

Linuxマシンでは、ファイルを /home に保存してから、次のようにします。

configureMapper("file:/home/dozer__mapping.xml");

そしてMac OSでは:

configureMapper("file:/Users/me/dozer__mapping.xml");

githubプロジェクト (単体テスト)から単体テストを実行している場合、マッピングファイルを適切な場所にコピーして入力を変更できます configureMapper メソッドの場合

マッピングファイルはGitHubプロジェクトのtest/resourcesフォルダーにあります。

@Test
public void givenMappingFileOutsideClasspath__whenMaps__thenCorrect() {
    configureMapper("file:E:\\dozer__mapping.xml");
    Person englishAppPerson = new Person("Marshall Bruce Mathers III","Eminem", 43);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

** 6. ワイルドカードとさらなるXMLのカスタマイズ

**

dozer mapping2.xml__という2番目のカスタムマッピングファイルを作成しましょう。

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net
      http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <mapping wildcard="false">
        <class-a>com.baeldung.dozer.Personne</class-a>
        <class-b>com.baeldung.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

<mapping> </mapping> 要素に属性 wildcard を追加したことに注目してください。

デフォルトでは、 wildcard true です。これは、Dozerエンジンに、ソースオブジェクト内のすべてのフィールドをそれらの適切な宛先フィールドにマップしたいことを伝えます。

これを__falseに設定すると、DozerにXMLで明示的に指定したフィールドのみをマップするように指示します。

したがって、上記の設定では、 age を除外して、2つのフィールドのみをマップする必要があります。

@Test
public void givenSrcAndDest__whenMapsOnlySpecifiedFields__thenCorrect() {
    configureMapper("dozer__mapping2.xml");
    Person englishAppPerson = new Person("Shawn Corey Carter","Jay Z", 46);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

最後の表明からわかるように、destination age フィールドは 0 のままです。

7. 注釈によるカスタムマッピング

単純なマッピングの場合や、マッピングしたいデータオブジェクトへの書き込みアクセス権もある場合は、XMLマッピングを使用する必要はありません。

アノテーションを使用して異なる名前のフィールドをマッピングするのは非常に簡単で、XMLマッピングよりもはるかに少ないコードで作成する必要がありますが、単純な場合にのみ役立ちます。

フィールドをまったく変更せずに、データオブジェクトを Person2.java Personne2.java に複製しましょう。

これを実装するには、ソースオブジェクトの getter メソッドに@ mapper(“ destinationFieldName”) アノテーションを追加するだけです。そのようです:

@Mapping("name")
public String getNom() {
    return nom;
}

@Mapping("nickname")
public String getSurnom() {
    return surnom;
}

今回は Personne2 をソースとして扱っていますが、Dozerエンジンの 双方向性 のために問題にはなりません。

XML関連のコードがすべて削除されたため、テストコードは短くなりました。

@Test
public void givenAnnotatedSrcFields__whenMapsToRightDestField__thenCorrect() {
    Person2 englishAppPerson = new Person2("Jean-Claude Van Damme", "JCVD", 55);
    Personne2 frenchAppPerson = mapper.map(englishAppPerson, Personne2.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

双方向性をテストすることもできます。

@Test
public void givenAnnotatedSrcFields__whenMapsToRightDestFieldBidirectionally__
  thenCorrect() {
    Personne2 frenchAppPerson = new Personne2("Jason Statham", "transporter", 49);
    Person2 englishAppPerson = mapper.map(frenchAppPerson, Person2.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

8カスタムAPIマッピング

フランス語のアプリケーションからデータオブジェクトを非整列化する前の例では、マッピングをカスタマイズするためにXMLと注釈を使用しました。

アノテーションマッピングと同様に、Dozerで利用可能なもう1つの選択肢はAPIマッピングです。 XML設定を排除し、厳密にJavaコードを使用するため、これらは似ています。

この場合、 BeanMappingBuilder クラスを使用します。これは、最も単純なケースで次のように定義されています。

BeanMappingBuilder builder = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom");
    }
};

ご覧のとおり、 configure() という抽象メソッドがあります。これは設定を定義するためにオーバーライドする必要があります。それから、XMLの <mapping> </mapping> タグと同じように、必要なだけ __TypeMappingBuilder __を定義します。

これらのビルダーは、Dozerにどのソースから宛先フィールドへのマッピングを行うのかを指示します。それから、XMLマッピングファイルと同じように、 BeanMappingBuilder DozerBeanMapper に渡しますが、別のAPIのみを使用します。

@Test
public void givenApiMapper__whenMaps__thenCorrect() {
    mapper.addMapping(builder);

    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

マッピングAPIも双方向です。

@Test
public void givenApiMapper__whenMapsBidirectionally__thenCorrect() {
    mapper.addMapping(builder);

    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

あるいは、このビルダー設定で明示的に指定されたフィールドのみをマッピングすることを選択できます。

BeanMappingBuilder builderMinusAge = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom")
              .exclude("age");
    }
};

そして age == 0 テストが帰ってきました:

@Test
public void givenApiMapper__whenMapsOnlySpecifiedFields__thenCorrect() {
    mapper.addMapping(builderMinusAge);
    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

9カスタムコンバーター

マッピングで直面する可能性があるもう1つのシナリオは、2つのオブジェクト間でカスタムマッピングを実行したい場合です。

フランスの Personne オブジェクトのように、送信元と送信先のフィールド名が異なるシナリオを調べました。このセクションは別の問題を解決します。

非整列化しているデータオブジェクトが long やUnix timeのような日時フィールドを表しているとしたらどうでしょうか。

1182882159000

しかし、私たち自身の同等のデータオブジェクトは String: のようにこのISOフォーマットで同じ日時フィールドと値を表します。

2007-06-26T21:22:39Z

デフォルトのコンバーターは、単純にlong値を String にマッピングします。

"1182882159000"

これは間違いなく我々のアプリをバグにするでしょう。では、どうやってこれを解決するのでしょうか。マッピングXMLファイルに設定ブロックを追加し、独自のコンバータを指定することで解決します。

最初に、リモートアプリケーションの Person DTOを name、次に生年月日、 dtob__フィールドで複製しましょう。

public class Personne3 {
    private String name;
    private long dtob;

    public Personne3(String name, long dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

   //standard getters and setters
}

そしてここに私たち自身のものがあります。

public class Person3 {
    private String name;
    private String dtob;

    public Person3(String name, String dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

   //standard getters and setters
}

ソースDTOと宛先DTOの dtob の型の違いに注目してください。

マッピングXMLでDozerに渡すための独自の CustomConverter も作成しましょう。

public class MyCustomConvertor implements CustomConverter {
    @Override
    public Object convert(Object dest, Object source, Class<?> arg2, Class<?> arg3) {
        if (source == null)
            return null;

        if (source instanceof Personne3) {
            Personne3 person = (Personne3) source;
            Date date = new Date(person.getDtob());
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            String isoDate = format.format(date);
            return new Person3(person.getName(), isoDate);

        } else if (source instanceof Person3) {
            Person3 person = (Person3) source;
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            Date date = format.parse(person.getDtob());
            long timestamp = date.getTime();
            return new Personne3(person.getName(), timestamp);
        }
    }
}

convert() メソッドをオーバーライドするだけで、その後は戻りたいものをすべて返すだけです。送信元オブジェクトと送信先オブジェクト、およびそれらのクラスタイプが利用できます。

ソースが、マッピングしている2つのクラスのいずれかになる可能性があると想定して、双方向性に注意を払ったことに注目してください。

わかりやすくするために、新しいマッピングファイル dozer custom convertor.xml を作成します。

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net
      http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <configuration>
        <custom-converters>
            <converter type="com.baeldung.dozer.MyCustomConvertor">
                <class-a>com.baeldung.dozer.Personne3</class-a>
                <class-b>com.baeldung.dozer.Person3</class-b>
            </converter>
        </custom-converters>
    </configuration>
</mappings>

これは、前のセクションで見た通常のマッピングファイルです。追加した <configuration> </configuration> ブロックには、それぞれのソースデータクラスと宛先データクラスで必要な数のカスタムコンバータを定義できます。

新しい CustomConverter コードをテストしましょう。

@Test
public void givenSrcAndDestWithDifferentFieldTypes__whenAbleToCustomConvert__
  thenCorrect() {

    configureMapper("dozer__custom__convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Person3 person = new Person3("Rich", dateTime);
    Personne3 person0 = mapper.map(person, Personne3.class);

    assertEquals(timestamp, person0.getDtob());
}

双方向であることを確認するためにテストすることもできます。

@Test
public void givenSrcAndDestWithDifferentFieldTypes__
  whenAbleToCustomConvertBidirectionally__thenCorrect() {
    configureMapper("dozer__custom__convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Personne3 person = new Personne3("Rich", timestamp);
    Person3 person0 = mapper.map(person, Person3.class);

    assertEquals(dateTime, person0.getDtob());
}

10結論

このチュートリアルでは、Dozer Mappingライブラリの基本のほとんどと、それをアプリケーションで使用する方法を紹介しました。

これらすべての例とコードスニペットの完全な実装は、Dozerのhttps://github.com/eugenp/tutorials/tree/master/dozer[githubプロジェクト]にあります。