Javaの動的プロキシ

1.はじめに

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

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

動的プロキシを使用すると、1つのメソッドを持つ1つのクラスで、任意の数のメソッドを持つ任意のクラスへの複数のメソッド呼び出しを処理できます。動的プロキシは一種のファサードと考えることができますが、任意のインターフェイスの実装のふりをすることができます。裏では、すべてのメソッド呼び出しを単一のハンドラーである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.ラムダ式を介した呼び出しハンドラー

ための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()

ここでは、マップと文字シーケンス(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にあります。