1. 概要

このチュートリアルでは、 MapStruct の使用法について説明します。これは、簡単に言えば、JavaBeanマッパーです。

このAPIには、2つのJavaBean間で自動的にマップする関数が含まれています。 MapStructを使用すると、インターフェイスを作成するだけで済み、ライブラリはコンパイル時に具体的な実装を自動的に作成します。

2. MapStructとTransferObjectPattern

ほとんどのアプリケーションでは、POJOを他のPOJOに変換する多くの定型コードに気付くでしょう。

たとえば、一般的なタイプの変換は、永続性に裏打ちされたエンティティと、クライアント側に送信されるDTOの間で発生します。

つまり、これがMapStructが解決する問題です。Beanマッパーを手動で作成するには時間がかかります。 ただし、ライブラリはBeanマッパークラスを自動的に生成できます。

3. Maven

以下の依存関係をMavenpom.xmlに追加しましょう。

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.4.2.Final</version> 
</dependency>

MapStruct の最新の安定版リリースとそのプロセッサーは、どちらもMaven中央リポジトリーから入手できます。

また、annotationProcessorPathsセクションをmaven-compiler-pluginプラグインの構成部分に追加しましょう。

mapstruct-processor は、ビルド中にマッパー実装を生成するために使用されます。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.4.2.Final</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

4. 基本的なマッピング

4.1. POJOの作成

まず、簡単なJavaPOJOを作成しましょう。

public class SimpleSource {
    private String name;
    private String description;
    // getters and setters
}
 
public class SimpleDestination {
    private String name;
    private String description;
    // getters and setters
}

4.2. マッパーインターフェイス

@Mapper
public interface SimpleSourceDestinationMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

MapStructが実装クラスを作成するため、 SimpleSourceDestinationMapper —の実装クラスを作成しなかったことに注意してください。

4.3. 新しいマッパー

mvn clean install を実行することで、MapStruct処理をトリガーできます。

これにより、 / target /generated-sources / annotations/の下に実装クラスが生成されます。

MapStructが自動作成するクラスは次のとおりです。

public class SimpleSourceDestinationMapperImpl
  implements SimpleSourceDestinationMapper {
    @Override
    public SimpleDestination sourceToDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }
        SimpleDestination simpleDestination = new SimpleDestination();
        simpleDestination.setName( source.getName() );
        simpleDestination.setDescription( source.getDescription() );
        return simpleDestination;
    }
    @Override
    public SimpleSource destinationToSource(SimpleDestination destination){
        if ( destination == null ) {
            return null;
        }
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName( destination.getName() );
        simpleSource.setDescription( destination.getDescription() );
        return simpleSource;
    }
}

4.4. テストケース

最後に、すべてが生成されたら、SimpleSourceの値がSimpleDestinationの値と一致することを示すテストケースを作成しましょう。

public class SimpleSourceDestinationMapperIntegrationTest {
    private SimpleSourceDestinationMapper mapper
      = Mappers.getMapper(SimpleSourceDestinationMapper.class);
    @Test
    public void givenSourceToDestination_whenMaps_thenCorrect() {
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName("SourceName");
        simpleSource.setDescription("SourceDescription");
        SimpleDestination destination = mapper.sourceToDestination(simpleSource);
 
        assertEquals(simpleSource.getName(), destination.getName());
        assertEquals(simpleSource.getDescription(), 
          destination.getDescription());
    }
    @Test
    public void givenDestinationToSource_whenMaps_thenCorrect() {
        SimpleDestination destination = new SimpleDestination();
        destination.setName("DestinationName");
        destination.setDescription("DestinationDescription");
        SimpleSource source = mapper.destinationToSource(destination);
        assertEquals(destination.getName(), source.getName());
        assertEquals(destination.getDescription(),
          source.getDescription());
    }
}

5. 依存性注入によるマッピング

次に、 Mappers.getMapper(YourClass.class)を呼び出すだけで、MapStructでマッパーのインスタンスを取得しましょう。

もちろん、これはインスタンスを取得するための非常に手動の方法です。 ただし、はるかに優れた代替手段は、マッパーを必要な場所に直接注入することです(プロジェクトで依存性注入ソリューションを使用している場合)。

幸いなことに、MapStructはSpringとCDI コンテキストと依存性注入)の両方をしっかりとサポートしています。

マッパーでSpringIoCを使用するには、componentModel属性を@Mapperに値springで追加する必要があり、CDIの場合はになります。 ]cdi

5.1. マッパーを変更する

次のコードをSimpleSourceDestinationMapperに追加します。

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

5.2. スプリングコンポーネントをマッパーに注入します

場合によっては、マッピングロジック内で他のSpringコンポーネントを利用する必要があります。 この場合、インターフェイスの代わりに抽象クラスを使用する必要があります。

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService

次に、よく知られている @Autowired アノテーションを使用して目的のコンポーネントを簡単に挿入し、コードで使用できます。

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {

    @Autowired
    protected SimpleService simpleService;

    @Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
    public abstract SimpleDestination sourceToDestination(SimpleSource source);
}

注入されたBeanをプライベートにしないことを忘れないでください!これは、MapStructが生成された実装クラスのオブジェクトにアクセスする必要があるためです。

6. 異なるフィールド名を持つフィールドのマッピング

前の例から、MapStructは、同じフィールド名を持っているため、Beanを自動的にマップできました。 では、マップしようとしているBeanのフィールド名が異なる場合はどうなるでしょうか。

この例では、EmployeeおよびEmployeeDTOという新しいBeanを作成します。

6.1. 新しいPOJO

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    // getters and setters
}
public class Employee {
    private int id;
    private String name;
    // getters and setters
}

6.2. マッパーインターフェイス

異なるフィールド名をマッピングする場合は、ソースフィールドをターゲットフィールドに構成する必要があります。そのためには、@Mappingsアノテーションを追加する必要があります。 このアノテーションは、 @Mapping アノテーションの配列を受け入れます。これを使用して、ターゲット属性とソース属性を追加します。

MapStructでは、ドット表記を使用してBeanのメンバーを定義することもできます。

@Mapper
public interface EmployeeMapper {
    @Mappings({
      @Mapping(target="employeeId", source="entity.id"),
      @Mapping(target="employeeName", source="entity.name")
    })
    EmployeeDTO employeeToEmployeeDTO(Employee entity);
    @Mappings({
      @Mapping(target="id", source="dto.employeeId"),
      @Mapping(target="name", source="dto.employeeName")
    })
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. テストケース

ここでも、ソースオブジェクトと宛先オブジェクトの両方の値が一致することをテストする必要があります。

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeId(1);
    dto.setEmployeeName("John");

    Employee entity = mapper.employeeDTOtoEmployee(dto);

    assertEquals(dto.getEmployeeId(), entity.getId());
    assertEquals(dto.getEmployeeName(), entity.getName());
}

その他のテストケースは、GitHubプロジェクトにあります。

7. Beanと子Beanのマッピング

次に、Beanを他のBeanへの参照とマッピングする方法を示します。

7.1. POJOを変更する

Employeeオブジェクトに新しいBean参照を追加しましょう。

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    // getters and setters omitted
}
public class Employee {
    private int id;
    private String name;
    private Division division;
    // getters and setters omitted
}
public class Division {
    private int id;
    private String name;
    // default constructor, getters and setters omitted
}

7.2. マッパーを変更する

ここで、DivisionDivisionDTOに変換するメソッドを追加する必要があります。 MapStructは、オブジェクトタイプを変換する必要があることを検出し、変換するメソッドが同じクラスに存在する場合、それを自動的に使用します。

これをマッパーに追加しましょう:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. テストケースを変更する

いくつかのテストケースを変更して、既存のテストケースに追加しましょう。

@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setDivision(new DivisionDTO(1, "Division1"));
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    assertEquals(dto.getDivision().getId(), 
      entity.getDivision().getId());
    assertEquals(dto.getDivision().getName(), 
      entity.getDivision().getName());
}

8. 型変換によるマッピング

MapStructは、いくつかの既成の暗黙的な型変換も提供します。この例では、文字列の日付を実際のDateオブジェクトに変換しようとします。

暗黙的な型変換の詳細については、MapStructリファレンスガイドを確認してください。

8.1. Beansを変更する

従業員の開始日を追加します。

public class Employee {
    // other fields
    private Date startDt;
    // getters and setters
}
public class EmployeeDTO {
    // other fields
    private String employeeStartDt;
    // getters and setters
}

8.2. マッパーを変更する

マッパーを変更し、開始日のdateFormatを提供します。

@Mappings({
  @Mapping(target="employeeId", source = "entity.id"),
  @Mapping(target="employeeName", source = "entity.name"),
  @Mapping(target="employeeStartDt", source = "entity.startDt",
           dateFormat = "dd-MM-yyyy HH:mm:ss")})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
  @Mapping(target="id", source="dto.employeeId"),
  @Mapping(target="name", source="dto.employeeName"),
  @Mapping(target="startDt", source="dto.employeeStartDt",
           dateFormat="dd-MM-yyyy HH:mm:ss")})
Employee employeeDTOtoEmployee(EmployeeDTO dto);

8.3. テストケースを変更する

変換が正しいことを確認するために、さらにいくつかのテストケースを追加しましょう。

private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
    Employee entity = new Employee();
    entity.setStartDt(new Date());
    EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
 
    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeStartDt("01-04-2016 01:00:00");
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
 
    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}

9. 抽象クラスを使用したマッピング

@Mapping機能を超える方法でマッパーをカスタマイズしたい場合があります。

たとえば、型変換に加えて、以下の例のように、何らかの方法で値を変換したい場合があります。

このような場合、抽象クラスを作成してカスタマイズしたいメソッドを実装し、MapStructによって生成される必要のあるメソッドを抽象のままにしておくことができます。

9.1. 基本モデル

この例では、次のクラスを使用します。

public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    //standard getters
}

および一致するDTO:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

ここで注意が必要なのは、 BigDecimal合計の金額をLongtotalInCentsに変換することです。

9.2. マッパーの定義

これは、Mapperを抽象クラスとして作成することで実現できます。

@Mapper
abstract class TransactionMapper {

    public TransactionDTO toTransactionDTO(Transaction transaction) {
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setUuid(transaction.getUuid());
        transactionDTO.setTotalInCents(transaction.getTotal()
          .multiply(new BigDecimal("100")).longValue());
        return transactionDTO;
    }

    public abstract List<TransactionDTO> toTransactionDTO(
      Collection<Transaction> transactions);
}

ここでは、単一オブジェクト変換用に完全にカスタマイズされたマッピングメソッドを実装しました。

一方、CollectionList abstractにマップするためのメソッドを残したので、MapStructが実装します。

9.3. 生成された結果

単一のTransactionTransactionDTOにマップするメソッドをすでに実装しているため、MapStructは2番目のメソッドでそれを使用することを期待しています。

以下が生成されます。

@Generated
class TransactionMapperImpl extends TransactionMapper {

    @Override
    public List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions) {
        if ( transactions == null ) {
            return null;
        }

        List<TransactionDTO> list = new ArrayList<>();
        for ( Transaction transaction : transactions ) {
            list.add( toTransactionDTO( transaction ) );
        }

        return list;
    }
}

12行目でわかるように、 MapStruct は、生成されたメソッドで実装を使用します。

10. マッピング前とマッピング後の注釈

@BeforeMappingおよび@AfterMappingアノテーションを使用して、@Mapping機能をカスタマイズする別の方法を次に示します。 アノテーションは、マッピングロジックの直前と直後に呼び出されるメソッドをマークするために使用されます。

これらは、この動作をすべてのマップされたスーパータイプに適用したいシナリオで非常に役立ちます。

Car ElectricCarおよびBioDieselCarのサブタイプをCarDTOにマップする例を見てみましょう。

マッピング中に、タイプの概念をDTOの FuelTypeenumフィールドにマッピングします。 次に、マッピングが完了したら、DTOの名前を大文字に変更します。

10.1. 基本モデル

次のクラスを使用します。

public class Car {
    private int id;
    private String name;
}

のサブタイプ:

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

列挙型フィールドタイプFuelTypeCarDTO

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}

10.2. マッパーの定義

次に、CarCarDTOにマップする抽象マッパークラスを作成しましょう。

@Mapper
public abstract class CarsMapper {
    @BeforeMapping
    protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
        if (car instanceof ElectricCar) {
            carDto.setFuelType(FuelType.ELECTRIC);
        }
        if (car instanceof BioDieselCar) { 
            carDto.setFuelType(FuelType.BIO_DIESEL);
        }
    }

    @AfterMapping
    protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
        carDto.setName(carDto.getName().toUpperCase());
    }

    public abstract CarDTO toCarDto(Car car);
}

@MappingTarget は、マッピングロジックが実行される直前にがターゲットマッピングDTOに入力するパラメータアノテーションです@BeforeMappingの場合およびの場合@AfterMappingアノテーション付きメソッドの。

10.3. 結果

上記で定義されたCarsMapperは、実装を生成します。

@Generated
public class CarsMapperImpl extends CarsMapper {

    @Override
    public CarDTO toCarDto(Car car) {
        if (car == null) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        enrichDTOWithFuelType(car, carDTO);

        carDTO.setId(car.getId());
        carDTO.setName(car.getName());

        convertNameToUpperCase(carDTO);

        return carDTO;
    }
}

注釈付きメソッドの呼び出しが、実装のマッピングロジックをどのように囲んでいるかに注目してください。

11. ロンボクのサポート

MapStructの最近のバージョンでは、Lombokのサポートが発表されました。 したがって、Lombokを使用してソースエンティティと宛先を簡単にマッピングできます。

Lombokサポートを有効にするには、アノテーションプロセッサパスに依存関係を追加する必要があります。 Lombokバージョン1.18.16以降、lombok-mapstruct-bindingへの依存関係も追加する必要があります。 これで、Mavenコンパイラプラグインにmapstruct-processorとLombokが追加されました。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.4.2.Final</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
	        <version>1.18.4</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-mapstruct-binding</artifactId>
	        <version>0.2.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Lombokアノテーションを使用してソースエンティティを定義しましょう。

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

そして、宛先データ転送オブジェクト:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

このためのマッパーインターフェイスは、前の例と同じままです。

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

12. defaultExpressionのサポート

バージョン1.3.0以降、 @MappingアノテーションのdefaultExpression属性を使用して、ソースフィールドがnullの場合に宛先フィールドの値を決定する式を指定できます。 これは既存のものに追加されます defaultValue 属性機能。

ソースエンティティ:

public class Person {
    private int id;
    private String name;
}

宛先データ転送オブジェクト:

public class PersonDTO {
    private int id;
    private String name;
}

ソースエンティティのidフィールドがnullの場合、ランダムな id を生成し、他のプロパティ値をそのままにして宛先に割り当てます。

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
    
    @Mapping(target = "id", source = "person.id", 
      defaultExpression = "java(java.util.UUID.randomUUID().toString())")
    PersonDTO personToPersonDTO(Person person);
}

式の実行を検証するためのテストケースを追加しましょう。

@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect() 
    Person entity  = new Person();
    entity.setName("Micheal");
    PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
    assertNull(entity.getId());
    assertNotNull(personDto.getId());
    assertEquals(personDto.getName(), entity.getName());
}

13. 結論

この記事では、MapStructの概要を説明しました。 マッピングライブラリの基本のほとんどと、アプリケーションでの使用方法を紹介しました。

これらの例とテストの実装は、GitHubプロジェクトにあります。 これはMavenプロジェクトなので、そのままインポートして実行するのは簡単です。