仕事でJavaのModelMapperを使用しているが、何故かフィールドの値が正しく変換されないことがあった。未だにちゃんとよく分かってはいないが、とりあえず調べたことをまとめる。
問題のコードと挙動
@Data @AllArgsConstructor public class Src { private int id; private String value1; private String value2; public boolean isValue1() { return value1 != null; } public boolean isValue2() { return value2 != null; } }
@Data public class Dest { private int id; private String value1; private String value2; }
public class Main { public static void main(String[] args) { ModelMapper mapper = new ModelMapper(); System.out.println(mapper.map(new Src(1, "value1", "value2"), Dest.class)); } }
大体上記のようなコードで実行している。
- 簡略化のためにLombokを使用している
- Srcには別途バリデーション目的で値が存在するかどうかのメソッドを実装している
Destの期待値は常にvalue1, value2になることだったが、なぜかローカル環境だとたまに値がtrueになることがあった。また、デプロイしていた開発サーバーではいずれの値も常に必ずtrueになっていた。
調べたこと
その1
ModelMapperはデフォルトだと同名のフィールド同士をマッピングするというのはマニュアルを見て理解できるが、その時にデフォルトだとどのメソッドを使ってマッピングするかどうかについては調べてもよく分からなかった。
こちらとしてはLombokで自動生成されたgetメソッドを使ってマッピングされることを期待していたが、恐らくバリデーション目的で作成していたis~メソッドもアクセサーとして認識され、たまにModelMapperがそちらを使用することで値がtrueになることがあったんだろうと想像している。
その2
とりあえずmainの部分を以下のように変更してみた。
public class Main { public static void main(String[] args) { for (int i = 0; i < 10; i++) { ModelMapper mapper = new ModelMapper(); System.out.println(mapper.map(new Src(1, "value1", "value2"), Dest.class)); } } }
ModelMapperを都度生成した場合にどうなるかを確認したかったので上記で実行してみた。以下は実行結果の一例。
Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true) Dest(id=1, value1=value1, value2=true)
何度もJavaを実行すると両方ともtrueになったり逆に両方とも正しく変換出来たりする場合があったが、いずれのケースでも最初にtrueに変換された場合はそのあとJavaを再実行しない限り何度変換しても値は必ずtrueになるようだった。
ひとまずこれらの結果を見る限り、フィールドへのアクセサーが複数ある場合どちらが使われることになるかはJava実行時に決まるようである。ローカル環境で毎回Javaを実行/停止させながら確認すると正しく変換出来たりできなかったりするが、起動しっぱなしの開発サーバーだと必ずうまく変換できないのはおそらくこれが原因だろうと思われる。
その3
試しにSrcを以下のように修正して実行してみた(is~をhas~に変更した)。
@Data @AllArgsConstructor public class Src { private int id; private String value1; private String value2; public boolean hasValue1() { return value1 != null; } public boolean hasValue2() { return value2 != null; } }
これだと何度実行しても値がtrueに変換されることはなかった。
どうもModelMapperがアクセサーとしてどのメソッドを使用するかは、メソッド名によってあらかじめ決まっているように見える。
結論
具体的にModelMapperがどういう命名規則のメソッドを使ってマッピングするかどうかはちょっとよく分からなかった(恐らくget~やis~だとは思うが)。
ただし対象となるSourceにアクセサーとして使用できそうなメソッドが複数あるとModelMapperが意図していたメソッドとは異なるメソッドを使ってしまう場合があるようなので、Sourceには初めから不要なメソッドを追加しないか、(意図しない変換が起きた場合は)addMappingsメソッド等で明示的にどのメソッドを使ってマッピングするかどうかを指定してやったほうがよさそう。