はじめに
MapStruct の公式サイトを見ていると、 Mapstruct Spring Extensions なるサブプロジェクトが発足していたので、何者か調べようと試してみました。
結果、これは Spring の ConversionService
の Converter
と 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 を組み込む
-
コード:
c4deee3
@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
として実装する
-
コード:
eb23410
マッパーが 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;
}
}
現在、 CarMapper
で uses = { WheelMapper.class, PedalMapper.class }
というように、内包するエンティティのマッパーも明示的に指定していますが、 ConversionService
にどの Converter
を使って変換するかは任せてしまえるんじゃないか、というのがこのライブラリのモチベーションのようです(参考)。