Javaにおける動的プロキシ
1前書き
この記事はhttps://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html[Javaの動的プロキシ]についての情報です。これは、この言語で使用可能な主要なプロキシメカニズムの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ラムダ式を介した呼び出しハンドラ
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()
ここでは、mapと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の動的プロキシとその使用方法のいくつかを調べました。
いつものように、例の中のコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang[over on GitHub]にあります。