|
ダイナミックプロキシクラス |
ドキュメントの目次 |
はじめに
ダイナミックプロキシの 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 FooClassCastExceptionはスローされません。(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_PROXYCLASSDESCnewHandle 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 ソフトウェア |