基本的にはIDEのサポートが得られなかったり、静的解析してくれないので、リフレクションは使ってはいけません。しかし、非推奨であることと、使えないことは違います。
当記事では、Javaでリフレクションを使ってprivateメソッドとフィールドを呼び出せることを目標とします。
環境
ゴール
- 別のクラスからリフレクションを使って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");
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);
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});
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 grandChildren = (List)fieldGrandChildren.get(target);
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