---
title: "Javaリフレクションとは？invoke・アノテーション・Proxyの使い方とJava 17以降の制限"
url: "https://www.issoh.co.jp/tech/details/3255/"
published: 2024-08-02
updated: 2026-10-28
categories: ["Java"]
publisher: "株式会社一創"
---

# Javaリフレクションとは？invoke・アノテーション・Proxyの使い方とJava 17以降の制限

Javaのリフレクションは、実行時にクラスのメソッド・フィールド・コンストラクタを `java.lang.reflect` のオブジェクトとして取り出し、名前を文字列で指定して呼び出したり書き換えたりする仕組みです。SpringのDI、JUnitのテスト検出、Jacksonのシリアライズが内部で使っています。この記事では、JDK 25（LTS）と2026年9月15日にGAとなったJDK 27で実際に動かしたコードで、`Method.invoke`、アノテーションの読み取り、動的プロキシを示し、Java 17以降に「禁止」される操作と代替手段の選び方までを扱います。言語をまたいだ概念の整理は[リフレクションとは？実行時にクラス情報を操る仕組みを言語横断で解説](/tech/details/15262/)にまとめています。

## まとめ：Javaリフレクションの使い方と制限の要点

- 起点は `Class` オブジェクトです。`Class.forName("p.Account")` で取得し、`getDeclaredMethod`・`getDeclaredField`・`getDeclaredConstructor` で部品を取り出します。
- `getMethod` は継承分を含むpublicメンバーだけ、`getDeclaredMethod` はそのクラス自身が宣言したメンバーをprivateも含めて返します。
- 別のネストに属するクラスのprivateメンバーは `setAccessible(true)` でアクセス検査を外してから使います。呼び出し先が投げた例外は `InvocationTargetException` に包まれるので、`getCause()` で取り出します。
- アノテーションを実行時に読めるのは `@Retention(RetentionPolicy.RUNTIME)` が付いたものだけです。
- `java.lang.reflect.Proxy` はインタフェースにだけ割り込めます。ハンドラーから投げた `InvocationTargetException` がインタフェースメソッドのthrows宣言で許容されていない場合、呼び出し側には `UndeclaredThrowableException` が届きます。
- Java 17以降、呼び出し元に開かれていないJDKのパッケージ内のprivateメンバーに対する `setAccessible(true)` は `InaccessibleObjectException` で失敗します。finalフィールドの書き換えはJDK 26から警告が出て、JDK 27でも同じです。
- 型安全を失うため、アプリケーションのコードで常用するものではありません。インタフェース、MethodHandle、アノテーションプロセッサで置き換えられるならそちらを選びます。

## Javaリフレクションの仕組みとjava.lang.reflectの主要クラス

JVMはロードしたクラスごとに `java.lang.Class` のインスタンスを1つ持ち、そこからメンバーの情報をたどれます。リフレクションはこの `Class` を入口に、メンバーを表すオブジェクトを受け取って操作するAPIです。

| クラス         | 表すもの               | 主な操作                        |
| ----------- | ------------------ | --------------------------- |
| Class       | クラス・インタフェース・record | forName / getDeclaredFields |
| Method      | メソッド               | invoke / getParameterTypes  |
| Field       | フィールド              | get / set / getGenericType  |
| Constructor | コンストラクタ            | newInstance                 |
| Proxy       | 動的プロキシ             | newProxyInstance            |

`Class` オブジェクトは `Account.class`、`obj.getClass()`、`Class.forName("p.Account")` の3通りで取れます。リフレクションが本領を発揮するのは、文字列でしか型名が分からない3つ目の場面です。

### getMethodとgetDeclaredMethodの違い

名前が似ていますが、返す範囲が違います。取り違えると `NoSuchMethodException` になります。

| メソッド                                   | private | 親クラスから継承したもの |
| -------------------------------------- | ------- | ------------ |
| getMethod / getMethods                 | 含まない    | 含む（publicのみ） |
| getDeclaredMethod / getDeclaredMethods | 含む      | 含まない         |

親クラスに定義されたpublicメソッド `hello()` を子クラスの `Class` から探すと、`getMethod("hello")` では見つかり、`getDeclaredMethod("hello")` では `NoSuchMethodException` になります。親のprivateメソッドが要るときは `getSuperclass()` で親の `Class` に移ってから `getDeclaredMethod` を呼びます。`getDeclaredMethods()` や `getDeclaredFields()` が返す配列の順序はJavadocで保証されていないので、順番に依存したコードは書かないでください。

### フィールド一覧と修飾子・ジェネリクス型の調べ方

クラスの構造を一覧するなら `getDeclaredFields()` を回します。修飾子の表示に長く使われてきた `Modifier.toString(f.getModifiers())` は、JDK 27で非推奨になりました。[JDK 27のModifierのJavadoc](https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/lang/reflect/Modifier.html)は、クラスファイルのアクセスフラグとJava言語の修飾子の対応がずれてきたことを理由に挙げ、フラグは `AccessFlag`、言語上の修飾子は各オブジェクトの `toGenericString()` で調べるよう案内しています。JDK 27の `javac -Xlint:deprecation` では `[deprecation] toString(int) in Modifier has been deprecated` と警告されます。

```
for (Field f : Class.forName("p.Account").getDeclaredFields()) {
    System.out.println(f.toGenericString() + "  " + f.accessFlags());
}
```

```
private java.lang.String p.Account.owner  [PRIVATE]
private final int p.Account.limit  [PRIVATE, FINAL]
private final int p.Account.quota  [PRIVATE, FINAL]
```

`accessFlags()` はJDK 20で追加されたメソッドで、JDK 25でも27でも同じ出力でした。`List<String>` 型のフィールドでは、`getType()` が `interface java.util.List` を、`getGenericType()` が `java.util.List<java.lang.String>` を返します。型引数まで要るJSONマッパーのような処理は `getGenericType()` を使います。

## リフレクションの使い方：インスタンス生成からMethod.invokeまで

別パッケージの次のクラスを、リフレクションだけで生成・呼び出し・書き換えする例です。1つ目を `p/Account.java`、2つ目を `Main.java` に保存し、親ディレクトリで `javac p/Account.java Main.java`、続いて `java -cp . Main` を実行します。

```
package p;

public class Account {
    private String owner = "taro";
    private final int limit = 100;
    private final int quota;

    public Account() { this.quota = 10; }

    private String mask(String s) { return s.charAt(0) + "***"; }

    public int limit() { return limit; }
    public int quota() { return quota; }
}
```

```
import java.lang.reflect.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("p.Account");
        Object a = c.getDeclaredConstructor().newInstance();

        Method mask = c.getDeclaredMethod("mask", String.class);
        try {
            mask.invoke(a, "taro");
        } catch (IllegalAccessException e) {
            System.out.println("IAE: " + e.getMessage());
        }
        mask.setAccessible(true);
        System.out.println(mask.invoke(a, "taro"));

        Field owner = c.getDeclaredField("owner");
        owner.setAccessible(true);
        owner.set(a, "jiro");
        System.out.println(owner.get(a));
    }
}
```

JDK 25.0.4.1（Temurin 25.0.4.1+1）での出力は次のとおりです。

```
IAE: class Main cannot access a member of class p.Account with modifiers "private"
t***
jiro
```

JDK 27（Zulu 27.28、ビルド27+35）では1行目が `... cannot access a member of class p.Account with private access` に変わりました。例外の型は同じ `IllegalAccessException` ですが、ログをメッセージの文字列で検索・集計している場合は、JDK 27以降で一致しなくなります。

### Class.forNameとgetDeclaredConstructorによるインスタンス生成

引数なしなら `c.getDeclaredConstructor().newInstance()`、String引数のコンストラクタなら `c.getDeclaredConstructor(String.class).newInstance("hanako")` のように、パラメータの型で目的のコンストラクタを指定します。以前よく使われた `c.newInstance()` はJava 9で非推奨になりました。コンストラクタが投げたチェック例外を宣言なしに素通しするためで、`Constructor.newInstance()` なら `InvocationTargetException` に包まれ、呼び出し側で扱えます。

### Method.invokeでprivateメソッドを呼ぶ手順とsetAccessible

`invoke` の第1引数は対象インスタンス、以降が実引数です。staticメソッドなら第1引数は `null` にします。上の出力のとおり、privateメソッドは `setAccessible(true)` を呼ぶまで `IllegalAccessException` で拒否されます。

呼び出し側と対象が同じトップレベルクラスの中にある（ネストしたクラス同士の）場合は、`setAccessible` なしでもprivateメソッドを呼べます。Java 11の[JEP 181（ネストメイト）](https://openjdk.org/jeps/181)で、同じネストのメンバーは互いのprivateにアクセスできるようになったためです。内部クラスで試すと「setAccessibleは不要」と誤解しやすいので、検証は別パッケージのクラスで行ってください。

### Field.setで値を書き換えるときの型と定数の落とし穴

`Field.set` は `Object` を受け取るので、型が合わなければ実行時に `IllegalArgumentException` になります。プリミティブ型には `setInt` などの専用メソッドがあります。

通常のクラスの非staticなfinalフィールドは、`setAccessible(true)` の後なら書き換えられます（JDK 26以降は警告付き）。static finalフィールド、recordのfinalフィールド、hidden classのfinalフィールドは書き換えられません。上の `Main.main` の末尾に次のコードを足し、`limit`（宣言時に定数 `100` で初期化）と `quota`（コンストラクタで代入）を書き換えます。

```
Field lim = c.getDeclaredField("limit");
lim.setAccessible(true);
lim.setInt(a, 999);
Field q = c.getDeclaredField("quota");
q.setAccessible(true);
q.setInt(a, 50);
p.Account acc = (p.Account) a;
System.out.println("limit via field=" + lim.getInt(a) + " via method=" + acc.limit());
System.out.println("quota via field=" + q.getInt(a) + " via method=" + acc.quota());
```

```
limit via field=999 via method=100
quota via field=50 via method=50
```

JDK 25・26・27のいずれも同じ結果です。`limit` は定数式で初期化されたfinalフィールドなので、`limit()` の中身がコンパイル時に `100` へ置き換わっています。テストで設定値を差し替えようとして効かないときは、この定数の畳み込みを疑ってください。

## アノテーションをリフレクションで読み取る手順

SpringやJPAが `@Autowired` や `@Column` を解釈できるのは、実行時にリフレクションでアノテーションを読んでいるからです。自作のアノテーションでも同じことができます。次の例は、`@Column` を付けたフィールドだけを列名と値の組にします。`Anno.java` に保存し、`javac Anno.java`、`java Anno` の順で実行します。

```
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Anno {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface Column { String value(); }

    @Target(ElementType.FIELD)
    @interface NoRuntime { }

    static class User {
        @Column("user_name") String name = "taro";
        @Column("age") int age = 30;
        @NoRuntime String memo = "x";
    }

    public static void main(String[] args) throws Exception {
        User u = new User();
        Map<String, Object> row = new LinkedHashMap<>();
        for (Field f : User.class.getDeclaredFields()) {
            Column col = f.getAnnotation(Column.class);
            if (col != null) row.put(col.value(), f.get(u));
        }
        System.out.println(row);
        Field memo = User.class.getDeclaredField("memo");
        System.out.println("NoRuntime=" + memo.getAnnotation(NoRuntime.class));
    }
}
```

```
{user_name=taro, age=30}
NoRuntime=null
```

`@NoRuntime` は付いているのに `null` が返ります。`@Retention` を省略した既定は `CLASS` で、[RetentionPolicyのJavadoc](https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/lang/annotation/RetentionPolicy.html)のとおりクラスファイルには残ってもVMは実行時に保持しません。自作アノテーションが読めないときに最初に確かめるのはここです。[Lombokとは？読み方・使い方とアノテーション一覧、Maven・Gradleの設定【1.18.48】](/tech/details/3300/)で扱う `@Getter` も、ソースコードで `@Retention(RetentionPolicy.SOURCE)` と宣言されているため、実行時のリフレクションでは見えません。Lombokはコンパイル時のアノテーションプロセッサとして動く道具です。

クラスに付けたアノテーションは、`@Inherited` を付けて宣言すると子クラスから `getAnnotation` で見えます。`getDeclaredAnnotation` はそのクラスに直接付いたものだけを返すので、同じ子クラスでも `null` になります（JDK 27で確認）。Springでのアノテーションの使われ方は[Spring Bootのアノテーション一覧｜仕組みと使い分け・Boot 4での変更点](/tech/details/4048/)を参照してください。

## 動的プロキシ（java.lang.reflect.Proxy）による呼び出しの制御

`Proxy.newProxyInstance` は、指定したインタフェースを実装するクラスを実行時に生成し、そのインタフェースのメソッド呼び出しを `InvocationHandler` に渡します。Objectのequals・hashCode・toStringも対象ですが、getClass・waitなどは対象外です。ログ出力やトランザクションのような横断処理を、対象クラスを変えずに差し込めます。次の例を `Proxy2.java` に保存し、`javac Proxy2.java`、`java Proxy2` の順で実行します。

```
import java.lang.reflect.*;

public class Proxy2 {
    interface Greeter { String greet(String name); }

    static class RealGreeter implements Greeter {
        public String greet(String name) {
            if (name.isEmpty()) throw new IllegalArgumentException("empty name");
            return "Hello, " + name;
        }
    }

    public static void main(String[] args) {
        Greeter target = new RealGreeter();
        InvocationHandler logging = (proxy, method, a) -> {
            System.out.println("call " + method.getName());
            try {
                return method.invoke(target, a);
            } catch (InvocationTargetException e) {
                throw e.getCause();
            }
        };
        Greeter g = (Greeter) Proxy.newProxyInstance(
                Greeter.class.getClassLoader(), new Class<?>[] { Greeter.class }, logging);
        System.out.println(g.greet("taro"));
        try { g.greet(""); } catch (RuntimeException e) { System.out.println("unwrapped: " + e); }

        InvocationHandler noUnwrap = (proxy, method, a) -> method.invoke(target, a);
        Greeter g2 = (Greeter) Proxy.newProxyInstance(
                Greeter.class.getClassLoader(), new Class<?>[] { Greeter.class }, noUnwrap);
        try { g2.greet(""); } catch (RuntimeException e) { System.out.println("as is: " + e); }
    }
}
```

```
call greet
Hello, taro
call greet
unwrapped: java.lang.IllegalArgumentException: empty name
as is: java.lang.reflect.UndeclaredThrowableException
```

JDK 25と27で同じ出力です。`catch` で剥がさない `noUnwrap` では、元の `IllegalArgumentException` ではなく `UndeclaredThrowableException` が届きます。`invoke` が投げる `InvocationTargetException` はチェック例外で、`greet` の `throws` 宣言に含まれないためです。[ProxyのJavadoc](https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/lang/reflect/Proxy.html)も、宣言されていないチェック例外はこの例外に包むと定めています。プロキシを自作するときは、ハンドラーで `getCause()` を投げ直す形を定型にしてください。

インタフェースではなくクラス（`RealGreeter.class`）を渡すと、`IllegalArgumentException: Proxy2$RealGreeter is not an interface` で失敗します。クラスそのものに割り込むにはサブクラスを生成するライブラリが要ります。[Spring Frameworkのリファレンス](https://docs.spring.io/spring-framework/reference/core/aop/proxying.html)によれば、Spring AOPは対象がインタフェースを1つ以上実装していればJDKの動的プロキシを、実装していなければCGLIBでサブクラスを作ります。CGLIBではfinalクラスをプロキシ化できず、finalメソッドやprivateメソッドにもアドバイスを適用できません。一方、JDKの動的プロキシなら、対象の実装クラスがfinalでもインタフェース経由の呼び出しにアドバイスを適用できます。仕組みの全体は[AOP（アスペクト指向プログラミング）とは？Spring AOPの仕組みと使い方・自己呼び出しの落とし穴](/tech/details/3422/)で解説しています。

## InvocationTargetExceptionの原因と例外処理

`java.lang.reflect.InvocationTargetException` は、`invoke` や `newInstance` で呼んだ先のメソッド自身が例外を投げたときに、その例外を包んで投げ直すチェック例外です。リフレクションの呼び出しは成功していて、中で例外が起きた、という意味になります。

```
try {
    mask.invoke(a, "");
} catch (InvocationTargetException e) {
    System.out.println("msg=" + e.getMessage());
    System.out.println("cause=" + e.getCause());
}
```

```
msg=null
cause=java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
```

上の出力のとおりメッセージは `null` なので、`e.getMessage()` だけをログに出すと原因が消えます。`e.getCause()` を出すか例外オブジェクトごとロガーに渡し、スタックトレースでは「Caused by:」の行を見てください。元の例外として扱いたいなら、`getCause()` が `RuntimeException` や `Error` のときはそのまま投げ直します。

| 例外                           | 起きる場面                      |
| ---------------------------- | -------------------------- |
| ClassNotFoundException       | forNameのクラス名誤り・クラスパス不足     |
| NoSuchMethodException        | 名前か引数の型が違う                 |
| IllegalAccessException       | setAccessible前のprivateアクセス |
| InvocationTargetException    | 呼び出した先で例外発生                |
| InaccessibleObjectException  | モジュールが開いていない               |
| UndeclaredThrowableException | プロキシが未宣言のチェック例外を投げた        |

## リフレクションが禁止される理由とJava 17以降の制限

「リフレクションは禁止」には2種類あります。型安全とリファクタリング耐性を失うためにアプリケーションコードでの使用を禁じるコーディング規約と、JDKそのものによる制限です。後者はJavaを上げると既存コードが実際に動かなくなります。

### JDK内部へのアクセスとInaccessibleObjectException（JEP 403）

Java 9のモジュールシステム以降、JDK内部への深いリフレクションは段階的に閉じられ、JDK 17の[JEP 403](https://openjdk.org/jeps/403)で `--illegal-access` オプションによる一括の抜け道がなくなりました。`String` のprivateフィールドを開こうとすると、JDK 25・26・27のいずれでも次の例外になります。

```
java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @7fbe847c
```

ライブラリの更新で解消できないときの応急処置は、`java --add-opens java.base/java.lang=ALL-UNNAMED Main` のように該当パッケージだけを開くことです（同じコードが通ることを確認済み）。ただしJDK内部の実装に依存し続けるので、JDKを上げるたびに壊れる可能性が残ります。Java 8から上げてこの例外が出たら、まず原因のライブラリを対応版に更新します。移行全体の進め方は[Java近代化の進め方｜Java 8から25へ上げる手順と実機で落ちる箇所【2026年10月】](/column/details/2902/)で扱っています。

### finalフィールドの書き換え警告（JEP 500・JDK 26と27）

JDK 26の[JEP 500](https://openjdk.org/jeps/500)で、リフレクションによるfinalフィールドの書き換えに警告が出るようになりました。前の章で `Account.limit` を書き換えるコードをJDK 26.0.2.1で動かすと、処理は続行し、次の警告が出ます（JDK 27でも同じ）。

```
WARNING: Final field limit in class p.Account has been mutated reflectively by class Main in unnamed module @33a10788 (file:/private/tmp/refl3255/out/)
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
```

既定は `--illegal-final-field-mutation=warn` で、警告はモジュールごとに最初の1回だけ出ます。`--illegal-final-field-mutation=deny` を付けて起動すると、将来の既定である拒否の挙動をいま試せます（JDK 25にはこのオプションが無く、起動時に `Unrecognized option` で止まります）。

```
java.lang.IllegalAccessException: class Main (in unnamed module @7bb11784) cannot set final field p.Account.limit (in unnamed module @7bb11784), unnamed module @7bb11784 is not allowed to mutate final fields
```

2026年10月22日更新の[JDK 28のページ](https://openjdk.org/projects/jdk/28/)を見る限り、拒否を既定にするJEPはまだ対象に入っていません。それでも、テストやDIでfinalフィールドに値を注入している箇所は、この警告を目印に今のうちに洗い出しておくべきです。根本対応はコンストラクタ注入への変更で、`--enable-final-field-mutation` での許可は移行までのつなぎです。JDK 26の変更点は[Java 26（JDK 26）の新機能10個のJEPと現在地｜サポート終了後の移行先と実機で確かめた挙動](/tech/details/11412/)、JDK 27は[Java 27とは｜非LTSの半年運用とG1・オブジェクトヘッダ既定化の影響](/tech/details/17712/)にまとめています。

### recordフィールドの書き換え禁止とSecurityManagerの恒久無効化

recordの構成要素に対応するfinalなインスタンスフィールドは `setAccessible(true)` の後でも書き換えられません。JEP 500によれば、JDK 16でrecordを導入した時点から深いリフレクションによる書き換えを禁じています。`record Point(int x, int y)` の `x` に `Field.setInt` を呼ぶと、`Can not set final int field Demo$Point.x to (int)5` の `IllegalAccessException` になります。構成要素の名前や型は `getRecordComponents()` で取得できます。

かつてリフレクションの権限を絞る手段だったSecurityManagerは、JDK 24の[JEP 486](https://openjdk.org/jeps/486)で恒久的に無効化されました。`ReflectPermission("suppressAccessChecks")` でアプリ内の `setAccessible` を止める設計は、現行のJDKでは成り立ちません。制限の手段はモジュールの `opens` と、上のfinal書き換えの制御です。

## SpringやJUnitがリフレクションを使っている箇所

- **Spring**：`@Autowired` のフィールド注入は、privateフィールドに `setAccessible(true)` して値を入れる処理です。`@Transactional` などは前の章の動的プロキシかCGLIBのサブクラスで実現されています。
- **JUnit**：`@Test` が付いたメソッドを走査で見つけ、`invoke` で実行します。テストが投げた例外がレポートで元の型のまま見えるのは、`InvocationTargetException` を内部で剥がしているためです。書き方は[JUnitとは？Javaの単体テストの書き方・実行結果の読み方とJUnit 6の変更点](/tech/details/3064/)で扱っています。
- **Jackson**：JSONのキー名とフィールド・getterの名前を実行時に突き合わせます。任意のクラス名をJSONから受け取ってインスタンス化する設定は、[安全でないデシリアライゼーションとは？RCEに至る仕組みと言語別の防御実装を解説](/tech/details/16369/)で説明している攻撃の入口になります。

## リフレクションを使うべき場面と代替手段の選び方

自前のコードでリフレクションを書いてよいのは、呼び出す型がコンパイル時に本当に決まらない場合だけです。プラグインのjarを実行時に読み込む、設定ファイルに書かれたクラス名から処理を切り替える、といった場面が該当します。「if文が長くなるから」「privateを触りたいから」という理由なら、次の代替を先に検討してください。業務システムでこの線引きを設計段階から決めておきたい場合は、[フルスクラッチ開発の支援](/service/system/fullscratch/)でもご相談いただけます。

| やりたいこと          | リフレクション以外の手段              |
| --------------- | ------------------------- |
| 処理を動的に切り替える     | インタフェース＋Map、ServiceLoader |
| 同じメソッドを繰り返し呼ぶ   | MethodHandle              |
| アノテーションからコードを作る | アノテーションプロセッサ              |
| privateをテストしたい  | 公開メソッド経由のテスト              |

### MethodHandleに置き換える条件

`java.lang.invoke.MethodHandle` は、アクセス権の検査を取得時に1回だけ行い、以降の呼び出しでは省けるAPIです。privateメソッドも `MethodHandles.privateLookupIn(Account.class, MethodHandles.lookup())` から `findVirtual` で取得できます（別モジュールのクラスなら対象パッケージの `opens` が前提）。JDK 18の[JEP 416](https://openjdk.org/jeps/416)で `Method.invoke` 自体もMethodHandleの上に作り直され、`Method` などを `static final` のフィールドに保持するとJITが定数として最適化できるようになりました。呼び出しのたびに `getDeclaredMethod` で探し直すコードはこの恩恵を受けられません。まず取得結果を `static final` に置き、それでも足りない箇所だけMethodHandleへ移します。

### GraalVMネイティブイメージで失敗する理由

GraalVMのネイティブイメージはビルド時に到達可能なコードだけを取り込みます。`Class.forName("Foo")` のような定数の呼び出しは自動で解析されますが、設定ファイルや入力で決まるクラス名は実行時に見つかりません。そうした型は `META-INF/native-image/` 配下の `reachability-metadata.json` で登録します。Spring Boot 3以降のAOT処理がリフレクション用のメタデータを自動生成しているのはこのためです。採用の可能性があるなら自前のリフレクションは避けておきます。全体像は[GraalVMとは？JVMとの違いとNative Imageの作り方【2026年版】](/tech/details/3969/)を参照してください。

### privateメソッドをリフレクションでテストしない理由

privateメソッドを `setAccessible(true)` で直接呼ぶテストは、名前を変えてもコンパイルエラーにならず、実行時の `NoSuchMethodException` で初めて気づくことになります。privateのロジックが単独でテストしたいほど大きいなら、責務を別クラスに切り出します。同じパッケージからテストできるなら、公開範囲はpackage-privateにとどめ、外部への公開が必要な場合だけpublicにします。外部依存を差し替えたいだけなら、[テストダブル（TestDouble）とは｜5分類の違いと使い分け](/column/details/3531/)で紹介しているスタブやモックを使います。

## よくある質問

### Javaのリフレクションとは何ですか？

実行時にクラスのメソッド・フィールド・コンストラクタを `java.lang.reflect` のオブジェクトとして取得し、名前を指定して呼び出したり書き換えたりする仕組みです。コンパイル時に型が決まらない処理に使います。

### リフレクションはなぜ禁止と言われるのですか？

型の誤りがコンパイル時に見つからず、名前の変更で実行時に壊れるため、コーディング規約で禁じる現場があります。JDK側でも、Java 17以降は、呼び出し元に開かれていないJDKのパッケージ内のprivateメンバーに対する `setAccessible(true)` が `InaccessibleObjectException` で失敗し、JDK 26からはfinalフィールドの書き換えに警告が出ます。

### Javaでリフレクションを禁止するにはどうすればよいですか？

SecurityManagerはJDK 24で無効化されたので、実行時に一律で止める手段はありません。自分のコードを守るならモジュール（`module-info.java`）で必要なパッケージだけを `opens` し、JDK 26以降では `--illegal-final-field-mutation=deny` で、許可されていないfinalフィールドのリフレクションによる書き換えを拒否できます。`--enable-final-field-mutation` で許可したモジュールは対象外です。チームで使用を禁じるなら、Checkstyleの `IllegalImport` などで `java.lang.reflect` のimportを検出します。

### method.invoke()でメソッドを動的に呼び出すには？

`Class` から `getDeclaredMethod("名前", 引数の型...)` で `Method` を取り、`method.invoke(インスタンス, 引数...)` を呼びます。別のネストにあるprivateメソッドは、先に `setAccessible(true)` でアクセス検査を外します。staticメソッドは第1引数に `null` を渡します。

### InvocationTargetExceptionが出たときの原因の調べ方は？

呼び出した先のメソッドが例外を投げています。`e.getCause()` で元の例外を取り出すか、スタックトレースの「Caused by:」を確認してください。`invoke` が投げたこの例外の `e.getMessage()` は `null` です。

## 関連記事

- [リフレクションとは？実行時にクラス情報を操る仕組みを言語横断で解説](/tech/details/15262/)
- [AOP（アスペクト指向プログラミング）とは？Spring AOPの仕組みと使い方・自己呼び出しの落とし穴](/tech/details/3422/)
- [Spring Bootのアノテーション一覧｜仕組みと使い分け・Boot 4での変更点](/tech/details/4048/)
- [GraalVMとは？JVMとの違いとNative Imageの作り方【2026年版】](/tech/details/3969/)
- [Java 27とは｜非LTSの半年運用とG1・オブジェクトヘッダ既定化の影響](/tech/details/17712/)

---

出典: [Javaリフレクションとは？invoke・アノテーション・Proxyの使い方とJava 17以降の制限](<https://www.issoh.co.jp/tech/details/3255/>)（株式会社一創）
