Javaのクラス変換にModelMapperを使っているが、LombokのBuilderアノテーションを付与したクラスに対し、 ModelMapper#map
メソッドを実行するとエラーが発生したので、対応方法をメモ。
環境
ModelMapper v2.3.2 で確認。
エラー発生状況
@Data @AllArgsConstructor public class MyData { private long id; } @Value @Builder public class MyValue { long id; } MyData data = new MyData(1L); MyValue value = new ModelMapper().map(data, MyValue.class); // Failed to instantiate instance of destination MyValue. // Ensure that MyValue has a non-private no-argument constructor.
原因はエラーメッセージの通り、引数なしのコンストラクタが存在しないため。 Builderアノテーションにより、引数ありのパッケージプライベートコンストラクタが生成されている。
試しにBuilderクラスを渡してみたが、値が設定されない。
MyValue value = new ModelMapper() .map(data, MyValue.MyValueBuilder.class) .build(); // value.getId() == 0L
対応
そのまんまなissueが解決済みだった。
v2.2.0から追加された NameTransformers.builder()
で設定を調整する模様。
ModelMapper modelMapper = new ModelMapper(); Configuration builderConfiguration = modelMapper.getConfiguration().copy() .setDestinationNameTransformer(NameTransformers.builder()) .setDestinationNamingConvention(NamingConventions.builder()); TypeMap<MyData, MyValue.MyValueBuilder> typeMap = modelMapper .createTypeMap(MyData.class, MyValue.MyValueBuilder.class, builderConfiguration); MyData data = new MyData(1L); MyValue value = typeMap.map(data).build(); // value.getId() == 1L
調整
これで大丈夫かと思ったが、問題が発生。
ModelMapperはスレッドセーフなので、シングルトンインスタンスを使いまわしていたが、上記の方法でcreateTypeMapを同じ引数で複数回実行すると、エラーが発生した。
ModelMapper modelMapper = ModelMapperHolder.getInstance(); // シングルトンインスタンスの取得 Configuration builderConfiguration = ModelMapperHolder.getBuilderConfiguration(); // ビルダー用設定の取得 // createTypeMapを2回呼ぶとエラー modelMapper.createTypeMap(MyData.class, MyValue.MyValueBuilder.class, builderConfiguration); modelMapper.createTypeMap(MyData.class, MyValue.MyValueBuilder.class, builderConfiguration); // A TypeMap already exists for class MyData and class MyValue$MyValueBuilder
ModelMapper#typeMap
系のメソッドを呼ぶと、 TypeMapStore#getOrCreate
が呼ばれるようだが、Configurationを渡せるメソッドはcreateTypeMap以外に見当たらなかった。
作成済みのTypeMapは ModelMapper#getTypeMap(Class<S>, Class<D>)
で取得できるので、以下のように記述して対応できた。
ModelMapper modelMapper = ModelMapperHolder.getModelMapper(); Configuration builderConfiguration = ModelMapperHolder.getBuilderConfiguration(); TypeMap<MyData, MyValue.MyValueBuilder> typeMap = modelMapper .getTypeMap(MyData.class, MyValue.MyValueBuilder.class); if (typeMap == null) { synchronized (modelMapper) { typeMap = modelMapper.getTypeMap(MyData.class, MyValue.MyValueBuilder.class); if (typeMap == null) { typeMap = modelMapper.createTypeMap(MyData.class, MyValue.MyValueBuilder.class, builderConfiguration); } } }
実際はユーティリティーとしてラップしているので、毎回こんな記述はしないが、もう少しシンプルにならないかと調べてみたところ、Builder用のModelMapperを作って解決できた。
ModelMapper builderModelMapper = new ModelMapper();
builderModelMapper.getConfiguration()
.setDestinationNameTransformer(NameTransformers.builder())
.setDestinationNamingConvention(NamingConventions.builder());
のように設定しておいて、
ModelMapper builderModelMapper = ModelMapperHolder.getBuilderModelMapper(); MyData data = new MyData(1L); MyValue value = builderModelMapper .map(data, MyValue.MyValueBuilder.class) .build(); // value.getId() == 1L
とすることで、TypeMapの有無を意識せず使用できるようになった。