Javaにおけるリフレクション機構と動的プロキシの実装パターン

Javaのリフレクション(反射)機構は、アプリケーション実行フェーズでクラス構造やオブジェクトの状態を動的に解析し、メソッドの呼び出しやフィールドへのアクセスを可能にするメタプログラミング機能です。通常、インスタンス生成には`new`キーワードを用いますが、リフレクションを用いればコンパイル時に型が確定していなくても、文字列としてのクラスパスから対象を解決し操作できます。

Classオブジェクトの取得には以下の3つの標準的な手法があります。

  • インスタンスメソッド Object.getClass()
  • クラスリテラル 型.class
  • 静的ファクトリ Class.forName("完全修飾名")

設定ファイルや外部プラグインから読み込んだ文字列から型を解決するシーンでは、Class.forName()が最も頻繁に利用されます。

インスタンスの生成は、ClassクラスのnewInstance()(旧式API)またはConstructorオブジェクトのnewInstance()を用います。後者であれば特定のシグネチャを持つコンストラクタを指定できるため、依存性の注入や複雑な初期化処理に適しています。

Class<?> clazz = Class.forName("com.example.Device");
Constructor<?> ctor = clazz.getConstructor(String.class, int.class);
Object instance = ctor.newInstance("Router", 1001);

クラス内部のメタデータを取得するには、getDeclaredFields()getDeclaredMethods()getDeclaredConstructors()が提供されています。これらはアクセス修飾子に関わらず宣言されたメンバ全てを対象とします。一方、getFields()などはpublicメンバおよび継承メンバのみを返却します。

Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
    System.out.printf("戻り値型: %s, 引数型: %s%n", 
        m.getReturnType().getSimpleName(), 
        java.util.Arrays.toString(m.getParameterTypes()));
}

通常は非公開(private)メンバへの直接アクセスは言語仕様で禁止されていますが、リフレクションではjava.lang.reflect.AccessibleObject#setAccessible(true)を呼び出すことでアクセス制御チェックを一時無効化できます。これはフィールドのシリアライズ処理や、フレームワークのDIコンテナ、テストフレームワークのフィールド注入などで必須の技術です。

Field modelField = Device.class.getDeclaredField("modelBrand");
modelField.setAccessible(true);
String brand = (String) modelField.get(instance);

プロキシパターンは、本来のオブジェクトへのアクセスを中間層で間接的に制御する設計パターンです。静的プロキシが手動で実装クラスを定義するのに対し、動的プロキシは実行時にバイトコードを生成してプロキシインスタンスを構築します。JDK標準の動的プロキシはjava.lang.reflect.Proxyjava.lang.reflect.InvocationHandlerの組み合わせで実装されます。

実装例を示します。インタフェースDataProcessorとその実装StandardProcessorを前提とします。

public interface DataProcessor {
    void process(String payload);
}

public class StandardProcessor implements DataProcessor {
    @Override
    public void process(String payload) {
        System.out.println("データ処理実行: " + payload);
    }
}

インボケーションハンドラを実装し、プロキシ生成ロジックを定義します。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyController implements InvocationHandler {
    private final Object target;

    public ProxyController(Object target) {
        this.target = target;
    }

    @SuppressWarnings("unchecked")
    public <T> T createProxy() {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[Before] インターセプト開始");
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        System.out.println("[After] 処理時間: " + (System.nanoTime() - start) / 1_000_000.0 + "ms");
        return result;
    }
}

Proxy.newProxyInstance()は内部で以下の手順で動作します。

  1. 指定されたクラスローダーとインタフェースのシグネチャに基づき、Proxyを継承しインタフェースを実装する動的クラス(例: $Proxy0)のバイトコードを生成。
  2. 生成されたクラスのコンストラクタにInvocationHandlerのインスタンスを渡してオブジェクトを初期化。
  3. 生成されたプロキシインスタンスを返却。呼び出し元はインタフェース型にキャストして利用可能。

生成されたプロキシクラスの構造を確認するため、Proxy.isProxyClass()や内部APIを用いてバイトコードをダンプできます。実際にデコンパイルすると、Proxyクラスを継承し、インタフェースの各メソッド内でh.invoke()に処理を委譲する実装になっていることが確認できます。

// デコンパイルされた$Proxy0の構造イメージ
public final class $Proxy0 extends Proxy implements DataProcessor {
    private static Method m_process;
    
    public $Proxy0(InvocationHandler h) { super(h); }

    @Override
    public final void process(String payload) {
        try {
            h.invoke(this, m_process, new Object[]{payload});
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable t) {
            throw new java.lang.reflect.UndeclaredThrowableException(t);
        }
    }
    static {
        m_process = Class.forName("DataProcessor").getMethod("process", String.class);
    }
}

この委譲構造により、実際のメソッド実行前後にログ記録、トランザクション管理、認可チェックなどを横断的に挿入できます。

JDK動的プロキシはインタフェース実装クラスに限定されるため、インタフェースを持たないConcrete Classをプロキシしたい場合はCGLIBが適しています。CGLIBはバイトコード生成ライブラリを使用してサブクラスを作成し、メソッドの呼び出しをインターセプトします。

Maven依存関係:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

実装はEnhancerMethodInterceptorを組み合わせます。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CglibProxy implements MethodInterceptor {
    public Object createProxy(Class<?> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("[CGLIB Before] インターセプト開始");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("[CGLIB After] 処理完了");
        return result;
    }
}

CGLIBは継承ベースで動作するため、finalクラスやfinalメソッドはプロキシできません。また、生成されたバイトコードをディスクに保存して解析したい場合は、DebuggingClassWriter.DEBUG_LOCATION_PROPERTYをシステムプロパティとして設定することで出力先を指定できます。現代のフレームワークでは、ターゲットがインタフェースを実装している場合はJDKプロキシ、否则の場合はCGLIBを自動選択するハイブリッド戦略が採用されるのが一般的です。

タグ: Java Reflection JDKDynamicProxy CGLIB BitCodeGeneration

6月8日 16:28 投稿