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