31
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaのリフレクション:標準APIとFieldUtils/MethodUtilsの使い分けと注意点

Last updated at Posted at 2025-12-11

1.はじめに

リフレクションとは

・実行時にクラスの構造(フィールド、メソッドなど)を動的に調査・操作できるJavaの機能
・通常はアクセスできないprivateなフィールドやメソッドにもアクセス可能

本記事の対象

・Java1.7をベースに開設
・Junit4を使用
・Apache Commons LangのFieldUtilsやMethodUtilsを活用
・標準リフレクションAPIとの比較

2.リフレクションの基礎

基本的なAPI

リフレクションでは以下のような基本APIを使用

// フィールドの取得
Field field = TestClass.class.getDeclaredField("fieldName");

// メソッドの取得
Method method = TestClass.class.getDeclaredMethod("methodName", String.class);

setAccessible(true)の役割

setAccessible(true)は、privateやprotectedなメンバーへのアクセス制限を解除するために使用

public class TargetClass {
    private String privateValue = "テスト";
}

public class SampleTest {

@Test
public void test() throws Exception {
    TargetClass obj = new TargetClass();

    // プライベートフィールドにアクセス
    Field field = TargetClass.class.getDeclaredField("privateValue");
    field.setAccessible(true);
    // 値を取得
    String value = (String) field.get(obj);
}

3.フィールドの書き換え

3.1 インスタンス変数の書き換え

標準リフレクションでの実装

TargetClass target = new TargetClass();

// すべてのフィールドを取得して対象フィールドを探す
Field[] allFields = target.getClass().getDeclaredFields();
for (Field field : allFields) {
    if ("privateField".equals(field.getName())) {
        field.setAccessible(true);
        field.set(target, "新しい値");
        break;
    }
}

または、フィールド名が分かっている場合:

Field field = target.getClass().getDeclaredField("privateField");
field.setAccessible(true);
field.set(target, "新しい値");

FieldUtilsを使った実装

Apache Commons LangのFieldUtilsを使うと、よりシンプルに書けます。

import org.apache.commons.lang.reflect.FieldUtils;

TargetClass target = new TargetClass();

// 第4引数のtrueはprivateフィールドへのアクセスを許可
FieldUtils.writeField(target, "privateField", "新しい値", true);

3.2 static変数の書き換え

標準リフレクションでの実装

Field staticField = TargetClass.class.getDeclaredField("staticField");
staticField.setAccessible(true);
staticField.set(null, "新しい値"); // staticフィールドなのでnullを指定

FieldUtilsを使った実装

// staticフィールドの場合、インスタンスの代わりにnullを渡す
FieldUtils.writeStaticField(TargetClass.class, "staticField", "新しい値", true);

4.メソッド呼び出しと例外ハンドリング

4.1 標準リフレクションでのメソッド呼び出し

getDeclaredMethod()とinvoke()

TargetClass target = new TargetClass();

// メソッドを取得(メソッド名と引数の型を指定)
Method method = target.getClass().getDeclaredMethod("methodName", String.class);
method.setAccessible(true);

// メソッドを実行
Object result = method.invoke(target, "引数の値");

private/protectedメソッドへのアクセス

privateやprotectedメソッドもsetAccessible(true)で呼び出し可能

// 引数なしのprivateメソッド
Method privateMethod = target.getClass().getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
Object result = privateMethod.invoke(target);

// 複数引数のprotectedメソッド
Method protectedMethod = target.getClass()
    .getDeclaredMethod("protectedMethod", String.class, int.class);
protectedMethod.setAccessible(true);
Object result = protectedMethod.invoke(target, "test", 123);

4.2 MethodUtilsを使ったメソッド呼び出し

publicメソッド専用という制約

MethodUtilsはpublicメソッドのみを対象

import org.apache.commons.lang.reflect.MethodUtils;

TargetClass target = new TargetClass();

// publicメソッドの呼び出し
Object result = MethodUtils.invokeMethod(target, "publicMethod", "引数の値");

標準リフレクションとの比較

// ❌ MethodUtilsではprivateメソッドを呼び出せない
try {
    MethodUtils.invokeMethod(target, "privateMethod", "test");
} catch (NoSuchMethodException e) {
    // privateメソッドは見つからない
}

// ✅ 標準リフレクションならprivateメソッドも呼び出せる
Method method = target.getClass().getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
method.invoke(target, "test");

4.3 MethodUtilsの落とし穴(null引数問題)

引数にnullを渡すとNoSuchMethodExceptionが発生

MethodUtilsは引数の実際の型からシグニチャ(引数の型や引数の数のこと)を推論
→nullを渡すと型が特定できず、例外が発生

TargetClass target = new TargetClass();

// ❌ これはNoSuchMethodExceptionが発生
try {
    MethodUtils.invokeMethod(target, "processValue", null);
} catch (NoSuchMethodException e) {
    // nullの型が判定できないためメソッドが見つからない
}

異常系テストでは標準リフレクションが必要

nullを引数として渡す異常系テストでは、標準リフレクションを使う必要がある

// ✅ 標準リフレクションなら型を明示的に指定できる
Method method = target.getClass()
    .getDeclaredMethod("processValue", String.class);
method.setAccessible(true);

// nullを渡してもOK(引数の型は既に指定済み)
try {
    method.invoke(target, (Object) null);
    fail("NullPointerExceptionが発生するはず");
} catch (Exception e) {
}

5.例外処理の注意点

5.1 InvocationTargetExceptionの扱い

メソッド内で発生した例外がラップされる

リフレクションで呼び出したメソッド内で例外が発生すると、
その例外はInvocationTargetExceptionに内包される

public class TargetClass {
    
    private void validateData(String data) {
        if (data == null) {
            throw new IllegalArgumentException("データがnullです");
        }
        // 正常処理
        System.out.println("データ: " + data);
    }
}

public class TestClass {
    
    @Test
    public void test() throws Exception {
        TargetClass obj = new TargetClass();
        
        Method method = TargetClass.class.getDeclaredMethod("validateData", String.class);
        method.setAccessible(true);

        try {
            // nullを渡して例外を発生させる
            method.invoke(obj, (String) null);
        } catch (IllegalArgumentException e) {
            // ❌ ここではキャッチできない
        }
    }

正しいコード

public class TestClass {
    
    @Test
    public void test() throws Exception {
        TargetClass obj = new TargetClass();
        
        Method method = TargetClass.class.getDeclaredMethod("validateData", String.class);
        method.setAccessible(true);

        try {
            // nullを渡して例外を発生させる
            method.invoke(obj, (String) null);
        } catch (InvocationTargetException e) {
            // ✅ リフレクション実行時はInvocationTargetExceptionに内包
            // 元の例外を取得
            Throwable cause = e.getCause();
            // 元の例外が IllegalArgumentException かどうか検証
            assertTrue(cause instanceof IllegalArgumentException);
            assertEquals("データがnullです", cause.getMessage());            
        }
    }

6.テストでの実践パターン

6.1 テストクラスでのoverride

テスト対象クラスを継承し、特定のメソッドで例外を発生させるテストパターン

テスト対象クラスを継承

public class TargetClass {

    /**
     * メイン処理:現在日時を返却
     */
    public String processWithCurrentDate() {
        Date currentDate = getCurrentDate();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(currentDate);
        
        return dateStr;
    }
    /**
     * 現在日時を取得
     */
    protected Date getCurrentDate() {
        return new Date();
    }
}

public class TestClass {
    // テスト用のインナークラスを定義
    private static class TestInstance extends TargetClass {
        private boolean exceptionFlg = false;
        
        public void setExceptionFlg(boolean flg) {
            this.exceptionFlg = flg;
        }
        
        @Override
        protected Date getCurrentDate() {
            // TargetClassのgetCurrentDateは実行されずに、このメソッドが実行される
            if (exceptionFlg) {
                throw new RuntimeException("テスト用例外です");
            }
            // flgが無効のときは元のメソッドを実行
            return super.getCurrentDate();
        }
    }    
    @Test
    public void test() throws Exception {
        TestInstance testInstance = new TestInstance();
        // 例外フラグを有効に設定
        testInstance.setExceptionFlg(true);

        try{
            // テスト実行
            MethodUtils.invokeMethod(testInstance, "processWithCurrentDate");
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            // 元の例外が IllegalArgumentException かどうか検証
            assertTrue(cause instanceof RuntimeException);
            assertEquals("テスト用例外です", cause.getMessage());            
        }

    }

最後に

本記事は、今年参画したプロジェクトでの経験をもとにまとめました。
約1000件のテストメソッドを設計・実装するという、
キャリアの中でも過去最大規模の単体テストフェーズでした。
プロジェクトではJUnit 4を使用していたため、
本記事で紹介したような標準リフレクションやApache Commons Langのユーティリティを活用する必要がありました。
正直なところ、JUnit 5であれば、もっとスマートな実現方法がいくつもあったはずで、当時はもやもやしていました。
とはいえ、この経験を通じてリフレクションAPIの深い部分まで理解でき、
標準APIとユーティリティライブラリの使い分けや、それぞれの制約・落とし穴についても実践的な知見を得ることができました。

いつか、JUnit 5についての内容もまとめたいと思います。

31
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?