Java
MapStruct

MapStruct を用いて高速に Bean Mapping を行う

More than 1 year has passed since last update.

http://mapstruct.org/

MapStruct は Annotation Processor という仕組みを用いて、クラスに定義したアノテーション情報などをヒントとしてコンパイル時にコードを自動生成することで Bean Mapping を手軽にかつ高速に行うことを目的としたライブラリです。

Awesome Java ( https://github.com/akullpp/awesome-java/blob/master/README.md#bean-mapping ) にも紹介されているライブラリですが、コンパイル時に静的で堅実なコードを生成してくれるというアプローチが個人のユースケースに合致したこと、Swagger などのツールで使われているなど理由で、こちらを開発現場では使っています。


Hello World 的に

蛇足な気もしますが、いちおう。

以下のような記述をすることで、人間がコードを書く量は極力減らすことができ、かつ自動生成したコードにより高速に(リフレクションなどに比べると高速に) Bean Mapping を実現することが出来ます。

なおクラスの中身は特に意味が無いので気にしないでください。

//BeanA (Sada)

import lombok.Data;

@Data
public class Sada {
String name;
int age;
double score;
}

//BeanB (Masashi)

import lombok.Data;

@Data
public class Masashi {
String title;
int age;
}

//BeanMapper

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface SadaMapper {
SadaMapper INSTANCE = Mappers.getMapper(SadaMapper.class);

@Mapping(source = "name", target = "title")
Masashi sadaToMasashi(Sada sada);
}

//Test

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class SadaMapperTest {

@Test
public void test() {
Sada sada = new Sada() {
{
setName("masashi");
setAge(65);
setScore(Double.MAX_VALUE);
}
};
Masashi masashi = SadaMapper.INSTANCE.sadaToMasashi(sada);

assertThat(masashi.getTitle(), is(sada.getName()));
assertThat(masashi.getAge(), is(sada.getAge()));
}
}


Generated Source

MapStruct では定められた書式にのっとって Interface を記述すると、実装クラスを自動生成してくれます。

実際に上記のコードを基にどのようなソースが自動生成されているか、デコンパイラで覗いてみると、以下のようなソースが生成されています。


public class SadaMapperImpl
implements SadaMapper
{
public Masashi sadaToMasashi(Sada sada)
{
if (sada == null) {
return null;
}
Masashi masashi = new Masashi();

masashi.setTitle(sada.getName());
masashi.setAge(sada.getAge());

return masashi;
}
}


MapStutct の使い方(応用編)

MapStruct の応用については、以下の Qiita が非常によくまとまっているのでおすすめです。

ここでは、上記 Qiita には載っていないような内容を書きます。


MapStruct 導入ではまった箇所

Annotation Processor を用いてコンパイル時にコードを自動生成するというアプローチのツールのため、通常のライブラリよりも使い方に工夫が必要になります。


Configuration

MapStruct は pom.xml の dependency にただ追加するだけでは IDE などでは動かず、環境に応じて設定を行う必要があります。

以下は Java8 の環境を前提として記述します。


pom.xml

以下の記述を追記します。


pom.xml


<properties>
<org.mapstruct.version>1.2.0.Final</org.mapstruct.version>
</properties>

<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>


mapstruct-processor は Annotation Processor の処理が含まれています。実装時に参照するクラス群は mapstruct-jdk8 に含まれています。

mapstruct はまだ完全に枯れているようには見えないライブラリなのでバージョンごとに振る舞いの差があります。今(2017/11/24 現在)は 1.2.0.Final を必ず指定しましょう。


Eclipse Plugin

以下の Plugin が推奨されているので、入れておきましょう。

https://marketplace.eclipse.org/content/mapstruct-eclipse-plugin


BeanMapper の Annotation 記述が Eclipse 上で即時反映されない

ちょっと根本的な解決方法が見つけられてないですが、ワークアラウンド的に以下の対応で対処できます。

なお、コマンドラインから maven コマンドを使う上では問題は起こりません。

Eclipse Project のクリーン

[Project] - [Clean]

Eclipse 上で maven package もしくは maven test
[Run As] - [maven test]

一応、公式サイトの FAQ ではプラグインを入れれば問題なく自動コンパイルされる旨が記載されているので、環境によっては上記の対処をしなくても動くかもしれません。

http://mapstruct.org/faq/#can-i-use-mapstruct-within-eclipse


Lombok との共存

MapStruct と同じようにアノテーション定義をもとにコードを自動生成するアプローチのライブラリ Lombok ( https://projectlombok.org/ ) とは相性が良くない、と言われていました。

しかし、 1.2.0.Final から MapStruct 側で Lombok のコード生成を必ず先に行うよう対処がされたので、最新版を使っていれば問題は起こらないです。

詳しくは、以下の FAQ を参照ください。

http://mapstruct.org/faq/#can-i-use-mapstruct-together-with-project-lombok


Project Lombok is an annotation processor that (amongst other things) adds getters and setters to the AST (abstract syntax tree) of compiled bean classes. AST modifications are not foreseen by Java annotation processing API, so quite some trickery was required within Lombok as well MapStruct to make both of them work together. Essentially, MapStruct will wait until Lombok has done all its amendments before generating mapper classes for Lombok-enhanced beans.


また、過去の経緯と、対応内容については、以下の github issue にも詳しいやりとりが記述されています。

https://github.com/mapstruct/mapstruct/issues/510


swagger-ui との共存

swagger-ui も MapStruct を使用しており、かつバージョンによっては参照している MapStruct のバージョンが古くなるため、dependency に swagger-ui が含まれている環境下だと正しくコンパイルが行えない場合があります。

(すくなくとも swagger-ui のバージョンが 2.7.0 の環境下では正しくコンパイルがされません)

上記のように、使用している他のライブラリで MapStruct が用いられ、かつバージョンが使いたいものと異なる場合は、個々のライブラリに exclude 定義を記述してください。


pom.xml

        <dependency>

<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
<exclusions>
<exclusion>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
<exclusions>
<exclusion>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</exclusion>
</exclusions>
</dependency>