きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Javaでリフレクションを素振りする

基本的にはIDEのサポートが得られなかったり、静的解析してくれないので、リフレクションは使ってはいけません。しかし、非推奨であることと、使えないことは違います。

当記事では、Javaでリフレクションを使ってprivateメソッドとフィールドを呼び出せることを目標とします。

環境

  • Java
    • 15
  • Lombok

ゴール

  • 別のクラスからリフレクションを使ってprivateメソッドを呼び出す
  • 別のクラスからリフレクションを使ってprivateフィールドを取得する
  • 別のクラスからリフレクションを使ってprivateフィールドを設定する

ユースケース

  • テストクラスでprivateメソッドを実行したい
    • そもそも、テストしたいメソッドはパッケージプライベートか、protectedにしましょう。
  • 巨大なネストするクラスから目的の変数を取得したい

クラス構成

この記事ではユースケースを意識するために、親子関係をもつクラスを使用します。

- Parent
  - Child
    - GrandChild
@Value
@Builder
public class Parent {
  private Child child;
}
@Value
@Builder
public class Child {
  private GrandChild grandChild;
  private List<GrandChild> grandChildren;
}
@Value
@Builder
public class GrandChild {
  private String description;
  
  // 引数無し
  private String getDescription() {
    return description;
  }

  // 引数あり
  private int computeMultiple(int num1, int num2) {
    return num1 * num2;
  }

  // 可変長引数の引数あり
  private int computeMultipleArray(int... nums) {
    int tmp = 1;
    for (int num : nums) {
      tmp *= num;
    }
    return tmp;
  }

引数無しのprivateメソッドを実行する

まずはリフレクション対象のインスタンスを生成します。

次にインスタンスから、リフレクションしたいprivateメソッドを取得します。一度getClassでクラスを取得してから、getDeclaredMethodでメソッドを取得できます。

次に、setAccessibleで可視性を変更します。実行しない場合は、java.lang.IllegalAccessExceptionが発生し、可視性を変更する必要があるとメッセージが出力されます。

最後に、取得したprivateメソッドのinvokeにインスタンスをパラメータ渡すと実行することができます。戻り値がある場合は、Object型で返却されるので必要に応じてキャストしてください。

@Test
void test_01() throws Exception {
  // リフレクション対象のインスタンス生成
  GrandChild target = GrandChild.builder()
      .description("説明")
      .build();

  // メソッドを取得する
  Method method = target.getClass().getDeclaredMethod("getDescription");
  // privateメソッドにアクセスできるようにする
  method.setAccessible(true);
  // インスタンスからメソッドを実行して値を取得する
  String result = (String) method.invoke(target);
  assertThat(result).isEqualTo("説明");
}

引数ありのprivateメソッドを実行する

引数無しのprivateメソッドの実行との比較差分は2つです。

  • メソッドを指定するときにパラメータの数と型を指定すること
  • invokeメソッドに実際のパラメータを渡すこと

Javaには同名クラスでパラメータが異なるオーバーロードがありますので、面倒くさがらずに設定する必要があります。

@Test
void test_03() throws Exception {
  // リフレクション対象のインスタンス生成
  GrandChild target = GrandChild.builder()
      .build();

  // メソッドを取得する
  Method method = target.getClass()
    .getDeclaredMethod("computeMultiple", int.class, int.class);
  // privateメソッドにアクセスできるようにする
  method.setAccessible(true);
  // インスタンスからメソッドを実行して値を取得する
  int result = (int) method.invoke(target, 100, 20);
  assertThat(result).isEqualTo(2000);
}

なお、可変長引数の場合は、内部的には配列になっていますので、配列を渡しましょう。リフレクションを使わない普通のメソッドであれば可変長でパラメータを渡せますが、リフレクション時には明示的に配列にしなければいけない点がポイントです。

@Test
void test_04() throws Exception {
  GrandChild target = GrandChild.builder()
      .build();

  Method method = target.getClass()
    .getDeclaredMethod("computeMultipleArray", int[].class);
  method.setAccessible(true);
  int result = (int) method.invoke(target, new int[]{100, 20, 3, 4});
  // この渡し方はできない
// int result = (int) method.invoke(target, 100, 20, 3, 4);
  assertThat(result).isEqualTo(24000);
}

privateなフィールドを取得したい

基本的にはメソッドと同じような処理です。getDeclaredMethodではなく、getDeclaredFieldを使用します。getDeclaredFieldを元に```get````すると値を取得できます。

    @Test
    void test_02_02() throws Exception {
      GrandChild target = GrandChild.builder()
          .tax(123)
          .build();

      Field fieldTax = target.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      Object tax = fieldTax.get(target);

      assertThat(tax).isEqualTo(123);
    }

配列項目を参照する場合は、取得した値を元にgetする必要がありそうです。

    @Test
    void test_02_03() throws Exception {
      Child target = Child.builder()
          .grandChildren(List.of(
              GrandChild.builder()
                  .tax(123)
                  .build()
          )).build();

      Field fieldGrandChildren = target.getClass().getDeclaredField("grandChildren");
      fieldGrandChildren.setAccessible(true);
     // 一旦Listにキャスト
      List grandChildren = (List)fieldGrandChildren.get(target);
     // 1個目の配列を取得する
      Object grandChild = grandChildren.get(0);
      Field fieldTax = grandChild.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      Object tax = fieldTax.get(grandChild);

      assertThat(tax).isEqualTo(123);
    }

privateなフィールドを設定したい

似たような流れなので、特に解説はしません。

    @Test
    void test_02_04() throws Exception {
      GrandChild target = GrandChild.builder()
          .build();

      Field fieldTax = target.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      fieldTax.set(target, 123);

      assertThat(target.getTax()).isEqualTo(123);
    }

巨大なネストするクラスから目的の変数を取得したい

この項目の新規性はありません。ユースケースを意識したものとなります。

例えば、「メールに必要な文言を巨大なネストするクラスから取得したい。しかも、簡単に書き換えられるようにマスタで管理したい。」という要件があったとします。

この要件を満たすために、マスタではchild.grandChild.taxという文言だけを管理しておいて、その値を元にリフレクションをするようにします。

今回の例では、ParentクラスからChildクラスを取得し、ChildクラスからGrandChildクラスを取得し、GrandChildクラスのtax変数を取得します。

@Test
void test_02() throws Exception {
  Parent target = Parent.builder()
      .child(Child.builder()
          .grandChild(GrandChild.builder()
              .tax(123)
              .build())
          .build())
      .build();

      Field childFiled = target.getClass().getDeclaredField("child");
      childFiled.setAccessible(true);
      Object child = childFiled.get(target);
      Field childField = child.getClass().getDeclaredField("grandChild");
      childField.setAccessible(true);
      Object grandChild = childField.get(child);
      Field fieldTax = grandChild.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      Object tax = fieldTax.get(grandChild);

  assertThat(tax).isEqualTo(123);
}

ソースコード

実装コード https://github.com/hirotoKirimaru/cucumber-sample/blob/fba4d7c4c0b8140982c7032e15cdba328613c3ed/src/test/java/kirimaru/biz/domain/ReflectionTests.java#L81a

終わりに

リフレクションは基本使いたくありませんが、たまに使いたくなる時があります。

実際は、Springの機能でもうちょっと簡単にリフレクションで値を取得できたりするのですが、それはまた別の記事にてご紹介させてください。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考

何かの時にスッと使える力技 - Reflection 編 qiita.com

リフレクション www.ne.jp

f:id:nainaistar:20210505112601p:plain