5.21.1. Beanマッピング(Dozer)


5.21.1.1. Overview

Beanマッピングは、一つのBeanを他のBeanにフィールド値をコピーすることである。

アプリケーションの異なるレイヤ間(アプリケーション層とドメイン層)で、データの受け渡しをする場合など、Beanマッピングが必要となるケースは多い。
例として、アプリケーション層のAccountFormオブジェクトを、ドメイン層のAccountオブジェクトに変換する場合を考える。
ドメイン層は、アプリケーション層に依存してはならないため、AccountFormオブジェクトをそのままドメイン層で使用できない。
そこで、AccountFormオブジェクトを、AccountオブジェクトにBeanマッピングし、ドメイン層では、Accountオブジェクトを使用する。
これによって、アプリケーション層と、ドメイン層の依存関係を一方向に保つことができる。
../../_images/beanmapping-overview.png
このオブジェクト間のマッピングは、Beanのgetter/setterを呼び出して、データの受け渡しを行うことで実現できる。
しかしながら、処理が煩雑になり、プログラムの見通しが悪くなるため、本ガイドラインでは、BeanマッピングライブラリであるOSSで利用可能な Dozer を使用することを推奨する。
Dozerを使用することで下図のように、コピー元クラスとコピー先クラスで型が異なるコピーや、ネストしたBean同士のコピーも容易に行うことができる。
../../_images/dozer-functionality-overview.png

Dozerをした場合と使用しない場合のコード例を挙げる。

  • 煩雑になり、プログラムの見通しが悪くなる例

    User user = userService.findById(userId);
    
    XxxOutput output = new XxxOutput();
    
    output.setUserId(user.getUserId());
    output.setFirstName(user.getFirstName());
    output.setLastName(user.getLastName());
    output.setTitle(user.getTitle());
    output.setBirthDay(user.getBirthDay());
    output.setGender(user.getGender());
    output.setStatus(user.getStatus());
    
  • Dozerを使用した場合の例

    User user = userService.findById(userId);
    
    XxxOutput output = beanMapper.map(user, XxxOutput.class);
    

以降は、Dozerの利用方法について説明する。

5.21.1.2. How to use

Dozerは、Java Beanのマッピング機能ライブラリである。 変換元のBeanから変換先のBeanに、再帰的(ネストした構造)に、値をコピーする。

5.21.1.2.1. Dozerを使用するためのBean定義

Dozerは、単独で使用するとき、以下のように、 org.dozer.Mapper のインスタンスを作成する。

Mapper mapper = new DozerBeanMapper();

Mapper のインスタンスを毎回作成するのは、効率が悪いため、 Dozerが提供している org.dozer.spring.DozerBeanMapperFactoryBean を使用すること。

Bean定義ファイル(applicationContext.xml)に、Mapperを作成するFactoryクラスであるorg.dozer.spring.DozerBeanMapperFactoryBeanを定義する

<bean class="org.dozer.spring.DozerBeanMapperFactoryBean">
    <property name="mappingFiles"
        value="classpath*:/META-INF/dozer/**/*-mapping.xml" /><!-- (1) -->
</bean>
項番 説明
(1)
mappingFilesに、マッピング定義XMLファイルを指定する。
org.dozer.spring.DozerBeanMapperFactoryBean は、 interfaceとして org.dozer.Mapper を保持している。そのため、 @Inject 時は Mapper を指定する。
この例では、クラスパス直下の、/META-INF/dozerの任意フォルダ内の
(任意の値)-mapping.xmlを、すべて読み込む。このXMLファイルの内容については、以降で説明する。

Beanマッピングを行いたいクラスに、 Mapper をインジェクトすればよい。

@Inject
Mapper beanMapper;

5.21.1.2.2. Bean間のフィールド名、型が同じ場合のマッピング

デフォルトの動作として、Dozerは対象のBean間のフィールド名が同じであれば、マッピング定義XMLファイルを作成せずにマッピングできる。

変換元のBean定義

public class Source {
    private int id;
    private String name;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private int id;
    private String name;
    // ommited setter/getter
}

以下のように、 Mappermap メソッドを使ってBeanマッピングを行う。 下記メソッドを実行した後、Destinationオブジェクトが新たに作成され、sourceの各フィールドの値が作成されたDestinationオブジェクトにコピーされる。

Source source = new Source();
source.setId(1);
source.setName("SourceName");
Destination destination = beanMapper.map(source, Destination.class); // (1)
System.out.println(destination.getId());
System.out.println(destination.getName());
項番 説明
(1)
第一引数に、コピー元のオブジェクトを渡し、第二引数に、コピー先のBeanのクラスを渡す。

上記のコードを実行すると以下のように出力される。作成されたオブジェクトにコピー元のオブジェクトの値が設定されていることが分かる。

1
SourceName

既に存在しているdestinationオブジェクトに、sourceオブジェクトのフィールドをコピーしたい場合は、

Source source = new Source();
source.setId(1);
source.setName("SourceName");
Destination destination = new Destination();
destination.setId(2);
destination.setName("DestinationName");
beanMapper.map(source, destination); // (1)
System.out.println(destination.getId());
System.out.println(destination.getName());
項番 説明
(1)
第一引数に、コピー元のオブジェクトを渡し、第二引数に、コピー先のオブジェクトを渡す。

上記のコードを実行すると以下のように出力される。コピー元のオブジェクトの値がコピー先に反映されていることが分かる。

1
SourceName

Note

DestinationクラスのフィールドでSourceクラスに存在しないものは、コピー前後で値は変わらない。

変換元のBean定義

public class Source {
    private int id;
    private String name;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private int id;
    private String name;
    private String title;
    // ommited setter/getter
}

マッピング例

Source source = new Source();
source.setId(1);
source.setName("SourceName");
Destination destination = new Destination();
destination.setId(2);
destination.setName("DestinationName");
destination.setTitle("DestinationTitle");
beanMapper.map(source, destination);
System.out.println(destination.getId());
System.out.println(destination.getName());
System.out.println(destination.getTitle());

上記のコードを実行すると以下のように出力される。Sourceクラスにはtitleフィールドが ないため、Destinationオブジェクトのtitleフィールドは、コピー前のフィールド値から変更がない。

1
SourceName
DestinationTitle

5.21.1.2.3. Bean間のフィールド名は同じ、型が異なる場合のマッピング

コピー元と、コピー先でBeanのフィールドの型が異なる場合、 型変換がサポートされている型は、自動でマッピングできる。

以下のような変換は、マッピング定義XMLファイル無しで変換できる。

例 : String -> BigDecimal

変換元のBean定義

public class Source {
    private String amount;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private BigDecimal amount;
    // ommited setter/getter
}

マッピング例

Source source = new Source();
source.setAmount("123.45");
Destination destination = beanMapper.map(source, Destination.class);
System.out.println(destination.getAmount());

上記のコードを実行すると以下のように出力される。型が異なる場合でも値をコピーできていることが分かる。

123.45

サポートされている型変換については、 マニュアル を参照されたい。

5.21.1.2.4. Bean間のフィールド名が異なる場合のマッピング

コピー元と、コピー先でフィールド名が異なる場合、マッピング定義XMLファイルを作成し、 Beanマッピングするフィールドを定義することで変換できる。

変換元のBean定義

public class Source {
    private int id;
    private String name;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private int destinationId;
    private String destinationName;
    // ommited setter/getter
}

Dozerを使用するためのBean定義の定義がある場合、 src/main/resources/META-INF/dozerフォルダ内に、(任意の値)-mapping.xmlという、マッピング定義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.xx.xx.Source</class-a><!-- (1) -->
      <class-b>com.xx.xx.Destination</class-b><!-- (2) -->
      <field>
        <a>id</a><!-- (3) -->
        <b>destinationId</b><!-- (4) -->
      </field>
      <field>
        <a>name</a>
        <b>destinationName</b>
      </field>
    </mapping>

</mappings>
項番 説明
(1)
<class-a>タグ内にコピー元のBeanの、完全修飾クラス名(FQCN)を指定する。
(2)
<class-b>タグ内にコピー先のBeanの、完全修飾クラス名(FQCN)を指定する。
(3)
<field>タグ内の<a>タグ内にコピー元のBeanの、マッピング用のフィールド名を指定する。
(4)
<field>タグ内の<b>タグ内に(3)に対応するコピー先のBeanの、マッピング用のフィールド名を指定する。

マッピング例

Source source = new Source();
source.setId(1);
source.setName("SourceName");
Destination destination = beanMapper.map(source, Destination.class); // (1)
System.out.println(destination.getDestinationId());
System.out.println(destination.getDestinationName());
項番 説明
(1)
第一引数に、コピー元のオブジェクトを渡し、第二引数に、コピー先のBeanのクラスを渡す。(基本マッピングと違いはない。)

上記のコードを実行すると以下のように出力される。

1
SourceName

Dozerを使用するためのBean定義の設定によって、mappingFilesプロパティにクラスパス直下のMETA-INF/dozer配下に存在するマッピング定義XMLファイルが読み込まれる。 ファイル名は(任意の値)-mapping.xmlである必要がある。 いずれかのファイル内にSourceクラスとDestinationクラス間におけるマッピング定義があれば、その設定が適用される。

Note

マッピング定義XMLファイルは、Controller単位で作成し、ファイル名は、(Controller名からControllerを除いた値)-mapping.xmlにすることを推奨する。 例えば、TodoControllerに対するマッピング定義XMLファイルは、src/main/resources/META-INF/dozer/todo-mapping.xmlに作成する。


5.21.1.2.5. 単方向・双方向マッピング

マッピングXMLで定義されているマッピングは、デフォルトで、双方向マッピングである。 すなわち前述の例ではSourceオブジェクトからDestinationオブジェクトへのマッピングを行ったが、 DestinationオブジェクトからSourceオブジェクトのマッピングも可能である。

単方向をのみを指定したい場合は、マッピング・フィールド定義に、<mapping>タグのtype属性に"one-way"を設定する。

<?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">
        <!-- omitted -->
        <mapping type="one-way">
              <class-a>com.xx.xx.Source</class-a>
              <class-b>com.xx.xx.Destination</class-b>
                <field>
                  <a>id</a>
                  <b>destinationId</b>
                </field>
                <field>
                  <a>name</a>
                  <b>destinationName</b>
                </field>
        </mapping>
        <!-- omitted -->
</mappings>

変換元のBean定義

public class Source {
    private int id;
    private String name;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private int destinationId;
    private String destinationName;
    // ommited setter/getter
}

マッピング例

Source source = new Source();
source.setId(1);
source.setName("SourceName");
Destination destination = beanMapper.map(source, Destination.class);
System.out.println(destination.getDestinationId());
System.out.println(destination.getDestinationName());

上記のコードを実行すると以下のように出力される。

1
SourceName

単方向を指定している場合に、逆方向のマッピングを行ってもエラーは発生しない。コピー処理は無視される。 なぜなら、マッピング定義がないとDestinationのフィールドに該当するSourceのフィールドが存在ないとみなされるためである。

Destination destination = new Destination();
destination.setDestinationId(2);
destination.setDestinationName("DestinationName");

Source source = new Source();
source.setId(1);
source.setName("SourceName");

beanMapper.map(destination, source);

System.out.println(source.getId());
System.out.println(source.getName());

上記のコードを実行すると以下のように出力される。

1
SourceName

5.21.1.2.6. Nestしたフィールドのマッピング

コピー元Beanが持つフィールドを、コピー先Beanが持つNestしたBeanのフィールドにも、マッピングできることである。 (Dozerの用語で、 Deep Mapping と呼ばれる。)

変換元のBean定義

public class EmployeeForm {
    private int id;
    private String name;
    private String deptId;
    // omitted setter/getter
}

変換先のBean定義

public class Employee {
    private Integer id;
    private String name;
    private Department department;
    // omitted setter/getter
}
public class Department {
    private String deptId;
    // omitted setter/getter and other fields
}

例 : EmployeeFormオブジェクトが持つdeptIdを、Employeeオブジェクトが持つDepartmentdeptIdにマップしたい場合、 以下のように定義する。

<?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">
    <!-- omitted -->
    <mapping map-empty-string="false" map-null="false">
        <class-a>com.xx.aa.EmployeeForm</class-a>
        <class-b>com.xx.bb.Employee</class-b>
        <field>
              <a>deptId</a>
              <b>department.deptId</b><!-- (1) -->
        </field>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
EmployeeフォームのdeptIdに対する、Employeeオブジェクトのフィールドを指定する。

マッピング例

EmployeeForm source = new EmployeeForm();
source.setId(1);
source.setName("John");
source.setDeptId("D01");

Employee destination = beanMapper.map(source, Employee.class);
System.out.println(destination.getId());
System.out.println(destination.getName());
System.out.println(destination.getDepartment().getDeptId());

上記のコードを実行すると以下のように出力される。

1
John
D01

上記の場合は、変換先クラスであるEmployeeの新規インスタンスが作成される。 Employeeの中のdepartment フィールドにも、新規に作成されたDepartmentインスタンスが設定され、 EmployeeFormdeptIdが、コピーされる。

下記ようにEmployeeの中のdepartment フィールドに既にDepartmentオブジェクトが設定されている場合は、 新規インスタンスは作成されず、既存のDepartmentオブジェクトのdeptIdフィールドに、 EmployeeFormdeptIdがコピーされる。

EmployeeForm source = new EmployeeForm();
source.setId(1);
source.setName("John");
source.setDeptId("D01");

Employee destination = new Employee();
Department department = new Department();
destination.setDepartment(department);

beanMapper.map(source, destination);
System.out.println(department.getDeptId());
System.out.println(destination.getDepartment() == department);

上記のコードを実行すると以下のように出力される。

D01
true

5.21.1.2.7. Collectionマッピング

Dozerは、以下のCollectionタイプの双方向自動マッピングをサポートしている。 フィールド名が同じである場合、マッピング定義XMLファイルが不要である。

  • java.util.List <=> java.util.List
  • java.util.List <=> Array
  • Array <=> Array
  • java.util.Set <=> java.util.Set
  • java.util.Set <=> Array
  • java.util.Set <=> java.util.List

次のクラスのコレクションをもつBeanのマッピングについて考える。

package com.example.dozer;

public class Email {
    private String email;

    public Email() {
    }

    public Email(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return email;
    }

    // generated by Eclipse
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((email == null) ? 0 : email.hashCode());
        return result;
    }

    // generated by Eclipse
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Email other = (Email) obj;
        if (email == null) {
            if (other.email != null)
                return false;
        } else if (!email.equals(other.email))
            return false;
        return true;
    }

}

変換元のBean

package com.example.dozer;

import java.util.List;

public class AccountForm {
    private List<Email> emails;

    public void setEmails(List<Email> emails) {
        this.emails = emails;
    }

    public List<Email> getEmails() {
        return emails;
    }
}

変換先のBean

package com.example.dozer;

import java.util.List;

public class Account {
    private List<Email> emails;

    public void setEmails(List<Email> emails) {
        this.emails = emails;
    }

    public List<Email> getEmails() {
        return emails;
    }
}

マッピング例

AccountForm accountForm = new AccountForm();

List<Email> emailsSrc = new ArrayList<Email>();

emailsSrc.add(new Email("a@example.com"));
emailsSrc.add(new Email("b@example.com"));
emailsSrc.add(new Email("c@example.com"));

accountForm.setEmails(emailsSrc);

Account account = beanMapper.map(accountForm, Account.class);

System.out.println(account.getEmails());

上記のコードを実行すると以下のように出力される。

[a@example.com, b@example.com, c@example.com]

ここまではこれまで説明したことと特に変わりはない。

次の例のように、コピー先のBeanのCollectionフィールドに既に要素が追加されている場合は要注意である。

AccountForm accountForm = new AccountForm();
Account account = new Account();

List<Email> emailsSrc = new ArrayList<Email>();
List<Email> emailsDest = new ArrayList<Email>();

emailsSrc.add(new Email("a@example.com"));
emailsSrc.add(new Email("b@example.com"));
emailsSrc.add(new Email("c@example.com"));

emailsDest.add(new Email("a@example.com"));
emailsDest.add(new Email("d@example.com"));
emailsDest.add(new Email("e@example.com"));

accountForm.setEmails(emailsSrc);
account.setEmails(emailsDest);

beanMapper.map(accountForm, account);

System.out.println(account.getEmails());

上記のコードを実行すると以下のように出力される。

[a@example.com, d@example.com, e@example.com, a@example.com, b@example.com, c@example.com]

コピー元BeanのCollectionの全要素が、コピー先BeanのCollectionに追加されている。 a@exmample.comをもつ2つのEmailオブジェクトは”等価”であるが、単純に追加される。

(ここでいう”等価”とはEmail.equals で比較するとtrueになり、Email.hashCodeの値も同じであることを意味する。)

上記の振る舞いは、Dozerの用語ではcumulativeと呼ばれ、Collectionをマッピングする際のデフォルトの挙動となっている。

この挙動はマッピング定義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">
    <!-- omitted -->
    <mapping>
        <class-a>com.example.dozer.AccountForm</class-a>
        <class-b>com.example.dozer.Account</class-b>
        <field relationship-type="non-cumulative"><!-- (1) -->
            <a>emails</a>
            <b>emails</b>
        </field>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
<field>タグのrelationship-type属性にnon-cumulativeを指定する。デフォルト値はcumulativeである。

マッピング対象のBeanの全フィールドに対してnon-cumulativeを指定したい場合は、<mapping>タグのrelationship-type属性にnon-cumulativeを指定することもできる。

この設定のもと、前述のコードを実行すると以下のように出力される。

[a@example.com, d@example.com, e@example.com, b@example.com, c@example.com]

等価であるオブジェクトの重複がなくなっていることが分かる。

Note

変換元のオブジェクトが、変換先のオブジェクトで更新されることに注意されたい。 上記の例ではAccountFormの中のa@exmample.comがコピー先に格納される。

noncumulative update using dozer

コピー先のコレクションにのみに存在する項目は除外したい場合も、マッピング定義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">
    <!-- omitted -->
    <mapping>
        <class-a>com.example.dozer.AccountForm</class-a>
        <class-b>com.example.dozer.Account</class-b>
        <field relationship-type="non-cumulative" remove-orphans="true" ><!-- (1) -->
            <a>emails</a>
            <b>emails</b>
        </field>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
<field>タグのremove-orphans属性にtrueを設定する。デフォルト値はfalseである。

この設定のもと、前述のコードを実行すると以下のように出力される。

[a@example.com, b@example.com, c@example.com]

コピー元にあるオブジェクトだけがコピー先のコレクション内に残っていることが分かる。

いかのように設定しても同じ結果が得られる。

<?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">
    <!-- omitted -->
    <mapping>
        <class-a>com.example.dozer.AccountForm</class-a>
        <class-b>com.example.dozer.Account</class-b>
        <field copy-by-reference="true"><!-- (1) -->
            <a>emails</a>
            <b>emails</b>
        </field>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
<field>タグのcopy-by-reference属性にtrueを設定する。デフォルト値はfalseである。

これまでの挙動を図で表現する。

  • デフォルトの挙動(cumulative)

    ../../_images/dozer-collection-cumulative.png
  • non-cumulative

    ../../_images/dozer-collection-non-cumulative.png
  • non-cumulativeかつremove-orphans=true

    ../../_images/dozer-collection-non-cumulative-and-orphan-remove.png

    copy-by-referenceもこのパターンである。

Note

「non-cumulativeかつremove-orphans=true」のパターンと「copy-by-reference」のパターンの違いは、Bean変換後のCollectionのコンテナがコピー先のものか、コピー元のものかで異なる点である。

「non-cumulativeかつremove-orphans=true」のパターンの場合は、Bean変換後のCollectionのコンテナはコピー先のものであり、「copy-by-reference」のパターンはコピー元のものである。 以下に図で説明する。

  • non-cumulativeかつremove-orphans=true

    ../../_images/dozer-differrence1.png
  • copy-by-reference

    ../../_images/dozer-differrence2.png

コピー先がJPA (Hibernate)のエンティティで1対多や多対多の関連を持つ場合は要注意である。コピー先のエンティティがEntityManagerの管理下にある場合、予期せぬトラブルに遭うことがある。 例えばコレクションのコンテナが変更されると全件DELETE + 全件INSERTのSQLが発行され、「non-cumulativeかつremove-orphans=true」でコピーした場合は変更内容をUPDATE(要素数が異なる場合はDELETE or INSERT)のSQLが発行される場合がある。 どちらが良いかは要件次第である。

Warning

マッピング対象のBeanがStringのコレクションを持つ場合、期待通りの挙動にならないバグがある。

StringListSrc src = new StringListSrc;
StringListDest dest = new StringListDest();

List<String> stringsSrc = new ArrayList<String>();
List<String> stringsDest = new ArrayList<String>();

stringsSrc.add("a");
stringsSrc.add("b");
stringsSrc.add("c");

stringsDest.add("a");
stringsDest.add("d");
stringsDest.add("e");

src.setStrings(stringsSrc);
dest.setStrings(stringsDest);

beanMapper.map(src, dest);

System.out.println(dest.getStrings());

上記のコードをnon-cumulativeかつremove-orphans=trueの設定で実行すると、

[a, b, c]

と出力されることを期待するが、実際には

[b, c]

と出力され、重複したStringが除かれてしまう

copy-by-reference=”true”の設定で実行すると、期待通り

[a, b, c]

と出力される。

Tip

Dozerでは、Genericsを使用しないリスト間でもマッピングできる。このとき、変換元と変換先に含まれているオブジェクトのデータ型をHINTとして指定できる。 詳細は、 マニュアル を参照されたい。(Using Hints for Collection Mapping)

Todo

Collection<T>を使用したBean間のマッピングは失敗することが確認されている。

例 :

public class ListNestedBean<T> {
   private List<T> nest;
   // omitted other declarations
}

実行結果 :

java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl cannot be cast to java.lang.Class

5.21.1.3. How to extend

5.21.1.3.1. カスタムコンバーターの作成

Dozerがサポートしていないデータ型のマッピングの場合、カスタムコンバーター経由でマッピングできる。
  • 例 : java.lang.String <=> org.joda.time.DateTime
カスタムコンバーターは、Dozerが提供している org.dozer.CustomConverter を実装したクラスである。
カスタムコンバーターの指定は、以下3パターンで行える。
  • Global Configuration
  • クラスレベル
  • フィールドレベル

アプリケーション全体で、同様のロジックにより変換を行いたい場合は、Global Configurationを推奨する。

カスタムコンバーターを実装する場合はorg.dozer.DozerConverterを継承するのが便利である。

package com.example.yourproject.common.bean.converter;

import org.dozer.DozerConverter;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.util.StringUtils;

public class StringToJodaDateTimeConverter extends
                                          DozerConverter<String, DateTime> { // (1)
    public StringToJodaDateTimeConverter() {
        super(String.class, DateTime.class); // (2)
    }

    @Override
    public DateTime convertTo(String source, DateTime destination) {// (3)
        if (!StringUtils.hasLength(source)) {
            return null;
        }
        DateTimeFormatter formatter = DateTimeFormat
                .forPattern("yyyy-MM-dd HH:mm:ss");
        DateTime dt = formatter.parseDateTime(source);
        return dt;
    }

    @Override
    public String convertFrom(DateTime source, String destination) {// (4)
        if (source == null) {
            return null;
        }
        return source.toString("yyyy-MM-dd HH:mm:ss");
    }

}
項番 説明
(1)
org.dozer.DozerConverterを継承する。
(2)
コンストラクタで対象の2つのクラスを設定する。
(3)
StringからDateTimeの変換ロジックを記述する。本例ではデフォルトLocaleを使用する。
(4)
DateTimeからStringの変換ロジックを記述する。本例ではデフォルトLocaleを使用する。

作成したカスタムコンバーターを、マッピングに利用するために定義する必要がある。

dozer-configration-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">

    <configuration>
        <custom-converters><!-- (1) -->
            <!-- these are always bi-directional -->
            <converter
                type="com.example.yourproject.common.bean.converter.StringToJodaDateTimeConverter"><!-- (2) -->
                <class-a>java.lang.String</class-a><!-- (3) -->
                <class-b>org.joda.time.DateTime</class-b><!-- (4) -->
            </converter>
        </custom-converters>
    </configuration>
    <!-- omitted -->
</mappings>
項番 説明
(1)
すべてのカスタムコンバーターが属する、custom-convertersを定義する。
(2)
個別の変換の行うconverterを定義する。converterのタイプに、実装クラスの完全修飾クラス名(FQCN)を指定する。
(3)
変換元Beanの完全修飾クラス名(FQCN)
(4)
変換先Beanの完全修飾クラス名(FQCN)

上記のマッピングを行ったことで、アプリケーション全体で、java.lang.String <=> org.joda.time.DateTimeの変換が必要な場合、標準のマッピングではなく、カスタムコンバーター呼び出しでマッピングが行われる。

例 :

変換元のBean定義

public class Source {
    private int id;
    private String date;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private int id;
    private DateTime date;
    // ommited setter/getter
}

マッピング (双方向例)

Source source = new Source();
source.setId(1);
source.setDate("2012-08-10 23:12:12");

DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
DateTime dt = formatter.parseDateTime(source.getDate());

// Source to Destination Bean Mapping (String to org.joda.time.DateTime)
Destination destination = dozerBeanMapper.map(source, Destination.class);
assertThat(destination.getId(), is(1));
assertThat(destination.getDate(),is(dt));

// Destination to Source Bean Mapping (org.joda.time.DateTime to String)
dozerBeanMapper.map(destination, source);

assertThat(source.getId(), is(1));
assertThat(source.getDate(),is("2012-08-10 23:12:12"));

カスタムコンバーターに関する詳細は、 マニュアル を参照されたい。

Note

Stringからjava.utl.Dateなど標準の日付・時刻オブジェクトへの変換については”文字列から日付・時刻オブジェクトへのマッピング”で述べる。

5.21.1.4. Appendix

マッピング定義XMLファイルで指定できるオプションを説明する。

すべてのオプションは、 Dozerのマニュアル で確認できる。

5.21.1.4.1. フィールド除外設定 (field-exclude)

Beanを変換する際に、コピーしてほしくないフィールドを除外することができる。

以下のようなBeanの変換を考える。

変換元のBean定義

public class Source {
    private int id;
    private String name;
    private String title;
    // ommited setter/getter
}

コピー先のBean定義

public class Destination {
    private int id;
    private String name;
    private String title;
    // ommited setter/getter
}

コピー元のBeanから任意のフィールドをマッピングから除外したい場合は以下のように定義する。

フィールド除外の設定は、マッピング定義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">
    <!-- omitted -->
    <mapping>
        <class-a>com.xx.xx.Source</class-a>
        <class-b>com.xx.xx.Destination</class-b>
        <field-exclude><!-- (1) -->
            <a>title</a>
            <b>title</b>
        </field-exclude>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
除外したいフィールドを、<field-exclude>要素で設定する。この例の場合、指定した上でmapメソッドを実行すると、SourceオブジェクトからDestinationオブジェクトをコピーする際に、destinationのtitleの値が、上書きされない。
Source source = new Source();
source.setId(1);
source.setName("SourceName");
source.setTitle("SourceTitle");

Destination destination = new Destination();
destination.setId(2);
destination.setName("DestinationName");
destination.setTitle("DestinationTitle");
beanMapper.map(source, destination);
System.out.println(destination.getId());
System.out.println(destination.getName());
System.out.println(destination.getTitle());

上記のコードを実行すると以下のように出力される。

1
SourceName
DestinationTitle

マッピング後、destinationのtitleの値は、前の状態のままである。


5.21.1.4.2. マッピングの特定化 (map-id)

フィールド除外設定 (field-exclude)で示したマッピングは、アプリケーション全体でBean変換する際に適用される。 マッピングの適用範囲を制限(特定化)したい場合は、以下のように、map-idを指定して定義する。

<?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">
    <!-- omitted -->
    <mapping map-id="mapidTitleFieldExclude">
        <class-a>com.xx.xx.Source</class-a>
        <class-b>com.xx.xx.Destination</class-b>
        <field-exclude>
            <a>title</a>
            <b>title</b>
        </field-exclude>
    </mapping>
    <!-- omitted -->
</mappings>

上記の設定を行うと、mapメソッドにmap-id(mapidTitleFieldExclude)を渡すことでtitleのコピーを除外できる。 map-idを指定しない場合はこの設定は適用されず、全フィールドがコピーされる。

map メソッドにmap-idを渡す例を、以下に示す。

Source source = new Source();
source.setId(1);
source.setName("SourceName");
source.setTitle("SourceTitle");

Destination destination1 = new Destination();
destination1.setId(2);
destination1.setName("DestinationName");
destination1.setTitle("DestinationTitle");
beanMapper.map(source, destination1); // (1)
System.out.println(destination1.getId());
System.out.println(destination1.getName());
System.out.println(destination1.getTitle());

Destination destination2 = new Destination();
destination2.setId(2);
destination2.setName("DestinationName");
destination2.setTitle("DestinationTitle");
beanMapper.map(source, destination2, "mapidSourceBeanFieldExclude"); // (2)
System.out.println(destination2.getId());
System.out.println(destination2.getName());
System.out.println(destination2.getTitle());
項番 説明
(1)
通常のマッピング。
(2)
第三引数にmap-idを渡し、特定のマッピングルールを適用する。

上記のコードを実行すると以下のように出力される。

1
SourceName
SourceTitle

1
SourceName
DestinationTitle

Tip

map-idの指定は、mapping項目だけでなく、フィールドの定義でも行える。 詳細は、 マニュアル を参照されたい。


Note

Webアプリケーションにおいて、新規追加・更新両方の操作で同じフォームオブジェクトを使う場合がある。 このとき、フォームオブジェクトをドメインオブジェクトにコピー(マップ)する上で、操作によってはコピーしたくないフィールドもある。 この場合に、<field-exclude>を使用する。

  • 例:新規作成のフォームではuserIdを含むが、更新用のフォームではuserIdを含まない。

この場合に同じフォームオブジェクトを使用すると、更新時にuserIdにnullが設定される。コピー先のオブジェクトをDBから取得して、 フォームオブジェクトをそのままコピーすると、コピー先のuserIdまでnullとなる。これを回避するために、 更新用のmap-idを用意し、更新時はuserIdに対して、フィールド除外の設定を行う。


5.21.1.4.3. コピー元のnull・空フィールドを除外する設定 (map-null, map-empty)

コピー元のBeanのフィールドが、nullの場合、あるいは空の場合に、マッピングから除外することができる。 以下のように、マッピング定義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">
    <!-- omitted -->
    <mapping map-null="false" map-empty-string="false"><!-- (1) -->
        <class-a>com.xx.xx.Source</class-a>
        <class-b>com.xx.xx.Destination</class-b>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
コピー元のBeanのフィールドがnullの場合にマッピングから除外したい場合はmap-null属性にfalseを設定する。デフォルト値はtrueである。
空の場合に、マッピングから除外したい場合はmap-empty-string属性にfalseを設定する。デフォルト値はtrueである。

変換元のBean定義

public class Source {
    private int id;
    private String name;
    private String title;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private int id;
    private String name;
    private String title;
    // ommited setter/getter
}

マッピング例

Source source = new Source();
source.setId(1);
source.setName(null);
source.setTitle("");

Destination destination = new Destination();
destination.setId(2);
destination.setName("DestinationName");
destination.setTitle("DestinationTitle");
beanMapper.map(source, destination);
System.out.println(destination.getId());
System.out.println(destination.getName());
System.out.println(destination.getTitle());

上記のコードを実行すると以下のように出力される。

1
DestinationName
DestinationTitle

コピー元Beanのnametitleフィールドは、null、あるいは空で、マッピングから除外されている。

5.21.1.4.4. 文字列から日付・時刻オブジェクトへのマッピング

コピー元の文字列型のフィールドを、コピー先の日付・時刻系のフィールドにマッピングできる。

以下6種類の変換をサポートしている。

日付・時刻系

  • java.lang.String <=> java.util.Date
  • java.lang.String <=> java.util.Calendar
  • java.lang.String <=> java.util.GregorianCalendar
  • java.lang.String <=> java.sql.Timestamp

日付のみ

  • java.lang.String <=> java.sql.Date

時刻のみ

  • java.lang.String <=> java.sql.Time
日付・時刻系の変換は、以下のように行う。
例として、java.util.Dateへの変換を説明する。
java.util.Calendar,java.util.GregorianCalendar,java.sql.Timestampも同じ方法で行える。
<?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">
    <!-- omitted -->
    <mapping>
        <class-a>com.xx.xx.Source</class-a>
        <class-b>com.xx.xx.Destination</class-b>
        <field>
            <a date-format="yyyy-MM-dd HH:mm:ss:SS">date</a><!-- (1) -->
            <b>date</b>
        </field>
    </mapping>
    <!-- omitted -->
</mappings>
項番 説明
(1)
コピー元のフィールド名と日付形式を指定する。

変換元のBean定義

public class Source {
    private String date;
    // ommited setter/getter
}

変換先のBean定義

public class Destination {
    private Date date;
    // ommited setter/getter
}

マッピング

Source source = new Source();
source.setDate("2013-10-10 11:11:11.111");
Destination destination = beanMapper.map(source, Destination.class);
assert(destination.getDate().equals(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").parse("2013-10-10 11:11:11.111")));
日付形式は、個別のマッピング定義毎に設定するよりも、プロジェクトで一括で設定したいケースが多い。
その場合はDozerのGlobal configurationファイルで設定することを推奨する。
その場合、アプリケーション全体のマッピングで設定された日付形式が、適用される。
<?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">
    <!-- omitted -->
    <configuration>
        <date-format>yyyy-MM-dd HH:mm:ss.SSS</date-format>
        <!-- omitted other configuration -->
    </configuration>
    <!-- omitted -->
</mappings>
ファイル名には制限はないが、src/main/resources/META-INF/dozer/dozer-configration-mapping.xmlを推奨する。
dozer-configration-mapping.xml内の設定の範囲は、この設定ファイル内でアプリケーション全体に影響を与える、Global Configurationを行えばよい。

設定可能な項目の詳細について、 マニュアル を参照されたい。

5.21.1.4.5. マッピングのエラー

マッピング中にマッピング処理が失敗したら、org.dozer.MappingException(実行時例外)がスローされる。

MappingException がスローされる代表的な例を、以下に挙げる。

  • mapメソッドに存在しないmap-idが渡されている。
  • mapメソッドに存在するmap-idを渡したが、マップ処理に渡したソース・ターゲット型は、そのmap-idに指定している定義とは異なる。
  • Dozerがサポートしていない変換の場合、かつ、その変換用のカスタムコンバーターも存在しない場合。

これらは通常プログラムバグであるので、mapメソッドの呼び出しの部分を正しく修正する必要がある。