blog

[Java]mybatisリフレクションは、問題が発生すると自動的にコレクション属性を組み立てる

コンパイルが通らないコードの中には、実際に動作するものもあります。コンパイルは基本的にコーディングを制限しているだけのように感じられ、通過するコードの一部は間違っており、通過しないコードの一部は間違っ...

Aug 23, 2020 · 4 min. read
シェア

発生した問題

DTOの属性にはアノテーションがあり、レストインターフェイスがシリアライズのためのオブジェクトを返すときに機能しますが、このアノテーションがある場所で機能しないことがわかり、最終的にリフレクションが問題を引き起こしていることがわかりました。

返されるオブジェクト:AppMessageDTO。

private List<MessageReceiverDTO> receiver; public List<MessageReceiverDTO> getReceiver() { return receiver; } public void setReceiver(List<MessageReceiverDTO> receiver) { this.receiver = receiver; }

MessageReceiverDTO: @Encryptが機能しないのはここです。

 private ReceiverTypeEnum receiverType;
 @Encrypt
 private Long receiverKey;
 private String receiverName;
 
 ...get/setを省略する

Encryptが機能するポイントは、AppMessageDTOが残りのインターフェイスのリターンをシリアライズするときです。したがって、問題は、シリアライズが受信配列内のオブジェクトプロパティのアノテーションをインターセプトしないことです。

AppMessageDTOオブジェクトのソースを投稿し、以下は、mybatisが構築した、検索するためのSQLです。

AppMessageDTO appMessageDTO = messageMapper.selectFullMessageById(messageId, tenantId);

問題の原因

核心的な問題は、シリアライゼーション、つまりオブジェクトを構築するためにmybatisをクエリする際に、リフレクションが使われることです。

そのため、最終的なオブジェクトはAppMessageDTOオブジェクトとして出力されますが、その中のReceiverプロパティはListプロパティではなくListであるため、シリアライズはアノテーションを調査していません。

解析

この属性はデータベースにテキストとして保存され、カスタムtypeHandlerを使用してオブジェクトに変換されます。

<result column="receiver" property="receiver" jdbcType="VARCHAR" javaType="java.util.List" typeHandler="org.hippius.common.util.ReciverTypeHandler"/>

カスタム型変換は、Jacksonから直接シリアライズとデシリアライズを使用します。

return new ObjectMapper().readValue(rs.getString(columnName), java.util.List.class);

通常はこれで問題ありませんし、たいていの場合はこれで十分なのですが、ここではListの汎用型が指定されていないため、戻り値はListになります。

通常の状況下でappMessageDTO.setReceiver(リスト)を使用する場合は、コンパイルされていないので、私はそれがmybatisコンストラクトがエラーを報告する必要があると思ったが、実際にはあなたが時間を呼び出すためにリフレクションを使用する場合、それは正常に実行することができ、mybatisコンストラクトは、冒頭の問題につながったリフレクションの使用です。

オブジェクトに照会たデータをデバッグとして送信します。

問題解決

 CollectionType listType = MAPPER.getTypeFactory().constructCollectionType(ArrayList.class, MessageReceiverDTO.class);
 return MAPPER.readValue(content, listType);

カスタムtypeHandlerを書き換えて、手動で汎用型を定義します。

拡張機能

 Method setReceiver = null;
 //リフレクションを手動で呼び出すには
 AppMessageDTO appMessageDTO = new AppMessageDTO();
 try {
 setReceiver = AppMessageDTO.class.getMethod("setReceiver", List.class);
 List<Long> list = new ArrayList<>();
 list.add(1L);
 setReceiver.invoke(appMessageDTO,list);
 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
 e.printStackTrace();
 }
 
 //ジェネリックを使った定義でオブジェクトを受け取る。
 List<MessageReceiverDTO> receiver = appMessageDTO.getReceiver();
 
 //ここでgetClass()は、型変換エラー!というエラーを報告する。
 //System.out.println(receiver.get(0).getClass());
 
 //なぜなら、そうするとコンパイル時に問題が発生するからだ。
 System.out.println(receiver.get(0) instanceof MessageReceiverDTO);
 
 //コンパイルエラー
 //System.out.println(receiver.get(0) instanceof Long);
 //は普通に追加できる
 List<MessageReceiverDTO> receiver2 = new ArrayList<>();
 receiver2.add(receiver.get(0));
 
 //コンパイルエラー
 //LinkedHashMap hash = receiver.get(0);
 //receiver2.add(new LinkedHashMap());

概要

  1. 変換した後にgetClass()メソッドを呼び出すとエラーになるという、なんともファンタジックな仕様なので、getClass()メソッドの内部で型判定が行われたのでしょう。
  2. このように見た場合、定義は弱い型対応であり、実際のオブジェクトの型を表現することも、実際のオブジェクトの型を制限することもありません。
  3. コンパイルに通らないコードの中には実際に動くものもあります。コンパイルは根本的に、ただコーディングすることを制限しているように感じられ、通過するコードの一部は間違っており、通過しないコードの一部は代わりに間違っています。
  4. この場合、型を決定する必要があるのであれば、ジェネリックを使わずにリストを使ってオブジェクトを受け取り、instanceofを使ってチェックする必要があります。
Read next

リンク・テーブルからノードを削除する3つの方法(JavaScript)

この方法のスケーラビリティは非常に優れています。例えば、先頭を返すには、まず先頭を保存し、最後に返すことができます。 このアプローチには問題があります。チェーンテーブルにノードが1つしかない場合、そのノードは削除できません。

Aug 23, 2020 · 1 min read