ダイナミックプロキシクラス |
ドキュメントの目次 |
はじめに
ダイナミックプロキシの API
直列化
例
ダイナミックプロキシクラスとは、実行時に指定されたインタフェースのリストを実装するクラスのことです。 ダイナミックプロキシクラスを使用すると、メソッドが呼び出されるときに、そのインスタンスのいずれかのインタフェースを介して符号化され、統一インタフェースを介して別のオブジェクトにディスパッチされます。このため、インタフェースのリストに対して型保証されたプロキシオブジェクトを作成できます。 コンパイル時にツールを使用するなど、プロキシクラスを事前に生成する必要がなくなります。ダイナミックプロキシクラスのインスタンス上でメソッドを呼び出すと、インスタンスの呼び出しハンドラ内の 1 つのメソッドにディスパッチされ、呼び出されたメソッドを識別する
java.lang.reflect.Method
オブジェクト、および引数を含むObject
型の配列を使用して符号化されます。ダイナミックプロキシクラスは、インタフェース API を提供するオブジェクト上で呼び出しを行うときに、アプリケーションまたはライブラリから型保証されたリフレクトディスパッチを行う必要がある場合に使用します。たとえば、ダイナミックプロキシクラスをアプリケーションで使用すると、複数の任意のイベントリスナーインタフェース (
java.util.EventListener
を継承するインタフェース) を実装するオブジェクトを作成し、すべてのイベントログをファイルに記録するなど、さまざまなタイプのイベントを統一された方式で処理できます。
ダイナミックプロキシクラス (以下「プロキシクラス」) は、クラスを作成するときに、実行時に指定されたインタフェースのリストを実装するクラスです。
プロキシインタフェースは、プロキシクラスによって実装されるインタフェースです。
プロキシインスタンスは、プロキシクラスのインスタンスです。
プロキシクラスの作成
プロキシクラスは、プロキシインスタンスと同様に、java.lang.reflect.Proxy クラスの static メソッドを使用して作成します。
Proxy.getProxyClass
メソッドは、クラスローダおよびインタフェースの配列が渡されると、プロキシクラスのjava.lang.Class
オブジェクトを返します。プロキシクラスは、特定のクラスローダ内に定義されており、提供されたすべてのインタフェースを実装します。同じ組み合わせのインタフェースを持つプロキシクラスが、クラスローダ内にすでに定義されている場合は、既存のプロキシクラスを返します。 既存のプロキシクラスが存在しない場合は、クラスローダ内にプロキシクラスを動的に生成し、定義します。
Proxy.getProxyClass
に渡すことのできるパラメータには、いくつかの制約があります。
interfaces
配列のすべてのClass
オブジェクトは、クラスまたはプリミティブ型ではなくインタフェースを表していなければならないinterfaces
配列の複数の要素が、同じClass
オブジェクトを参照してはならない- すべての型のインタフェースは、対応するクラスローダから名前で参照できなければならない。つまり、クラスローダが
cl
、各インタフェースがi
の場合は、次の式が true でなければならないClass.forName(i.getName(), false, cl) == i- public 以外のインタフェースは、すべて同じパッケージ内になければならない。 同じパッケージ内にない場合は、インタフェースが定義されているパッケージにかかわりなく、プロキシクラスはすべてのインタフェースを実装できない
- 指定されたインスタンスが同じ署名を持つ場合、そのようなメソッドの任意の組に対して
- 任意のメソッドの戻り値の型がプリミティブ型または void の場合、それらのメソッドはすべて戻り値の型が同じでなければならない
- そうでない場合、いずれかのメソッドによる戻り値の型は、残りのメソッドによる戻り値の型すべてに対して割り当て可能でなければならない
- プロキシクラスを作成するときは、Virtual Machine に定義されているクラスの制限を超えてはならない。たとえば、VM が、クラスが実装できるインタフェース数を 65535 に制限している場合は、
interfaces
配列のサイズは 65535 を超えてはならないこれらの制約に対して違反が発生した場合は、
Proxy.getProxyClass
によってIllegalArgumentException
がスローされます。interfaces
配列の引数または要素がnull
の場合は、NullPointerException
がスローされます。プロキシインタフェースは、順番が区別されます。プロキシクラスを 2 回要求したときに、インタフェースの組み合わせが同じで順番が異なる場合は、2 つの異なるプロキシクラスが作成されます。プロキシクラスは、プロキシインタフェースの順番が区別されます。 これは、複数のプロキシインタフェースで、名前およびパラメータシグニチャーを共有するメソッドが使用されている場合に、メソッド呼び出しのエンコーディングが正しく行われるようにするためです。 詳細は、「複数のプロキシインタフェースで重複するメソッド」を参照してください。
同一のクラスローダとインタフェースのリストを使用して
Proxy.getProxyClass
を呼び出したときに、新しいプロキシクラスが再生成されないようにするには、ダイナミックプロキシクラス API を実装したときに、対応するローダおよびインタフェースのリストをキーにして、生成されたプロキシクラスをキャッシュに格納する必要があります。その実装においては、クラスローダおよびそのクラスが適時ガベージコレクトされるのを妨げるような方法で、クラスローダ、インタフェース、およびプロキシクラスを参照しないように注意する必要があります。プロキシクラスの特性
プロキシクラスには、次の特性があります。
- プロキシクラスは public または final で、abstract ではありません。
- プロキシクラスの修飾されていない名前は指定されません。ただし、文字列
「$Proxy」
で始まるクラス名の空きは、プロキシクラス用に予約されています。- プロキシクラスは、
java.lang.reflect.Proxy
を継承します。- プロキシクラスは、作成時に指定された順番でインタフェースを実装します。
- public 以外のインタフェースを実装する場合は、そのインタフェースと同じパッケージに定義されます。public インタフェースを実装する場合は、プロキシクラスのパッケージは指定されません。パッケージのシーリングを行なった場合でも、プロキシクラスは実行時に特定のパッケージに適切に定義されます。 クラスおよび特定の署名者が、同一のクラスローダおよびパッケージにすでに定義されている場合も、同様です。
- プロキシクラスは、作成時に指定されたすべてのインタフェースを実装します。 このため、
Class
オブジェクト上でgetInterfaces
を呼び出した場合は、同じインタフェースのリストを含む配列が、作成時に指定された順番で返されます。Class
オブジェクト上でgetMethods
を呼び出した場合は、それらのインタフェース内のすべてのメソッドを含むMethod
オブジェクトの配列が返されます。getMethod
を呼び出した場合は、プロキシインタフェース内で目的のメソッドが検索されます。Proxy.isProxyClass
メソッドは、プロキシクラス (Proxy.getProxyClass
によって返されるクラス、またはProxy.newProxyInstance
によって返されるオブジェクトのクラス) が渡された場合は true、それ以外の場合は false を返します。セキュリティを判定するときにこのメソッドを使用する場合は、信頼性が重要になります。 このため、渡されたクラスがjava.lang.reflect.Proxy
を継承しているかどうかを検査してから、追加の検査を行う必要があります。- プロキシクラスの
java.security.ProtectionDomain
は、java.lang.Object
などの、ブートストラップクラスローダによってロードされるシステムクラスのjava.security.ProtectionDomain
と同じです。 プロキシクラスのコードは、信頼されたシステムコードによって生成されるためです。標準では、この保護ドメインに対してjava.security.AllPermission
が与えられます。プロキシインスタンスの作成
各プロキシクラスには、public コンストラクタが 1 つ組み込まれています。 引数には、
InvocationHandler
インタフェースの実装を指定します。各プロキシインスタンスには、コンストラクタに渡された呼び出しハンドラオブジェクトが関連付けられます。プロキシインスタンスは、リフレクション API を介して public コンストラクタにアクセスしなくても、
Proxy.newProxyInstance
メソッドを呼び出すことによっても作成できます。Proxy.newProxyInstance
メソッドでは、Proxy.getProxyClass
を呼び出すアクションと、呼び出しハンドラを使用してコンストラクタを呼び出すアクションが行われます。Proxy.newProxyInstance
は、Proxy.getProxyClass
の場合と同じ理由で、IllegalArgumentException
をスローします。プロキシインスタンスの特性
プロキシインスタンスには、次の特性があります。
proxy
プロキシインスタンス、およびそのFoo
プロキシクラスによって実装されたインタフェースに対して次の式を実行すると、true が返されます。次のキャスト操作は、正常終了し、proxy instanceof Foo
ClassCastException
はスローされません。(Foo) proxy
- static
Proxy.getInvocationHandler
メソッドは、引数として渡されたプロキシインスタンスに関連付けられた呼び出しハンドラを返します。Proxy.getInvocationHandler
に渡されるオブジェクトがプロキシインスタンスでない場合は、IllegalArgumentException
がスローされます。- プロキシインスタンス上でインタフェースメソッドを呼び出すと、符号化され、前述したように呼び出しハンドラの
invoke
メソッドにディスパッチされます。プロキシインスタンス自体は、
invoke
の第 1 引数として、Object
型で渡されます。
invoke
に渡される第 2 引数は、プロキシインスタンス上で呼び出したインタフェースメソッドに対応するjava.lang.reflect.Method
インスタンスです。Method
オブジェクトの宣言クラスは、このメソッドが宣言されたインタフェースです。 プロキシクラスがメソッドを継承するプロキシインタフェースのスーパーインタフェースのこともあります。
invoke
に渡される第 3 引数は、プロキシインスタンス上のメソッド呼び出しで渡された引数の値が含まれるオブジェクトの配列です。プリミティブ型の引数は、java.lang.Integer
、java.lang.Boolean
など、適切なプリミティブラッパークラスのインスタンス内にラップされます。invoke
メソッドの実装を使用して、この配列の内容を任意に変更することができます。
invoke
メソッドから返された値は、プロキシインスタンス上のメソッド呼び出しの戻り値になります。インタフェースメソッドの宣言済みの戻り値がプリミティブ型の場合は、invoke
から返された値は、対応するプリミティブラッパークラスのインスタンスでなければなりません。 プリミティブ型以外の場合は、宣言済みの戻り値の型に割り当てることができる型でなければなりません。invoke
から返された値がnull
で、インタフェースメソッドの戻り値の型がプリミティブの場合は、プロキシインスタンス上のメソッド呼び出しからNullPointerException
がスローされます。invoke
によって返された値が、前述のようにメソッドの宣言済みの戻り値の型と互換性がない場合は、プロキシインスタンスからClassCastException
がスローされます。
invoke
メソッドから例外がスローされた場合は、その例外はプロキシインスタンス上のメソッド呼び出しからもスローされます。例外の型は、インタフェースメソッドのシグニチャー内に宣言されている例外の型か、チェックされないjava.lang.RuntimeException
またはjava.lang.Error
例外型に割り当てることができなければなりません。チェックされる例外がinvoke
からスローされたときに、その例外がインタフェースメソッドのthrows
句で宣言されている例外の型のどれにも割り当てることができない場合は、プロキシインスタンス上のメソッド呼び出しからUndeclaredThrowableException
がスローされます。UndeclaredThrowableException
は、invoke
メソッドからスローされた例外を使用して構築されます。- プロキシインスタンス上の
java.lang.Object
に宣言されているhashCode
、equals
またはtoString
呼び出しは、前述したようにインタフェースメソッド呼び出しと同じ方法で、符号化され、呼び出しハンドラのinvoke
メソッドにディスパッチされます。invoke
に渡されるMethod
オブジェクトの宣言クラスは、java.lang.Object
です。java.lang.Object
から継承されるプロキシインスタンスのその他の public メソッドは、プロキシクラスによってオーバーライドされません。 このため、これらのメソッドの呼び出しは、java.lang.Object
のインスタンスに対する呼び出しと同様に行われます。複数のプロキシインタフェースで重複するメソッド
複数のインタフェースに、同じ名前とパラメータシグニチャーを持つメソッドが含まれる場合は、プロキシクラスのインタフェースの順番が区別されます。プロキシインスタンス上で「重複するメソッド」が呼び出された場合、呼び出しハンドラに渡される
Method
オブジェクトで、プロキシメソッドの呼び出しに使用されたインタフェースの参照型から宣言クラスを割り当てることができないことがあります。このような制約が存在するのは、生成されたプロキシクラス内の対応するメソッドの実装から、その実装が呼び出されたときに使用されたインタフェースを特定できないためです。このため、プロキシインスタンス上で重複するメソッドが呼び出された場合は、メソッド呼び出しに使用された参照型にかかわりなく、プロキシクラスのインタフェースリストでそのメソッド (直接またはスーパーインタフェースから継承) を含むインタフェースのうち、最初のインタフェースのメソッドのMethod
オブジェクトが呼び出しハンドラのinvoke
メソッドに渡されます。プロキシインタフェースに、
java.lang.Object
のhashCode
、equals
、またはtoString
メソッドと同じ名前およびパラメータシグニチャーを持つメソッドが含まれる場合は、プロキシインスタンス上でそのメソッドが呼び出されると、呼び出しハンドラに渡されるMethod
オブジェクトの宣言クラスはjava.lang.Object
になります。つまり、public で非 final であるjava.lang.Object
のメソッドは、呼び出しハンドラに渡すMethod
オブジェクトを決定するときに、論理的にほかのプロキシインタフェースより優先されます。重複するメソッドが呼び出しハンドラにディスパッチされた場合は、
invoke
メソッドからスローできる例外の型は、チェックされる型のうち、呼び出されるすべてのプロキシインタフェースのメソッドに指定されている、throws
句の例外の型に割り当てることができるものに限定されます。invoke
メソッドが、チェックされる例外のうち、呼び出されるプロキシインタフェースのメソッドで宣言されている例外の型に割り当てることができないものをスローした場合は、プロキシインスタンス上の呼び出しによって、チェックされないUndeclaredThrowableException
がスローされます。つまり、invoke
メソッドに渡されたMethod
オブジェクト上で、getExceptionTypes
を呼び出して例外の型を取得しても、invoke
メソッドから正常にスローされないことがあります。
java.lang.reflect.Proxy
は、java.io.Serializable
を実装するため、この節で説明するように、プロキシインスタンスを直列化することができます。ただし、プロキシインスタンスにjava.io.Serializable
に割り当てることができない呼び出しハンドラが含まれている場合は、インスタンスがjava.io.ObjectOutputStream
に書き込まれるとjava.io.NotSerializableException
がスローされます。プロキシクラスの場合、java.io.Externalizable
を実装することは、直列化という点では、java.io.Serializable
を実装することと同じ効果があります。つまり、直列化の過程で、Externalizable
インタフェースのwriteExternal
およびreadExternal
メソッドが、プロキシインスタンス (または呼び出しハンドラ) 上に呼び出されることはありません。プロキシクラスのClass
オブジェクトは、すべてのClass
オブジェクトと同様に常に直列化することができます。プロキシクラスには、直列化可能なフィールドおよび
0L
のserialVersionUID
がありません。つまり、プロキシクラスのClass
オブジェクトがjava.io.ObjectStreamClass
の staticlookup
メソッドに渡されたときに、返されるObjectStreamClass
インスタンスには次の特性があります。
getSerialVersionUID
メソッドを呼び出すと、0L
が返されるgetFields
メソッドを呼び出すと、長さゼロの配列が返される- 任意の
String
引数を使用してgetField
メソッドを呼び出すと、null
が返されるオブジェクトの直列化に使用されるストリームプロトコルでは、
TC_PROXYCLASSDESC
という名前の型コードがサポートされています。TC_PROXYCLASSDESC
は、ストリーム形式の構文で使用する終端記号です。 この型と値は、java.io.ObjectStreamConstants
インタフェースの次の定数フィールドに定義されています。final static byte TC_PROXYCLASSDESC = (byte)0x7D;この構文には、次の 2 つの規則も適用されます。 最初の規則は、元の newClassDesc 規則の拡張として、代わりに適用されます。
newClassDesc:
TC_PROXYCLASSDESC
newHandle proxyClassDescInfoproxyClassDescInfo:
(int)<count>
proxyInterfaceName[count] classAnnotation superClassDescproxyInterfaceName:
(utf)
ObjectOutputStream
を使用してプロキシクラスのクラス記述子を直列化する場合は、Class
オブジェクトがProxy.isProxyClass
メソッドに渡されるため、上記の規則に従って型コードTC_CLASSDESC
の代わりにTC_PROXYCLASSDESC
が使用されます。proxyClassDescInfo が展開されたときに、proxyInterfaceName 項目のシーケンスは、プロキシクラスによって実装されたすべてのインタフェースの名前で構成されており、Class
オブジェクト上でgetInterfaces
メソッドの呼び出しから返された順番に並んでいます。classAnnotation と superClassDesc は、classDescInfo 規則の対応する項目と同じ意味を持ちます。プロキシクラスの場合は、superClassDesc はjava.lang.reflect.Proxy
スーパークラスのクラス記述子です。 この記述子を指定すると、プロキシインスタンスに対して直列化されたProxy
クラスを展開することができます。プロキシクラス以外の場合は、
ObjectOutputStream
から protectedannotateClass
メソッドが呼び出され、サブクラスから特定のクラスのストリームにカスタムデータを書き込むことができます。プロキシクラスの場合は、annotateClass
ではなく、そのプロキシクラスのClass
オブジェクトによってjava.io.ObjectOutputStream
内の次のメソッドが呼び出されます。protected void annotateProxyClass(Class cl) throws IOException;
ObjectOutputStream
内のannotateProxyClass
のデフォルトの実装は、処理を行いません。
ObjectInputStream
は、型コードTC_PROXYCLASSDESC
を検出すると、プロキシクラスのクラス記述子をストリームから上記の形式で直列化復元します。クラス記述子のClass
オブジェクトを解決するために、resolveClass
メソッドが呼び出される代わりに、次のjava.io.ObjectInputStream
のメソッドが呼び出されます。protected Class resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException;プロキシクラス記述子に直列化復元されたインタフェース名のリストは、
interfaces
引数としてresolveProxyClass
に渡されます。
ObjectInputStream
内のresolveProxyClass
のデフォルトの実装は、interfaces
パラメータに指定されたインタフェースのClass
オブジェクトのリストを使用して、Proxy.getProxyClass
を呼び出した結果を返します。各インタフェース名i
に対して使用されるClass
オブジェクトは、次の呼び出しによって返された値です。Class.forName(i, false, loader)loader
は、実行スタックに格納されるnull
でない最初のクラスローダです。 ただし、null
でないクラスローダがスタック上にない場合は、null
です。このクラスローダは、resolveClass
メソッドによってデフォルトで選択されるクラスローダと同じです。このloader
の値は、Proxy.getProxyClass
に渡されるクラスローダとなります。Proxy.getProxyClass
からIllegalArgumentException
がスローされた場合は、resolveClass
から、IllegalArgumentException
が含まれるClassNotFoundException
がスローされます。プロキシクラスには独自の直列化フィールドが存在しないため、プロキシインスタンスのストリーム表現内の classdata[] は、すべて
java.lang.reflect.Proxy
スーパークラスのインスタンスデータで構成されます。Proxy
には、h
という直列化フィールドが 1 つあります。 このフィールドには、プロキシインスタンスの呼び出しハンドラが含まれています。
任意のインタフェースのリストを実装するオブジェクト上で、メソッドを呼び出す前後にメッセージを出力する簡単な例を示します。
public interface Foo { Object bar(Object obj) throws BazException; } public class FooImpl implements Foo { Object bar(Object obj) throws BazException { // ... } } public class DebugProxy implements java.lang.reflect.InvocationHandler { private Object obj; public static Object newInstance(Object obj) { return java.lang.reflect.Proxy.newProxyInstance( obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new DebugProxy(obj)); } private DebugProxy(Object obj) { this.obj = obj; } public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { Object result; try { System.out.println("before method " + m.getName()); result = m.invoke(obj, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } catch (Exception e) { throw new RuntimeException("unexpected invocation exception: " + e.getMessage()); } finally { System.out.println("after method " + m.getName()); } return result; } }
Foo
インタフェースの実装に対してDebugProxy
を構築し、特定のメソッドを呼び出します。Foo foo = (Foo) DebugProxy.newInstance(new FooImpl()); foo.bar(null);ユーティリティ呼び出しハンドラクラスの例を示します。
java.lang.Object
から継承されるメソッドのデフォルトのプロキシ動作を提供し、呼び出されたメソッドのインタフェースに応じて、特定のプロキシメソッド呼び出しを各オブジェクトに委譲します。import java.lang.reflect.*; public class Delegator implements InvocationHandler { // preloaded Method objects for the methods in java.lang.Object private static Method hashCodeMethod; private static Method equalsMethod; private static Method toStringMethod; static { try { hashCodeMethod = Object.class.getMethod("hashCode", null); equalsMethod = Object.class.getMethod("equals", new Class[] { Object.class }); toStringMethod = Object.class.getMethod("toString", null); } catch (NoSuchMethodException e) { throw new NoSuchMethodError(e.getMessage()); } } private Class[] interfaces; private Object[] delegates; public Delegator(Class[] interfaces, Object[] delegates) { this.interfaces = (Class[]) interfaces.clone(); this.delegates = (Object[]) delegates.clone(); } public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { Class declaringClass = m.getDeclaringClass(); if (declaringClass == Object.class) { if (m.equals(hashCodeMethod)) { return proxyHashCode(proxy); } else if (m.equals(equalsMethod)) { return proxyEquals(proxy, args[0]); } else if (m.equals(toStringMethod)) { return proxyToString(proxy); } else { throw new InternalError( "unexpected Object method dispatched: " + m); } } else { for (int i = 0; i < interfaces.length; i++) { if (declaringClass.isAssignableFrom(interfaces[i])) { try { return m.invoke(delegates[i], args); } catch (InvocationTargetException e) { throw e.getTargetException(); } } } return invokeNotDelegated(proxy, m, args); } } protected Object invokeNotDelegated(Object proxy, Method m, Object[] args) throws Throwable { throw new InternalError("unexpected method dispatched: " + m); } protected Integer proxyHashCode(Object proxy) { return new Integer(System.identityHashCode(proxy)); } protected Boolean proxyEquals(Object proxy, Object other) { return (proxy == other ? Boolean.TRUE : Boolean.FALSE); } protected String proxyToString(Object proxy) { return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode()); } }
Delegator
のサブクラスは、invokeNotDelegated
をオーバーライドします。 この結果、プロキシメソッド呼び出しの動作が実装され、ほかのオブジェクトに直接委譲されることがなくなります。 また、proxyHashCode
、proxyEquals
、およびproxyToString
をオーバーライドします。 この結果、java.lang.Object
からプロキシが継承するメソッドのデフォルトの動作がオーバーライドされます。
Foo
インタフェースの実装に対して、Delegator
を構築します。Class[] proxyInterfaces = new Class[] { Foo.class }; Foo foo = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), proxyInterfaces, new Delegator(proxyInterfaces, new Object[] { new FooImpl() }));上記の
Delegator
クラスの実装は、わかりやすくするために、最適化されていません。 たとえば、hashCode
、equals
、およびtoString
メソッドのMethod
オブジェクトをキャッシュして比較する代わりに、文字列名で照合することもできます。 これらのメソッド名は、java.lang.Object
内でオーバーロードされないためです。
Copyright © 1999-2004 Sun Microsystems, Inc.All Rights Reserved. |
Java ソフトウェア |