ダイナミックプロキシクラスとは、実行時に指定されたインタフェースのリストを実装するクラスのことです (クラスのインスタンスでのいずれかのインタフェースによるメソッド呼び出しがエンコードされ、同じインタフェースを介して別のオブジェクトにディスパッチされる)。このため、インタフェースのリストに対して型保証されたプロキシオブジェクトを作成できます。コンパイル時ツールなどでプロキシクラスを事前に生成する必要がなくなります。ダイナミックプロキシクラスのインスタンスでのメソッド呼び出しは、インスタンスの呼び出しハンドラ内の 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
内でオーバーロードされないため)。