※当メディアのリンクにはアフィリエイト広告が含まれています

Javaの単体テストでprivateなメソッドを呼び出したい!そんなときはリフレクションを使用しよう。

jUnit等のテストツールを使う際、privateなフィールドやメソッドにアクセスしたい時ってありますよね。
でもprivateで宣言されているフィールドやメソッドは宣言されたクラスでしか呼び出すことができません。
でもリフレクションというものを使用することでprivateなフィールドやメソッドにアクセスすることができちゃうんです。

リフレクションとは

リフレクションというのはプログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことでです。
もう少しわかりやすくすると、文字列を使ってクラスの生成やメソッドのアクセス等ができる技術です。

リフレクションの基本的な使い方

まず、以下のようなクラスがあったとします。

public class ReflectionSample {

  private static int staticIntField = 0;
  private String stringField = null;
  private int intField = 0;

  private ReflectionSample() {
    System.out.println("private コンストラクタ実行");
  }

  private ReflectionSample(String val1, int val2) {
    System.out.println("private コンストラクタ実行 引数 val1=" + val1 + " val2=" + val2);
    this.stringField = val1;
    this.intField = val2;
  }

  private String output() {
    System.out.println("private メソッド実行");
    return "outputメソッド実行";
  }

  private int add(int val1, int val2) {
    System.out.println("private メソッド実行 引数 val1=" + val1 + " val2=" + val2);
    return val1 + val2;
  }

  private void voidMethod() {
    System.out.println("private メソッド実行(戻り値なし)");
  }

  private void setMyField(String val1, int val2) {
    System.out.println("private メソッド実行(戻り値なし) 引数 val1=" + val1 + " val2=" + val2);
    this.stringField = val1;
    this.intField = val2;
  }

  private static String staticOutput() {
    System.out.println("private 静的メソッド実行");
    return "staticOutputメソッド実行";
  }

  private static int staticAdd(int val1, int val2) {
    System.out.println("private 静的メソッド実行 引数 val1=" + val1 + " val2=" + val2);
    return val1 + val2;
  }

  private static void staticVoidMethod() {
    System.out.println("private 静的メソッド実行(戻り値なし)");
  }

  private static void staticSetMyField(int val) {
    System.out.println("private 静的メソッド実行(戻り値なし) 引数 val=" + val);
    staticIntField = val;
  }
}

全てprivate宣言されていて、何の役にも立ちません。
完全に引きこもり状態です。
それでは、このクラスの引数なしコンストラクタを呼び出してみましょう。
呼び出し方は以下のようになります。

Constructor<ReflectionSample> constructor = ReflectionSample.class.getDeclaredConstructor();
constructor.setAccessible(true);
ReflectionSample rs = constructor.newInstance();

これを実行するとコンソールに以下のように出力されたと思います。

private コンストラクタ実行

フィールドやメソッドも同じように呼び出すことができます。
呼び出し方は以下のようになります。

Constructor<ReflectionSample> constructor = ReflectionSample.class.getDeclaredConstructor();
constructor.setAccessible(true);
ReflectionSample rs = constructor.newInstance();

// privateメソッド呼び出し
Method m1 = ReflectionSample.class.getDeclaredMethod("output");
m1.setAccessible(true);
m1.invoke(rs);

// privateメソッド(引数あり)呼び出し
Class<?>[] methodParams = {int.class, int.class};
Method m2 = ReflectionSample.class.getDeclaredMethod("add", methodParams);
m2.setAccessible(true);
m2.invoke(rs, 1, 2);

// privateな変数を読み取り
Field f = ReflectionSample.class.getDeclaredField("intField");
f.setAccessible(true);
int i = (Integer) f1.get(rs);
System.out.println("i = " + );

これを実行するとコンソールに以下のように出力されたと思います。

private コンストラクタ実行
private メソッド実行
private メソッド実行 引数 val1=1 val2=2
i = 0

やり方としてはアクセスしたいところに対してsetAccessibleメソッドを使用してアクセス可能にするだけですね。

リフレクション支援クラスの作成

リフレクションのやり方を見ていただいたのでお気づきかもしれませんが
アクセスしたい対象が多ければ多いほど同じような記述が増えてしまうのが難点です。

そこで、記述量を減らすためリフレクション支援クラスを作成しました。

/**
 * リフレクション支援クラス
 * @author k.oba
 *
 * @param <T1>
 */
public class ClassReflector<T1> {
  private Class<T1> targetClass = null;
  private T1 targetClassInstance = null;

  /**
   * コンストラクタ.
   * @param targetClass リフレクション対象クラス
   * @throws ReflectiveOperationException
   */
  public ClassReflector(Class<T1> targetClass) throws ReflectiveOperationException {
    this.targetClass = targetClass;
    this.targetClassInstance = this.getInstance();
  }

  /**
   * コンストラクタ.<br>
   * リフレクション対象のクラスのコンストラクタに引数が必要な場合はこちらを使用してください。
   * @param targetClass リフレクション対象クラス
   * @param paramType 引数の型
   * @param args リフレクション対象クラスのコンストラクタ引数
   * @throws ReflectiveOperationException
   */
  public ClassReflector(Class<T1> targetClass, Class<?>[] paramType, Object... args) throws ReflectiveOperationException {
    this.targetClass = targetClass;
    this.targetClassInstance = this.getInstance(paramType, args);
  }

  /**
   * リフレクション対象クラスのインスタンス取得.
   * @return リフレクション対象クラスのインスタンス
   */
  public T1 getTargetClassInstance() {
    return this.targetClassInstance;
  }

  /**
   * 戻り値のあるメソッドを実行.
   * @param returnClass メソッド戻り値のクラス
   * @param methodName メソッド名
   * @return メソッド実行結果
   * @throws ReflectiveOperationException
   */
  @SuppressWarnings("unchecked")
  public <T2> T2 runMethod(Class<T2> returnClass, String methodName) throws ReflectiveOperationException {
    Method m = this.targetClass.getDeclaredMethod(methodName);
    m.setAccessible(true);
    return (T2)m.invoke(this.targetClassInstance);
  }

  /**
   * 戻り値のあるメソッドを実行.
   * @param paramType
   * @param returnClass メソッド戻り値のクラス
   * @param methodName メソッド名
   * @param args メソッド引数
   * @return メソッド実行結果
   * @throws ReflectiveOperationException
   */
  @SuppressWarnings("unchecked")
  public <T2> T2 runMethod(Class<?>[] paramType, Class<T2> returnClass, String methodName, Object... args) throws ReflectiveOperationException {
    Method m = this.targetClass.getDeclaredMethod(methodName, paramType);
    m.setAccessible(true);
    return (T2)m.invoke(this.targetClassInstance, args);
  }

  /**
   * 戻り値のないメソッドを実行.
   * @param methodName メソッド名
   * @throws ReflectiveOperationException
   */
  public void runMethod(String methodName) throws ReflectiveOperationException {
    Method m = this.targetClass.getDeclaredMethod(methodName);
    m.setAccessible(true);
    m.invoke(this.targetClassInstance);
  }

  /**
   * 戻り値のないメソッドを実行.
   * @param paramType
   * @param methodName メソッド名
   * @param args メソッド引数
   * @throws ReflectiveOperationException
   */
  public void runMethod(Class<?>[] paramType, String methodName, Object... args) throws ReflectiveOperationException {
    Method m = this.targetClass.getDeclaredMethod(methodName, paramType);
    m.setAccessible(true);
    m.invoke(this.targetClassInstance, args);
  }

  /**
   * 戻り値のある静的メソッドを実行.
   * @param returnClass メソッド戻り値のクラス
   * @param methodName メソッド名
   * @return メソッド実行結果
   * @throws ReflectiveOperationException
   */
  @SuppressWarnings("unchecked")
  public <T2> T2 runStaticMethod(Class<T2> returnClass, String methodName) throws ReflectiveOperationException {
    Method m = targetClass.getDeclaredMethod(methodName);
    m.setAccessible(true);
    return (T2)m.invoke(null);
  }

  /**
   * 戻り値のある静的メソッドを実行.
   * @param returnClass メソッド戻り値のクラス
   * @param methodName メソッド名
   * @param args メソッド引数
   * @return メソッド実行結果
   * @throws ReflectiveOperationException
   */
  @SuppressWarnings("unchecked")
  public <T2> T2 runStaticMethod(Class<T2> returnClass, String methodName, Object... args) throws ReflectiveOperationException {
    Method m = targetClass.getDeclaredMethod(methodName);
    m.setAccessible(true);
    return (T2)m.invoke(null, args);
  }

  /**
   * 戻り値のない静的メソッドを実行.
   * @param methodName メソッド名
   * @throws ReflectiveOperationException
   */
  public void runStaticMethod(String methodName) throws ReflectiveOperationException {
    Method m = targetClass.getDeclaredMethod(methodName);
    m.setAccessible(true);
    m.invoke(null);
  }

  /**
   * 戻り値のない静的メソッドを実行.
   * @param methodName メソッド名
   * @param args メソッド引数
   * @throws ReflectiveOperationException
   */
  public void runStaticMethod(String methodName, Object... args) throws ReflectiveOperationException {
    Method m = targetClass.getDeclaredMethod(methodName);
    m.setAccessible(true);
    m.invoke(null, args);
  }

  /**
   * インスタンス変数に値を設定.
   * @param fieldName フィールド名
   * @param value 設定値
   * @throws ReflectiveOperationException
   */
  public void setFieldValue(String fieldName, Object value) throws ReflectiveOperationException {
    Field f = this.targetClass.getDeclaredField(fieldName);
    f.setAccessible(true);
    f.set(this.targetClassInstance, value);
  }

  /**
   * インスタンス変数の値を取得.
   * @param returnClass 戻り値のクラス
   * @param fieldName フィールド名
   * @return 値
   * @throws ReflectiveOperationException
   */
  @SuppressWarnings("unchecked")
  public <T2> T2 getFieldValue(Class<T2> returnClass, String fieldName) throws ReflectiveOperationException {
    Field f = this.targetClass.getDeclaredField(fieldName);
    f.setAccessible(true);
    return (T2)f.get(this.targetClassInstance);
  }

  /**
   * クラス変数に値を設定.
   * @param fieldName フィールド名
   * @param value 設定値
   * @throws ReflectiveOperationException
   */
  public void setStaticFieldValue(String fieldName, Object value) throws ReflectiveOperationException {
    Field f = this.targetClass.getDeclaredField(fieldName);
    f.setAccessible(true);
    f.set(this.targetClass, value);
  }

  /**
   * クラス変数の値を取得
   * @param returnClass 戻り値のクラス
   * @param fieldName フィールド名
   * @return 値
   * @throws ReflectiveOperationException
   */
  @SuppressWarnings("unchecked")
  public <T2> T2 getStaticFieldValue(Class<T2> returnClass, String fieldName) throws ReflectiveOperationException {
    Field f = this.targetClass.getDeclaredField(fieldName);
    f.setAccessible(true);
    return (T2)f.get(null);
  }

  /**
   * 引数なしコンストラクタにアクセス.
   * @return リフレクション対象クラスのインスタンス
   * @throws ReflectiveOperationException
   */
  private T1 getInstance() throws ReflectiveOperationException {
    Constructor<T1> constructor = this.targetClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    return constructor.newInstance();
  }

  /**
   * 引数ありコンストラクタにアクセス.
   * @param paramType 引数の型
   * @param args リフレクション対象クラスのコンストラクタ引数
   * @return リフレクション対象クラスのインスタンス
   * @throws ReflectiveOperationException
   */
  private T1 getInstance(Class<?>[] paramType, Object... args) throws ReflectiveOperationException {
    Constructor<T1> constructor = this.targetClass.getDeclaredConstructor(paramType);
    constructor.setAccessible(true);
    return constructor.newInstance(args);
  }
}

先ほどのReflectionSampleクラスを例にした使用方法は以下のようになります。

public class Main {

  public static void main(String[] args) {

    try {

      // privateコンストラクタアクセス
      ClassReflector<ReflectionSample> cr1 = new ClassReflector<ReflectionSample>(ReflectionSample.class);

      // privateコンストラクタアクセス(引数あり)
      Class<?>[] constructorParams = {String.class, int.class};
      ClassReflector<ReflectionSample> cr2 = new ClassReflector<ReflectionSample>(ReflectionSample.class, constructorParams, "サンプル", 1);

      // privateメソッドアクセス
      System.out.println(cr1.runMethod(String.class, "output"));

      // privateメソッドアクセス(引数あり)
      Class<?>[] methodParams1 = {int.class, int.class};
      System.out.println(cr1.runMethod(methodParams1, int.class, "add", 1, 2));

      // 戻り値のないprivateメソッドアクセス
      cr1.runMethod(String.class, "voidMethod");

      // 戻り値のないprivateメソッドアクセス(引数あり)
      Class<?>[] methodParams2 = {String.class, int.class};
      cr1.runMethod(methodParams2, "setMyField", "本日は晴天なり", 100);

      // private静的メソッドアクセス
      System.out.println(cr1.runMethod(String.class, "staticOutput"));

      // privateメソッドアクセス(引数あり)
      System.out.println(cr1.runMethod(methodParams1, int.class, "staticAdd", 1, 2));

      // 戻り値のないprivate静的メソッドアクセス
      cr1.runMethod(String.class, "staticVoidMethod");

      // 戻り値のないprivate静的メソッドアクセス(引数あり)
      Class<?>[] methodParams3 = {int.class};
      cr1.runMethod(methodParams3, "staticSetMyField", 100);

      // privateインスタンス変数アクセス
      System.out.println(cr1.getFieldValue(String.class, "stringField"));
      cr1.setFieldValue("stringField", "曇ってきたようだ");
      System.out.println(cr1.getFieldValue(String.class, "stringField"));

      System.out.println(cr1.getFieldValue(int.class, "intField"));
      cr1.setFieldValue("intField", 9999);
      System.out.println(cr1.getFieldValue(int.class, "intField"));

      // privateクラス変数アクセス
      System.out.println(cr1.getStaticFieldValue(int.class, "staticIntField"));
      System.out.println(cr2.getStaticFieldValue(int.class, "staticIntField"));
      cr1.setStaticFieldValue("staticIntField", 1234);
      System.out.println(cr1.getStaticFieldValue(int.class, "staticIntField"));
      System.out.println(cr2.getStaticFieldValue(int.class, "staticIntField"));

    } catch (ReflectiveOperationException e) {
      e.printStackTrace();
    }
  }
}

これを実行するとコンソールに以下のように出力されます。

private コンストラクタ実行
private コンストラクタ実行 引数 val1=サンプル val2=1
private メソッド実行
outputメソッド実行
private メソッド実行 引数 val1=1 val2=2
3
private メソッド実行(戻り値なし)
private メソッド実行(戻り値なし) 引数 val1=本日は晴天なり val2=100
private 静的メソッド実行
staticOutputメソッド実行
private 静的メソッド実行 引数 val1=1 val2=2
3
private 静的メソッド実行(戻り値なし)
private 静的メソッド実行(戻り値なし) 引数 val=100
本日は晴天なり
曇ってきたようだ
100
9999
100
100
1234
1234

基本的な使い方では触れていませんが、引数のあるコンストラクタやメソッド、静的なフィールドのアクセスなども対応しています。
もし単体テストなどでリフレクションを使用しなければならない時がありましたらご自由にお使いください。
※当ロジックを使用した際に発生したトラブルなどについては責任は負いかねますのでご了承ください。

以上、記事の大半がソースコードになってしまいましたがリフレクションの使用方法でした。

Javaを勉強するのにおすすめの書籍はこちら


コメント

  1. […] 通常はアクセスできないシステムのメソッドやインスタンスに、shell権限を利用してリフレクションでアクセスするという力技を行っています。 「injectInputEvent」というのがそうで、そのメソッドにイベントデータを渡すとイベントが実行されます。 そのメソッドは実際どこにあるのか、AOSPのソースを見てみたところ、ここの914行目にありました。 Hideアノテーションで普通はアクセス出来ないようになっています。 リフレクション?という方はこの記事が参考になるかと思います。 […]