1. 序章

この記事は、 Javaの動的プロキシについてです。これは、この言語で使用できる主要なプロキシメカニズムの1つです。

簡単に言えば、プロキシは、関数の呼び出しを独自の機能(通常は実際のメソッドに)を介して渡すフロントまたはラッパーであり、一部の機能を追加する可能性があります。

動的プロキシを使用すると、1つのメソッドを持つ1つのクラスで、任意の数のメソッドを持つ任意のクラスへの複数のメソッド呼び出しを処理できます。 動的プロキシは、一種の Facade と考えることができますが、任意のインターフェイスの実装のふりをすることができます。 裏では、すべてのメソッド呼び出しを単一のハンドラー invoke()メソッドにルーティングします。

これは日常のプログラミングタスク用のツールではありませんが、動的プロキシはフレームワークの作成者にとって非常に便利です。 また、実行時まで具体的なクラスの実装がわからない場合にも使用できます。

この機能は標準のJDKに組み込まれているため、追加の依存関係は必要ありません。

2. 呼び出しハンドラー

呼び出されるように要求されたメソッドを出力し、ハードコードされた番号を返す以外は実際には何もしない単純なプロキシを構築しましょう。

まず、java.lang.reflect.InvocationHandlerのサブタイプを作成する必要があります。

public class DynamicInvocationHandler implements InvocationHandler {

    private static Logger LOGGER = LoggerFactory.getLogger(
      DynamicInvocationHandler.class);

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
      throws Throwable {
        LOGGER.info("Invoked method: {}", method.getName());

        return 42;
    }
}

ここでは、呼び出されたメソッドをログに記録し、42を返す単純なプロキシを定義しました。

3. プロキシインスタンスの作成

定義したばかりの呼び出しハンドラーによって処理されるプロキシインスタンスは、java.lang.reflect.Proxyクラスのファクトリメソッド呼び出しを介して作成されます。

Map proxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), 
  new Class[] { Map.class }, 
  new DynamicInvocationHandler());

プロキシインスタンスを取得したら、通常どおりそのインターフェイスメソッドを呼び出すことができます。

proxyInstance.put("hello", "world");

予想どおり、呼び出されている put()メソッドに関するメッセージがログファイルに出力されます。

4. Lambda式を介した呼び出しハンドラ

InvocationHandler は機能的なインターフェースであるため、ラムダ式を使用してハンドラーをインラインで定義できます。

Map proxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), 
  new Class[] { Map.class }, 
  (proxy, method, methodArgs) -> {
    if (method.getName().equals("get")) {
        return 42;
    } else {
        throw new UnsupportedOperationException(
          "Unsupported method: " + method.getName());
    }
});

ここでは、すべてのget操作に対して42を返し、それ以外のすべてに対してUnsupportedOperationExceptionをスローするハンドラーを定義しました。

まったく同じ方法で呼び出されます。

(int) proxyInstance.get("hello"); // 42
proxyInstance.put("hello", "world"); // exception

5. タイミング動的プロキシの例

動的プロキシの1つの潜在的な現実のシナリオを調べてみましょう。

関数の実行にかかる時間を記録したいとします。 この点で、最初に「実際の」オブジェクトをラップし、タイミング情報を追跡し、リフレクティブ呼び出しを行うことができるハンドラーを定義します。

public class TimingDynamicInvocationHandler implements InvocationHandler {

    private static Logger LOGGER = LoggerFactory.getLogger(
      TimingDynamicInvocationHandler.class);
    
    private final Map<String, Method> methods = new HashMap<>();

    private Object target;

    public TimingDynamicInvocationHandler(Object target) {
        this.target = target;

        for(Method method: target.getClass().getDeclaredMethods()) {
            this.methods.put(method.getName(), method);
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
      throws Throwable {
        long start = System.nanoTime();
        Object result = methods.get(method.getName()).invoke(target, args);
        long elapsed = System.nanoTime() - start;

        LOGGER.info("Executing {} finished in {} ns", method.getName(), 
          elapsed);

        return result;
    }
}

その後、このプロキシはさまざまなオブジェクトタイプで使用できます。

Map mapProxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, 
  new TimingDynamicInvocationHandler(new HashMap<>()));

mapProxyInstance.put("hello", "world");

CharSequence csProxyInstance = (CharSequence) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), 
  new Class[] { CharSequence.class }, 
  new TimingDynamicInvocationHandler("Hello World"));

csProxyInstance.length()

ここでは、マップとcharシーケンス(String)をプロキシしました。

プロキシメソッドを呼び出すと、ラップされたオブジェクトに委任され、ログステートメントが生成されます。

Executing put finished in 19153 ns 
Executing get finished in 8891 ns 
Executing charAt finished in 11152 ns 
Executing length finished in 10087 ns

6. 結論

このクイックチュートリアルでは、Javaの動的プロキシとその可能な使用法のいくつかを調べました。

いつものように、例のコードはGitHubにあります。