Mapstruct Spring Extensions を試してみる
   3 min read

はじめに

MapStruct の公式サイトを見ていると、 Mapstruct Spring Extensions なるサブプロジェクトが発足していたので、何者か調べようと試してみました。

結果、これは Spring の ConversionServiceConverter と MapStruct の Mapper を統合して、実装を少し楽にしよう、というもののようでした。

今回は、 MapStruct の基本的な使い方から始めて、順に Spring Boot に統合していってみます。

ちなみに公式サンプルは こちら になります。 (いろいろ機能を紹介するサンプルになっていて本質が分かりづらいので、今回シンプルな実装で試してみています。)

今回のコードはこちらです。

実装

前提

  • Java17

  • Spring Boot 2.6.0-M3

  • MapStruct 1.4.2.Final

  • MapStruct Spring Extensions 0.1.0

README にも書いていますが、どのタイミングでもリクエストは次のコマンドで行います。

curl --location --request POST 'http://localhost:8080/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "my car",
    "wheel": {
        "size": 100
    },
    "pedal": {
        "size": 20
    }
}'

1. 普通の使い方で MapStruct を組み込む

@Data
public class Car {
    private String name;
    private Wheel wheel;
    private Pedal pedal;
}

@Data
public class Wheel {
    private int size;
}

@Data
public class Pedal {
    private int size;
}

のようなコードを、

@Data
public class CarDto {
    private String name;
    private WheelDto steeringWheel;
    private PedalDto footPedal;
}

@Data
public class WheelDto {
    private int wheelSize;
}

@Data
public class PedalDto {
    private int pedalSize;
}

にマッピングすることを考えます。 このとき、マッパーは次のような実装になります。

@Mapper(uses = { WheelMapper.class, PedalMapper.class })
public interface CarMapper {
    @Mappings({
        @Mapping(source = "wheel", target = "steeringWheel"),
        @Mapping(source = "pedal", target = "footPedal"),
    })
    CarDto convert(Car car);
}

@Mapper
public interface WheelMapper {
    @Mapping(source = "size", target = "wheelSize")
    WheelDto convert(Wheel wheel);
}

@Mapper
public interface PedalMapper {
    @Mapping(source = "size", target = "pedalSize")
    PedalDto convert(Pedal pedal);
}

マッピング処理を行いたい箇所でマッパーをインジェクションして利用します。

@RestController
@RequiredArgsConstructor
@Slf4j
public class MyController {

    private final CarMapper carMapper;

    @PostMapping("/")
    public CarDto index(@RequestBody final Car car) {
        log.info("car: {}", car);

        final CarDto dto = carMapper.convert(car);
        log.info("dto: {}", dto);

        return dto;
    }

}

2. Converter として実装する

マッパーが org.springframework.core.convert.converter.Converter を実装したコンポーネントであれば ConversionService の仕組みで変換できるよね、というのが次の発想になります。 extends Converter<_,_> を加えるだけです(正確には、 MapStruct 変換メソッド名は何でもよかったのですが、 Converter を実装するなら convert という名前でないといけないので一般的にはメソッド名変更も伴います)。

@Mapper(uses = { WheelMapper.class, PedalMapper.class })
public interface CarMapper extends Converter<Car, CarDto> {
    @Override
    @Mappings({
        @Mapping(source = "wheel", target = "steeringWheel"),
        @Mapping(source = "pedal", target = "footPedal"),
    })
    CarDto convert(Car car);
}

// (他の2つのマッパーも同様に extends Converter する)

そうすると、利用個所ではマッパーの代わりに ConversionService をインジェクションして変換できるようになります。

@RestController
@RequiredArgsConstructor
@Slf4j
public class MyController {

    private final ConversionService conversionService;

    @PostMapping("/")
    public CarDto index(@RequestBody final Car car) {
        log.info("car: {}", car);

        final CarDto dto = conversionService.convert(car, CarDto.class);
        log.info("dto: {}", dto);

        return dto;
    }

}

現在、 CarMapperuses = { WheelMapper.class, PedalMapper.class } というように、内包するエンティティのマッパーも明示的に指定していますが、 ConversionService にどの Converter を使って変換するかは任せてしまえるんじゃないか、というのがこのライブラリのモチベーションのようです(参考)。

3. Mapstruct Spring Extensions を利用する

  • コード: 105b509

    1. アノテーションとアノテーションプロセッサを追加(参考: 2. Set up)します(link)。

    2. Application クラスに @SpringMapperConfig アノテーションを付与します(link)。

    3. マッパーの uses 値を ConversionServiceAdapter.class に置き換えます(link)。

この手順の最後で行っている uses 値が固定値で良くなる、というのが本ライブラリを使うメリット、ということのようです。