Javaにおける動的プロキシ

Javaの動的プロキシ

1. 前書き

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

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

動的プロキシを使用すると、1つのメソッドを持つ1つのクラスで、任意の数のメソッドを持つ任意のクラスへの複数のメソッド呼び出しを処理できます。 動的プロキシは一種のFacadeと考えることができますが、任意のインターフェイスの実装のふりをすることができます。 裏で、it routes all method invocations to a single handlerinvoke()メソッド。

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

この機能は標準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. ラムダ式を介した呼び出しハンドラー

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 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の動的プロキシとその可能な使用法のいくつかを調べました。

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