1. はじめに
Javaの標準ライブラリではjava.lang.reflect.Proxy
クラスがあります。このクラスはJ2SE 1.3から追加されたもので、提供する機能は一般的にDynamicProxy(以下、日本語の「動的プロキシ」で表記)と呼ばれています。J2SE 1.3は2000年5月にリリースされたものなので動的プロキシもリリースから長い年月が経過しており、すでに枯れた技術となっていますが、今でも多数のJavaのフレームワークやライブラリが使用している一方、一般的なJavaプログラマの認知度はあまり高くないようです。
本記事では、Javaの基本的な文法を理解している方を対象に、動的プロキシを理解するために一般的なプロキシパターンを紹介して、動的プロキシの必要性を説明した上で、Javaの動的プロキシの使い方を説明します。また、Java以外の言語(C#, Python, EcmaScript)の動的プロキシの実装方法も紹介します。
本記事で掲載するJavaコードは、以下の環境で動作確認をしています。(Java以外の言語の動作環境は、随時記載します)また、掲載するJavaコードはコンパイルに必要な一部のimport句を省略しています。
名称 | バージョン |
---|---|
OpenJDK | 11.0.1 |
2. 「静的」なプロキシ(の続き)
動的プロキシである、java.lang.reflect.Proxy
の使い方を理解するのはJavaを習得しているプログラマであれば、決して難しくはないでしょう。しかし、動的プロキシの必要性を理解し有効に使用するためには、「動的」の反対語となる「静的」なプロキシを理解する必要があります。これは「プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる」で解説をしました。
###2-1. 静的プロキシのおさらい
「プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる」では、共通のインターフェースを具象する複数のクラスについて、ひとつのプロキシで共通処理を定義できる点までを紹介しました。本記事では、そのアンチパターンから説明します。
###2-2. 静的プロキシの限界
静的なプロキシの適用には、アンチパターンがあります。
上記で示した「インターフェースに対する複数の具象クラスの共通処理を追加するプロキシ」の、さらに具体的な例を示します。
ある販売サイトのモデル層の実装で、「顧客」「注文」「商品」の3つのクラスを定義します。
このクラスは全てのメソッドの処理でRDBMSにアクセスをするために、メソッドの実行前にはRDBMSの接続を取得してトランザクションを開始する必要があります。そして、実行後にはトランザクションのコミットあるいはロールバックと接続を閉じる処理が必要です。この3つのクラスはいずれも、データを作成するための「追加する」、データを更新するための「更新する」、データの検索するための「検索する」、詳細なデータを取得するための「詳細取得する」のメソッドがあれば事足りそうでした。ですので、この3つのクラスは
Model
というインターフェースの具象クラスとします。そして、RDBMS処理に関するプロキシとなるTransactionBarrier
というクラスをModel
の具象クラスで定義します。
これらのクラスの関係を表したクラス図です。
上記のクラス設計である程度までは、開発がうまくいっていましたが、ある日問題が起きます。
「顧客」の情報は、顧客情報保護のため一括削除が必要となりました。
そこで「顧客」に「一括削除する」というメソッドを追加しますが、この「一括削除する」というメソッドにも
TransactionBarrier
によるRDBMSの事前事後処理が必要です。そのため、Model
インターフェースにも「一括削除する」というメソッドを追加します。すると、「一括削除する」というメソッドが必要ない「注文」と「商品」にも、
Model
インターフェースに追加した「一括削除する」を追加しなければならなくなります。しかし、実際には「注文」と「商品」は「一括削除する」は呼び出してほしくないため、「注文」と「商品」は「一括削除する」メソッドには@deprecated
タグをつけた上でこのメソッドが呼ばれたら無条件でUnsupportedOperationException
をthrowしなければならなくなりました。
上記の例では、3つのクラスで一つの特殊なメソッド追加が発生しただけなので事態の深刻さが伝わりにくいのですが、実際のシステム開発ではプロジェクトによっては数十から数百のモデルが定義されます。すると、各モデルで独自のメソッドを定義する度に上記のような事例が頻発し、各クラスにはUnsuppportedOperationException
をthrowするメソッドが大量に追加する事になります。
このような状況に陥る理由は、本来一つのインターフェースの実装とさせてはいけないクラス群を、プロキシで共通処理を実装したいがために強引に一つのインターフェースを派生したために起こります。
本来は、以下のように「顧客」「注文」「商品」は、別々のインターフェースとプロキシを定義するべきでした。
しかし、上記のクラス設計では、プロキシクラスはインタフェース毎に実装をしなければならず、非常に面倒です。
そもそも、上記のようにインターフェースとクラスが1対1の関係であるのに、それぞれに同じ処理を実装するプロキシを定義するのであれば、直接実装クラスに共通処理を実装しても実装の手間は変わらないという点で、プロキシを使用する利点すら失われます。
異なる複数のインターフェースでも、ひとつの実装で定義できるプロキシがあればいいのですが、通常のJavaの文法ではそのような実装をはできません。ですが、動的プロキシを使えば複数のインターフェースを横断して共通のプロキシを実装できます。
3. 「動的な」プロキシ
それでは、動的プロキシであるjava.lang.reflect.Proxy
(以下、「Proxy
」と表記)を使用した実装例を紹介します。
3-1. java.lang.reflect.Proxyクラスの実装例
まずは、Proxy
で仲介されるクラスと、そのインターフェースを定義します。このコードは、「プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる」で掲載した、MyInterface
とMyClass
の実装と同じです。
public interface MyInterface {
public String myMethod(String value);
}
public class MyClass implements MyInterface {
@Override
public String myMethod(String value) {
System.out.println("Method:" + value);
return "Result";
}
}
Proxy
を使用するには、java.lang.reflect.InvocationHandler
インターフェースの具象クラスを定義しなけばなりません。InvocationHandler
の具象クラスではinvoke(Object, Method, Object[])
メソッドを実装します。このinvoke
メソッドは、具体的なプロキシの処理を実装するメソッドです。
InvocationHandler
の具象クラスの実装例のMyInvocationHandler
のコードです。MyInvocationHandler
のプロキシの処理では、事前処理でMyInvocationHandler:start
、事後処理でMyInvocationHandler:exit
を標準出力に出力します。
import java.lang.reflect.*;
/** java.lang.reflect.Proxyに指定する、プロキシの処理を実装するInvocationHandler実装クラス */
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("DynamicProxy:before");
Object result = method.invoke(target, args);
System.out.println("DynamicProxy:before");
return result;
}
}
上で定義したMyInvocationHandler
を利用するには、Proxy
のnewProxyInstance
メソッドを使用してProxy
のオブジェクトを取得します。
newProxyInstance
メソッドの第3引数に上で定義したMyInvocationHandler
クラスのオブジェクトを指定します。
MyClass myClassObj = new MyClass();
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(myClassObj);
MyInterface proxiedObj = (MyInterface)Proxy.newProxyInstance(MyClass.class.getClassLoader(),
new Class[] { MyInterface.class },
myInvocationHandler);
System.out.println(proxiedObj.myMethod("Argument"));
上記のコードを実行した結果です。
DynamicProxy:before
Method:Argument
DynamicProxy:after
Result
3-2. ProxyクラスのnewProxyInstanceメソッド
まずは、Proxy
のnewProxyInstance
メソッドの解説します。
newProxyInstance
によりProxy
オブジェクトを取得できます。利用者は取得したObject
型のオブジェクトを、第2引数で指定したインターフェースのクラス配列中のいずれかの型にキャストして使用します。このメソッドの返却値はObject
型ですが、Proxy
クラスの派生クラスであり、第二引数のすべてのインターフェースの具象クラスです。
MyInterface proxiedObj = (MyInterface)Proxy.newProxyInstance(MyClass.class.getClassLoader(),
new Class[] { MyInterface.class },
myInvocationHandler);
-
newProxyInstance
の第1引数にはクラスローダーを指定します。クラスローダーは全てのクラスからgetClassLoader()
メソッドで取得できるので、どのクラスのgetClassLoader()
で取得しても良いのですが、Webアプリケーションのような多数のクラスローダーチェインを用いた環境で使用する事を想定して、なるべくアプリで独自に実装しているクラスを指定しておいた方が良いです。
ここでは動的プロキシで仲介されるクラスであるMyClassのクラスローダーを指定しています。 -
newProxyInstance
の第2引数にはインターフェースクラスの配列を指定します。
Javaではひとつのクラスで複数のインターフェースを実装できるので、複数のインターフェースを実装した動的プロキシを生成できるように、配列で指定します。 -
newProxyInstance
の第3引数にはInvocationHandler
の具象クラスのオブジェクトを指定します。
この引数で指定するオブジェクトが、実際に動的プロキシの内部で実行する処理です。
ここでは実際にInvocationHandler
の具象クラスのMyInvocationHandler
を実装しましたので、そのオブジェクトを指定しています。
3-3. InvocationHandlerのinvokeメソッド
次にMyInvocationHandler
で実装した、InvocationHandler
インターフェースのinvoke
メソッドの解説します。
invoke
メソッドでプロキシの処理を実装します。Proxy
オブジェクトのインターフェースを具象するメソッドが呼ばれると、どのメソッドでも必ずこのinvoke
メソッドを呼び出します。invoke
メソッドには、Proxy
オブジェクトのどのメソッドがどのような引数を指定して呼び出されたかを引数で渡します。
一般的なプロキシを実装するのであれば、invoke
メソッドの中で、仲介先のオブジェクトのメソッドを呼び出します。呼び出すメソッドはinvoke
メソッドの引数で渡されますが、呼び出すオブジェクトはInvocationHandler
の具象クラス自身で保持します。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
-
invoke
の第1引数には、このinvoke
を呼び出したProxyクラスのオブジェクトが渡されます。 -
invoke
の第2引数には、Proxy
オブジェクトで呼ばれたメソッドを表すMethod
クラスのオブジェクトが渡されます。invoke
の処理の中では、この引数の値を利用して実際のオブジェクトのメソッドを呼び出します。 -
invoke
の第3引数には、Proxy
オブジェクトが呼ばれたメソッドに指定された引数をObject配列で渡されます。
これもまた、この引数の値を利用して実際のオブジェクトのメソッドを呼び出します。
3-4. 動的プロキシとは何が「動的」なのか?
動的プロキシの実装を実行した結果を見れば、「2-2. プロキシクラスを定義する」と同じ実行結果を得られるため、動的プロキシがプロキシの動作しているという点は容易に理解できます。しかし、何故これを「動的」と呼ぶのでしょうか?
動的プロキシの「動的」性を理解するために、Proxy
のnewPorxyInstance
を呼び出すコードを以下のように変えて実行します。MyInvocationHandler
は上で示した実装のままで構いません。
ArrayList<String> list = new ArrayList<String>();
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(list);
@SuppressWarnings("unchecked")
List<String> proxiedObj = (List<String>)Proxy.newProxyInstance(MyInterface.class.getClassLoader(),
new Class[] { List.class },
myInvocationHandler);
proxiedObj.add("hoge");
上記のコードを実行した結果です。
DynamicProxy:before
DynamicProxy:after
上記の実行結果の出力は、proxiedObj.add("hoge");
の呼出しにより、MyInvocationHandler
のinvoke
が呼び出されたことを示します。
ここで注目すべきは、これまではMyInterface
のProxy
オブジェクトを取得できていたものが、MyInvocationHandler
を一切変更せずに、List
インターフェースのプロキシも取得できている点です。MyInvocationHandler
のコードにはMyInterface
やList
の文字は一切なく、MyInvocationHandler
は実際にこれらのインターフェースには一切の依存をしていません。Proxy
のnewProxyInstance
の第2引数のインターフェースクラスの指定により、Proxy
の内部では新しいクラスをnewProxyInstance
の実行中に生成して、そのProxy
オブジェクトを取得します。
動的プロキシと実装したクラスの関係をクラス図で示します。
緑色のクラスはJavaのプロセス起動前から用意されたクラスであるのに対し、赤色のクラスはJavaのプロセスが起動してProxy
のnewProxyInstance
が呼び出されてから作成される(インスタンスではなく)クラスです。newProxyInstance
は(既に同じクラスを生成していればマップから取り出しますが)新しく生成したクラスのインスタンスを返却します。
なんと、newProxyInstance
メソッド中の処理では、ソースコードに存在しないクラスを生成します! 1
プロキシの「静的」「動的」とは、Javaのプロセスが実行されている期間において、事前から定義されていて最後まで不変なプロキシクラスを「静的」、プロセスを実行中の処理により途中から生成されるプロキシクラスを「動的」と呼んでいます。
4. より実践的な動的プロキシの実装
以上で、Proxy
の使い方は終了です。
・・・とはいえ、これまで紹介した動的プロキシの実装例では、オブジェクトの利用者はProxy
のnewProxyInstance
を呼び出して対象のオブジェクトを含むProxy
オブジェクトを取得します。しかし、この実装はあまり便利とは言えません。
ファクトリパターンを適用した実装
そこで、GoFのデザインパターンに**ファクトリパターン(Factory Pattern)**というのがあるので、このパターンを使用してProxy
オブジェクトの取得するコードを簡便にしていきます。ファクトリパターンは、オブジェクトの作成で利用者に直接コンストラクタを呼ばせず、オブジェクトを作成する専用のクラスのメソッドから取得する方法です。
例えば、
MyInterface myObj = MyFactory.getInstance();
のように、MyFactory
のgetInstance()
メソッドの呼出しで、Proxy
オブジェクトが取得できるようになれば、Proxy
オブジェクト利用側の実装は大変に簡便になります。
ところが、MyFactory
のgetInstance()
メソッドは呼び出し元から何の情報も渡さないため、このままでは、いつも同じInvocationHandler
を経由して同じクラス(ここではMyClass
のオブジェクトを呼び出すProxy
オブジェクトしか取得できません。引数でMyInterface
クラスとMyClass
オブジェクトとInvocationHandler
オブジェクトを渡せば、汎用性のあるファクトリにはなります。しかしよく考えてみると、InvocationHandler
は、プロキシが実行する処理の定義なので、ファクトリは内部で固定のInvocationHandler
の具象クラスを使用しても良いでしょう(InvocationHandler
の具象クラスを切り替えるのは、ファクトリクラスやメソッドの分別で可能です)。また、MyClass
オブジェクトもMyInterface
クラスに対する派生クラスがMyClass
で固定であれば引数での指定をする必要はありません。
そこで、ここでは、以下のようにファクトリの引数に、インターフェースのみ指定してProxy
オブジェクトを取得するファクトリを実装してきます。
MyInterface myObj = MyFactory.getInstance(MyInterface.class);
上記のようなファクトリを実装すると仮定した場合には、ひとつ問題があります。
この引数にはインターフェースのクラスしか指定が無いので、インターフェースに対する派生クラスをファクトリの内部で取得し、その派生クラスのオブジェクトを作成する必要があります。一番簡単な実現方法は、ファクトリの中でインターフェースに対するクラスのマップを持つ方法です。この方法でも良いですし十分実践的なのですが、インタフェースとクラスの定義が数十件、あるいは数百件となるようなシステムでは、インターフェースとクラスを新たに定義する度に、都度、ファクトリ内のマップを追加する必要があり、少し面倒なのとグループ開発ではソースの競合が発生し得ます。
そこで、インターフェースの付加情報で派生クラスを指定する方法をここでは紹介します。最初に、以下のような、アノテーションを定義します。
/** インターフェースに対する実装クラスを指定するアノテーション */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
Class<?> concretizedClass();
}
このアノテーションを、先ほど定義したMyInterface
に追加します。
アノテーションの引数で、ファクトリ内で選択する派生クラスを指定します。
/** アノテーションでインスタンス化する実装クラス定義を追加 */
@MyAnnotation(concretizedClass = MyClass.class)
public interface MyInterface {
public String myMethod(String value);
}
それではようやく、ファクトリクラスとなるMyFactory
の実装です。InvocationHandler
の具象クラスはMyFactory
のgetInstance
メソッド内で、匿名クラスで定義しています。
/** 引数で指定したインターフェースのオブジェクトを作成するクラス */
public class MyFactory {
/** 引数interfaceClassで指定したインターフェースの実装クラスのオブジェクトを取得するメソッド */
public static <I> I getInstance(Class<I> interfaceClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// MyInterfaceに対する実装クラスを、MyInterfaceのアノテーションから取得
MyAnnotation myAnnotation = interfaceClass.getAnnotation(MyAnnotation.class);
Class<?> objClass = myAnnotation.concretizedClass();
// 実装クラスのオブジェクトを作成
@SuppressWarnings("unchecked")
I obj = (I)objClass.getDeclaredConstructor().newInstance();
// InvocationHandlerを匿名クラスで定義してオブジェクトを作成
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("DynamicProxy:before");
Object result = method.invoke(obj, args);
System.out.println("DynamicProxy:after");
return result;
}
};
// プロキシオブジェクトを作成
@SuppressWarnings("unchecked")
I proxiedObj = (I)Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class[] { interfaceClass },
invocationHandler);
return proxiedObj;
}
}
上記のMyFactory
利用側の実装と実行結果です。
MyInterface myObj = MyFactory.getInstance(MyInterface.class);
System.out.println(myObj.myMethod("Argument"));
DynamicProxy:before
Method:Argument
DynamicProxy:after
Result
さらに実践的なコードとするために
上記の改善で、実際に「使える」コードに近づきましたが、まだ改善の余地や、修正すべき問題点が残っています。
以下に列挙するので、実際に動的プロキシを適用する際の参考にしてください。
-
MyFactory
のgetInstance
メソッドは、内部で発生しうるNoSuchMethodException
をそのままthrowしている。実際に発生しえない例外はthrows句に含めないようにするべき - 引数で指定した
interfaceClass
に対して、アノテーションで指定したobjClass
がinterfaceClass
の具象クラスであるチェックをするべき -
interfaceClass
に対するobjClass
の指定をインターフェースのアノテーションのみではなく、MyFactoryに直接指定できるようにするか、設定ファイルから設定できるようにするオプションを設けると便利 -
interfaceClass
はメソッドの引数で指定するより、ジェネリックで指定する方がカッコいい -
Object result = method.invoke(obj, args);
で発生する例外をそのままthrowするか、getCause()
で原因となったThrowable
オブジェクトをthrowするかを決めるべき -
getInstance()
で毎回新しいProxy
オブジェクトを生成しているが、Sigletoneで良いオブジェクトであれば都度生成しないようにする -
Proxy
の利用側は、直接MyFactory
を使用するのではなくDI(Devedency Injection)によりオブジェクトを取得できるようにする
なお、このようなインターフェースに対する派生クラスを決定して適切なオブジェクトを返却するパターンを、ファクトリパターンとは別にServiceLocatorパターンと呼びます。
5. 他の言語での動的プロキシ
これまで、Javaによる動的プロキシの紹介をしてきました。
他のプログラム言語では、動的プロキシはどのように提供されているかを本章では紹介します。
5-1.C#
(cfm-art様のご指摘により、標準のRealProxyで動的プロキシ機能を提供しているとのことです。改めて本章は追記いたします)
C#の標準ライブラリである.NET Frameworkには、動的プロキシに相当するクラスはありません。
~~しかし、~~外部プロジェクトのCastleにより外部ライブラリで動的プロキシの機能が提供されています。
名称 | バージョン |
---|---|
.NET Framework | 4.6.1 |
Castle.DynamicProxy2 | 2.2.0 |
using Castle.Core.Interceptor;
using Castle.DynamicProxy;
namespace MyNamespace
{
public interface IMyInterface
{
string MyMethod(int value);
}
public class MyClass : IMyInterface
{
public string MyMethod(int value)
{
Console.WriteLine("MyMethod:" + value);
return "Result";
}
}
public class MyInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("MyInterceptor:before");
object ret = invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments);
Console.WriteLine("MyInterceptor:after");
invocation.ReturnValue = ret;
}
}
}
ProxyGenerator generator = new ProxyGenerator();
IMyInterface proxy = generator.CreateInterfaceProxyWithTargetInterface(
typeof(IMyInterface),
new MyClass(),
new MyInterceptor()) as IMyInterface;
Console.WriteLine(proxy.MyMethod(1));
MyInterceptor:before
MyMethod:1
MyInterceptor:after
Result
ProxyGenerator
で定義しているメソッドを見るとわかりますが、JavaのProxy
より提供している機能は豊富です。Javaでは、Proxy
クラスはインターフェースの具象クラスでのみ作成できますが、C#のProxyGenerator
ではインターフェースに加えてクラスの派生クラスの動的プロキシも作成できます。
5-2.Python
Pythonも動的プロキシに相当するクラスはありません。
しかし、標準の特殊メソッドで動的プロキシ相当の機能を実現できます。
名称 | バージョン |
---|---|
Python | 3.7.1 |
class MyProxy(object):
def __init__(self, obj):
self._obj = obj
def __getattr__(self, name):
def func(*args):
print(before)
result = getattr(self._obj, name)(*args)
print('after')
return result
return func
class MyClass(object):
def myFunction(self, arg):
print('myFunction ' + arg)
return 'result'
myObj = MyClass()
myProxy = MyProxy(myObj)
print(myProxy.myFunction('arg'))
before
myFunction arg
after
result
このMyProxy
クラスは、Pythonの文法を知らない方のために、簡単に解説をします。
Pythonではクラス定義に対して、前後に"__"が付く特定の名称のメソッドの特殊メソッド(Special Method)があります。特殊メソッドは、オブジェクトの作成や属性の呼出し、あるいは特定の演算子の値で指定された場合などの、オブジェクトで発生した特定のイベントにより呼び出されます。
__getattr__
メソッドは、そのクラスのオブジェクトで定義されていない属性(Attribute)を取得しようとしたときに呼び出されます。オブジェクトの利用側は、そのクラスに指定した名前の属性が定義されていなかった場合は、__getattr__
の戻り値を属性値と取得します。
Pythonでは、クラス上の変数もメソッドも全て属性です。MyProxy
クラスは__init__
メソッドと__getattr__
メソッドの二つの特殊メソッドしか定義していないため、このクラスのオブジェクトから属性を取得しようとする(つまり、何かしらのメソッドを呼び出そうとする)と、必ず__getattr__
メソッドが呼び出されます。そこで、MyProxy
が動的プロキシと同じ動作をするために、__getattr__
メソッドの内部ではfunc
という、プロキシの役割をする関数を定義してその関数を返却します。
呼出し元の実装は1行で、
myProxy.myFunction('hoge')
となっていますが、これはJavaのメソッド呼出しとは異なり、
-
myProxy
オブジェクトからmyFunction
という名称の属性を取得する。 - 1.で取得した属性の値を
('hoge')
で実行する。
と解釈する必要があります。myProxy
オブジェクトからmyFunction
という名称の属性を取得しようとしたけれども、myProxy
にはmyFunction
という名称の属性が無かったため、__getattr__
メソッドによる戻り値を取得して実行します。
5-3.EcmaScript(JavaScript)
EcmaScriptでは6からProxy
というクラスを提供してます。しかし、JavaのProxy
とは使い方が違い、オブジェクトのイベントハンドラーの役割を担います。
ここでは、Proxy
の中のイベントハンドラーの一つであるapply
という関数を紹介します。apply
関数は、関数を呼出した際に呼出されるイベントハンドラーです。
名称 | バージョン |
---|---|
node.js | v6.11.3 |
function myFunction(arg1, arg2) {
console.log("myFunction:" + arg1 + ", " + arg2);
return "result";
}
const handler = {
apply : function (target, thisArg, argumentsList) {
console.log("before");
var result = target(...argumentsList);
console.log("after");
return result;
}
}
let proxy = new Proxy(myFunction, handler);
console.log(proxy("hoge", "hage"));
before
myFunction:hoge, hage
after
result
6. 動的プロキシのアンチパターン
ここまで読んでいただければ、動的プロキシによる実装ができるようになったと思います。しかし、動的プロキシは自由度の高さと機能の特殊さ故に、使い方を誤ると機能的に要件は満たしていても、機能以外の問題を内包させる可能性がある技術でもあります。「5.他の言語での動的プロキシ」で紹介した言語は、本記事の執筆時点(2018年12月)ではいずれも広く使用されているメジャーなプログラム言語ですが、いずれも標準ライブラリでは動的プロキシを提供していない点を注目すべきです。極論を述べると、動的プロキシの使用そのものがアンチパターンであると言えます。 2
ここで、動的プロキシの特徴をまとめると、以下となります。
- 動的プロセス実行時に、存在しないソースコードのクラスを生成する。
- 動的プロキシの処理と、プロキシで仲介される処理を完全に分離する。
- 動的プロキシは仲介するインターフェースやクラスに依存せずに仲介をできる。
この特徴のなかで「2. 動的プロキシの処理と、プロキシで仲介される処理を完全に分離する」は、動的プロキシの利点でもあり欠点でもあります。動的プロキシで仲介されるオブジェクトを実装するクラスの開発者は、実際にアプリケーションが動作する環境では自分が作ったクラスが動的プロキシの仲介で呼び出されている事実を知らない可能性があります。しかし、そのクラスを動作させるには動的プロキシの仲介が必須であるとすると、自分が作ったクラスを別の環境で再利用したり試験のために独自の環境で動かすためには、プロキシ内部の処理を再現せねばならず難易度が大きくなります。また、エラーや障害の解決が困難になるかもしれません。
動的プロキシの最も簡単な代替策は、以下のようにラムダ式を使ってプロキシを定義する方法です。
MyInterface myObj = new MyClass();
MyProxy.invoke(() -> { nyObj.myMethod(); });
MyProxy
のinvoke
メソッドでは、プロキシの処理を実装して、引数で渡されたRunnable
オブジェクトを呼び出して、仲介した本来の処理を呼び出せます。この方法でも、インターフェースやクラスを横断したプロキシを実装できます。しかも、開発者はMyProxy
の実装を見れば、自分が定義したクラスがどのような仲介処理によって呼び出されるかがわかります。
以上を踏まえて、動的プロキシの適用法のアンチパターンと思われる使用例を挙げていきます。しかし、これらのパターンは必ずしも使ってはいけないパターンではありません。本当に動的プロキシの適用が最適であるか?他により有効な代替案はないか?を検討した方が良いという意味でのアンチパターンです。プロキシの本来の意味は代理人です。そこで、以下に「〇〇の代理人」という題でアンチパターンを紹介してきます。
6-1. 片隅で仕事をする代理人
例えば、ある巨大なWebシステムの中にある、ユーザなどの限定した情報を扱うために定義したクラスの中の、情報を検索するメソッドの中の、入力値をチェックする処理の中で、動的プロキシを適用するのはあまり好ましくは無いでしょう。仮に入力値の項目が大量にあるために処理の正規化と共通化が必要であっても、大量の異なる処理の一部を共通化する方法は動的プロキシ以外にもあります。
動的プロキシは、処理を完全に分離してしまうが故に、処理の全体像をコードから読み取りづらくなる副作用があります。狭い領域の処理は規模が大きくなっても、全体を把握できるような実装をするのが望ましいのではないでしょうか。
6-2. 秘密主義の代理人
Proxy
が使用するInvocationHandler
の具象クラスの中身が公開されていないにもかかわらず、開発者はその動的プロキシの処理を経由しないと実行できないクラスの実装をさせられたとします。自分が開発する実装の前提条件が秘密にされているのは、開発者にとってストレスとなるでしょう。もちろん、心理的な問題だけではなく、動的プロキシの処理が原因の問題がシステムに発生した場合、開発者はその調査に多くの工数をかける可能性があります。また、実装した処理の再利用が困難になるかもしれません。
6-3. 仕事を抱えすぎる代理人
動的プロキシの責務が曖昧になるケースです。当初は動的プロキシにログの出力やDB関連の初期化終期化の処理のようなシステムの業務以外の処理のみが実装されていたものが、HTTPリクエストボディのパース処理が入り、事前のチェック処理が入り、承認処理が入り・・・と動的プロキシの責務が増えてしまうケースです。
これは、動的プロキシに限った話ではありませんが、クラスの責務は明確にするべきです。また、修正をする際もそのポリシーは守り続けるべきです。必要であれば責務の異なる動的プロキシを多重に経由するべきです。
6-4. 代理人の氾濫
システムをコントローラ層、サービス層、ロジック層、データアクセス層のように多層構造とした場合や、オブジェクト同士が複雑に依存しているシステムで、全てのオブジェクトが代理人を仲介しているケースです。このような状況では、処理の上で必要なプロキシもあるのでしょうが、不要なプロキシも多数あると想像できます。プロキシの仲介は必要なものだけにするべきで、それ以外のプロキシの仲介が本当に必要なのかは慎重に検討するべきです。
6-5. 代理人もどき
InvocationHandler
の具象クラスの実装例を読んで気づいた方は多いと思いますが、Proxy
はプロキシパターンの実装を強制していません。InvocationHandler
のinvoke
のメソッドの中で、引数で渡されたメソッドを実際に呼出すかどうかは、完全にInvocationHandler
の具象クラスを実装する側の任意に委ねられています。したがって、Proxy
をプロキシパターン以外にも応用できます。
例えば、DTO(Data Transfer Object)はクラスメンバとそれに対応するsetメソッドとgetメソッドを定義するパターンです。これは、DTOの定義はsetメソッドとgetメソッドの宣言をするインターフェースのみで、内部の実装をInvocationHadler
で定義します。
public class MyDtoInvocationHandler implements InvocationHandler {
private Map<String, Object> map = new HashMap<String, Object>();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// getで始まるメソッドの処理
if (method.getName().startsWith("get")) {
return map.get(method.getName().substring(3));
}
// setで始まるメソッドの処理
else if (method.getName().startsWith("set")) {
// 省略
}
}
}
この適用が本当に有用であるかは疑問があります。DTOクラスの実装を省略できても、インターフェースの定義は必要で、そのインターフェースには各項目のsetメソッドとgetメソッドを定義しなければなりません。大抵の開発プロジェクトでは、DTOクラスやDBテーブルに対応するEntityクラスの実装は手書きではなく、設計書などから自動でコードを出力するツールを持っているでしょう。それであれば、出力するコードがクラスであるかインターフェースであるかはあまり問題にはなりません。逆に、特定のDTOに単純なクラスメンバのやり取り以外の実装を組み込む選択肢を開発者から奪うことになります。
6-6. 注文の多い代理人
料理店ではありません。代理人を仲介させるのに、インターフェースのアノテーションや設定ファイルに大量の情報を必要とする代理人です。「6-5. 代理人もどき」のようにProxy
にプロキシ以外の処理を行わせようとする場合も起こりがちです。大量の情報を代理人の利用者へ注文をするのであれば、それは代理人ではなく利用者側で独自に実装すればよいのではないでしょうか?
7. さいごに
本記事の動的プロキシの解説は以上となります。より詳しい解説は後述の参考サイトを読んでください。
動的プロキシの理解は決して難しくはありませんが、「1. はじめに」で述べた通り、私の個人的な経験では動的プロキシの一般的なJavaプログラマの認知度は決して高くない状況と感じております。これは、以下の理由によるものと推測しています。
- 初心者向けの文献では動的プロキシを紹介していない。
- 「プロキシ」という名称がサーバサイドの「プロキシ」と混同しやすい。
- 一般的なJavaの開発において動的プロキシが必要な局面は僅少である。(そもそも、動的プロキシでしか実現できない機能はない)
しかし、幾つかのJavaでの開発プロジェクトに携わった経験のあるプログラマの方であれば、自分の実装したコードをフレームワーク上で実行すると、スタックトレースに見覚えのないクラスの呼出しが表れているのを見て、不思議に感じた経験は無いでしょうか?そのような疑問を解決するには、動的プロキシを理解しておいた方が良いでしょう。また、Javaから他のプログラミング言語を学習する際には、Javaの動的プロキシの知識は無駄ではないと思います。
一方で、Proxy
を含むJ2SE 1.3のリリースが2000年5月であったのに対し、2000年9月にはマーティン・ファウラー氏がPOJO(Plane Old Java Object)を提唱したのは無関係ではいのでしょうが、本記事では準備不足のため扱えませんでした。
本記事を読んでいただいた方にとって、動的プロキシの知識を得るきっかけとなれば幸いです。
参考サイト
https://docs.oracle.com/javase/jp/10/docs/api/java/lang/reflect/Proxy.html
https://docs.oracle.com/javase/jp/8/docs/technotes/guides/reflection/proxy.html
https://www.ibm.com/developerworks/jp/java/library/j-jtp08305/
-
厳密には、上記の参考サイトに示したJava公式ドキュメントでは
プロキシ・クラスのコードは、信頼されたシステム・コードによって生成されるためです
とだけあり、newProxyInstnce
メソッド内で新しいクラスを生成するとは明記していません。実際はVMの実装に依存しますし、Pythonのようなアプローチで動的プロキシを実現している可能性もあります。しかし、Proxy
クラスはnewProxyInstnce
メソッドの引数でインターフェースクラスが指定されるまでは、どのような動的プロキシクラスが必要かはわからないため、newProxyInstnce
メソッド内で新しいクラスを生成すると解釈しています。 ↩ -
これは極論ですが、動的プロキシの使用は常にアンチパターンに陥る可能性を疑うくらい、慎重に検討をするべきです。 ↩