ダイナミック・プロキシ・クラスとは、実行時に指定されたインタフェースのリストを実装するクラスのことです(クラスのインスタンスでのいずれかのインタフェースによるメソッド呼出しがエンコードされ、同じインタフェースを介して別のオブジェクトにディスパッチされる)。このため、インタフェースのリストに対して型保証されたプロキシ・オブジェクトを作成できます。コンパイル時ツールなどでプロキシ・クラスを事前に生成する必要がなくなります。ダイナミック・プロキシ・クラスのインスタンスでのメソッド呼出しは、インスタンスの呼出しハンドラ内の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
配列の2つの要素が同一のClass
オブジェクトを参照することはできない。cl
、各インタフェースがi
の場合は、次の式がtrueでなければならない。
Class.forName(i.getName(), false, cl) == i
interfaces
配列のサイズは65535を超えてはならない。これらの制約に対して違反が発生した場合は、Proxy.getProxyClass
によってIllegalArgumentException
がスローされます。interfaces
配列引数またはそのいずれかの要素がnull
の場合、NullPointerException
がスローされます。
プロキシ・インタフェースは、順番が区別されます。プロキシ・クラスを2回要求したときに、インタフェースの組み合わせが同じで順番が異なる場合は、2つの異なるプロキシ・クラスが作成されます。プロキシ・クラスは、複数のプロキシ・インタフェースが同じ名前とパラメータ・シグネチャを持つメソッドを共有する場合に、決定論的メソッド呼出しエンコードを提供するために、プロキシ・インタフェースの順番によって区別されます。この説明の詳細は、後続の項「複数のプロキシ・インタフェースで重複するメソッド」を参照してください。
同じクラス・ローダーとインタフェースのリストでProxy.getProxyClass
が呼び出されるたびに新しいプロキシ・クラスを生成する必要がないように、ダイナミック・プロキシ・クラスAPIの実装が生成されたプロキシ・クラスのキャッシュを保持するべきです(対応するローダーおよびインタフェース・リストでキー付け)。実装は、クラス・ローダーおよびそのすべてのクラスが適切なときにガベージ・コレクトされるのを妨げる方法で、クラス・ローダー、インタフェース、およびプロキシ・クラスを参照しないように注意するべきです。
プロキシ・クラスには次のプロパティがあります。
"$Proxy"
で始まるクラス名の領域は、プロキシ・クラス用に予約されています。java.lang.reflect.Proxy
を継承します。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
が与えられます。各プロキシ・クラスは、1つの引数(InvocationHandler
インタフェースの実装)を取るpublicコンストラクタを1つ持ちます。
各プロキシ・インスタンスには、呼出しハンドラ・オブジェクト(コンストラクタに渡されたもの)が関連付けられています。プロキシ・インスタンスは、リフレクションAPIを介してpublicコンストラクタにアクセスしなくても、Proxy.newProxyInstance
メソッドを呼び出すことによっても作成できます。このメソッドでは、Proxy.getProxyClass
を呼び出すアクションと、呼出しハンドラを使用してコンストラクタを呼び出すアクションが行われます。Proxy.getProxyClass
の場合と同じ理由で、Proxy.newProxyInstance
はIllegalArgumentException
をスローします。
プロキシ・インスタンスには次のプロパティがあります。
Foo
がプロキシ・インスタンスproxy
およびインタフェースの1つを実装している場合、次の式がtrueを返します。
proxy instanceof Foo
また、次のキャスト操作が成功します(ClassCastException
をスローする場合を除く)。
(Foo) proxy
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
メソッドが、呼出しに使えるプロキシ・インタフェースの1つのメソッドで宣言された例外タイプのどれにも割当てできないチェック例外をスローした場合、チェックされない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
のstatic lookup
メソッドに渡されるとき、返されるObjectStreamClass
インスタンスは次の特性を持ちます。
getSerialVersionUID
メソッドを呼び出すと、0L
が返される。getFields
メソッドを呼び出すと、ゼロ長の配列が返される。String
引数でgetField
メソッドを呼び出すと、null
が返される。オブジェクト直列化用のストリーム・プロトコルは、TC_PROXYCLASSDESC
という名前の型コード(ストリーム・フォーマットの文法のターミナル・シンボル)をサポートします。その型と値は、java.io.ObjectStreamConstants
インタフェースの次の定数フィールドによって定義されます。
final static byte TC_PROXYCLASSDESC = (byte)0x7D;
この文法は、次の2つの規則も含みます(1番目は、元のnewClassDesc規則の代替展開)。
newClassDesc:
TC_PROXYCLASSDESC
newHandle proxyClassDescInfo
proxyClassDescInfo:
(int)<count>
proxyInterfaceName[count] classAnnotation superClassDesc
proxyInterfaceName:
(utf)
ObjectOutputStream
がプロキシ・クラスであるクラス(Class
オブジェクトをProxy.isProxyClass
メソッドに渡すことで判断される)のクラス記述子を直列化するときは、前述の規則に従ってTC_CLASSDESC
のかわりにTC_PROXYCLASSDESC
型コードを使用します。proxyClassDescInfoの展開では、proxyInterfaceName項目のシーケンスは、プロキシ・クラスによって(Class
オブジェクト上でgetInterfaces
メソッドを呼び出すことで返される順番で)実装されるすべてのインタフェースの名前です。classAnnotationおよびsuperClassDesc項目は、classDescInfo規則の場合と同じ意味を持ちます。プロキシ・クラスの場合、superClassDescはスーパー・クラスjava.lang.reflect.Proxy
のクラス記述子です。この記述子は、プロキシ・インスタンスのクラスProxy
の直列化表現を展開できます。
非プロキシ・クラスの場合、ObjectOutputStream
は、サブクラスが特定のクラスのストリームにカスタム・データを書き込めるように、そのprotected annotateClass
メソッドを呼び出します。プロキシ・クラスの場合は、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
は、1つの直列化可能フィールド、h
を持ちます(プロキシ・インスタンスの呼出しハンドラを含みます)。
任意のインタフェース・リストを実装するオブジェクト上でのメソッド呼出しの前後に、メッセージを出力する簡単な例を示します。
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
内でオーバーロードされないため)。