ダイナミック・プロキシ・クラス

目次

はじめに
ダイナミック・プロキシAPI
直列化

はじめに

ダイナミック・プロキシ・クラスとは、実行時に指定されたインタフェースのリストを実装するクラスのことです(クラスのインスタンスでのいずれかのインタフェースによるメソッド呼出しがエンコードされ、同じインタフェースを介して別のオブジェクトにディスパッチされる)。このため、インタフェースのリストに対して型保証されたプロキシ・オブジェクトを作成できます。コンパイル時ツールなどでプロキシ・クラスを事前に生成する必要がなくなります。ダイナミック・プロキシ・クラスのインスタンスでのメソッド呼出しは、インスタンスの呼出しハンドラ内の1つのメソッドにディスパッチされ、呼び出されたメソッドを識別するjava.lang.reflect.Methodオブジェクト、および引数を含むObject型の配列を使用してエンコードされます。

ダイナミック・プロキシ・クラスは、インタフェースAPIを提示するオブジェクト上での呼出しの、型保証されたリフレクション・ベース・ディスパッチを提供する必要があるアプリケーションまたはライブラリに役立ちます。たとえば、アプリケーションはダイナミック・プロキシ・クラスを使用することで、複数の任意のイベント・リスナー・インタフェース(java.util.EventListenerを継承するインタフェース)を実装するオブジェクトを作成し、さまざまな種類のさまざまなイベントを同じ方式で処理できます(そのようなイベントのすべてのログをファイルに記録するなど)。

ダイナミック・プロキシ・クラスのAPI

ダイナミック・プロキシ・クラス (以下「プロキシ・クラス」)とは、クラスが作成されるときに、実行時に指定されたインタフェースのリストを実装するクラスのことです。

プロキシ・インタフェースは、プロキシ・クラスが実装するインタフェースです。

プロキシ・インスタンスは、プロキシ・クラスのインスタンスです。

プロキシ・クラスの作成

プロキシ・クラスとそれらのインスタンスは、java.lang.reflect.Proxyクラスのstaticメソッドを使用して作成されます。

Proxy.getProxyClassメソッドは、クラス・ローダーおよびインタフェースの配列が渡されると、プロキシ・クラスのjava.lang.Classオブジェクトを返します。プロキシ・クラスは、指定されたクラス・ローダー内に定義されており、指定されたすべてのインタフェースを実装します。同じ組み合わせのインタフェースのためのプロキシ・クラスがクラス・ローダー内にすでに定義されている場合は、既存のプロキシ・クラスが返されます。そうでない場合は、それらのインタフェースのためのプロキシ・クラスがクラス・ローダー内に動的に生成され、定義されます。

Proxy.getProxyClassに引き渡されるパラメータには、いくつかの制約があります。

これらの制約に対して違反が発生した場合は、Proxy.getProxyClassによってIllegalArgumentExceptionがスローされます。interfaces配列引数またはそのいずれかの要素がnullの場合、NullPointerExceptionがスローされます。

プロキシ・インタフェースは、順番が区別されます。プロキシ・クラスを2回要求したときに、インタフェースの組み合わせが同じで順番が異なる場合は、2つの異なるプロキシ・クラスが作成されます。プロキシ・クラスは、複数のプロキシ・インタフェースが同じ名前とパラメータ・シグネチャを持つメソッドを共有する場合に、決定論的メソッド呼出しエンコードを提供するために、プロキシ・インタフェースの順番によって区別されます。この説明の詳細は、後続の項「複数のプロキシ・インタフェースで重複するメソッド」を参照してください。

同じクラス・ローダーとインタフェースのリストでProxy.getProxyClassが呼び出されるたびに新しいプロキシ・クラスを生成する必要がないように、ダイナミック・プロキシ・クラスAPIの実装が生成されたプロキシ・クラスのキャッシュを保持するべきです(対応するローダーおよびインタフェース・リストでキー付け)。実装は、クラス・ローダーおよびそのすべてのクラスが適切なときにガベージ・コレクトされるのを妨げる方法で、クラス・ローダー、インタフェース、およびプロキシ・クラスを参照しないように注意するべきです。

プロキシ・クラスの特性

プロキシ・クラスには次のプロパティがあります。

プロキシ・インスタンスの作成

各プロキシ・クラスは、1つの引数(InvocationHandlerインタフェースの実装)を取るpublicコンストラクタを1つ持ちます。

各プロキシ・インスタンスには、呼出しハンドラ・オブジェクト(コンストラクタに渡されたもの)が関連付けられています。プロキシ・インスタンスは、リフレクションAPIを介してpublicコンストラクタにアクセスしなくても、Proxy.newProxyInstanceメソッドを呼び出すことによっても作成できます。このメソッドでは、Proxy.getProxyClassを呼び出すアクションと、呼出しハンドラを使用してコンストラクタを呼び出すアクションが行われます。Proxy.getProxyClassの場合と同じ理由で、Proxy.newProxyInstanceIllegalArgumentExceptionをスローします。

プロキシ・インスタンスの特性

プロキシ・インスタンスには次のプロパティがあります。

複数のプロキシ・インタフェースで重複するメソッド

複数のインタフェースに、同じ名前とパラメータ・シグニチャを持つメソッドが含まれる場合は、プロキシ・クラスのインタフェースの順番が区別されます。プロキシ・インスタンス上で重複するメソッドが呼び出された場合、呼出しハンドラに渡されるMethodオブジェクトで、プロキシ・メソッドの呼出しに使用されたインタフェースの参照型から宣言クラスを割り当てることができないことがあります。このような制約が存在するのは、生成されたプロキシ・クラス内の対応するメソッドの実装から、その実装が呼び出されたときに使用されたインタフェースを特定できないためです。このため、プロキシ・インスタンス上で重複するメソッドが呼び出された場合は、メソッド呼出しに使用された参照型にかかわりなく、プロキシ・クラスのインタフェース・リストでそのメソッド(直接またはスーパー・インタフェースから継承)を含むインタフェースのうち、最初のインタフェースのメソッドのMethodオブジェクトが呼出しハンドラのinvokeメソッドに渡されます。

プロキシ・インタフェースに、java.lang.ObjecthashCodeequals、またはtoStringメソッドと同じ名前およびパラメータ・シグニチャを持つメソッドが含まれる場合は、プロキシ・インスタンス上でそのメソッドが呼び出されると、呼出しハンドラに渡されるMethodオブジェクトの宣言クラスはjava.lang.Objectになります。つまり、publicで非finalであるjava.lang.Objectのメソッドは、呼出しハンドラに渡すMethodオブジェクトを決定するときに、論理的にほかのプロキシ・インタフェースより優先されます。

重複するメソッドが呼出しハンドラにディスパッチされた場合は、invokeメソッドからスローできるチェック例外の型は、チェックされる型のうち、呼び出されるすべてのプロキシ・インタフェースのメソッドに指定されている、throws句の例外の型に割り当てることができるものに限定されます。invokeメソッドが、呼出しに使えるプロキシ・インタフェースの1つのメソッドで宣言された例外タイプのどれにも割当てできないチェック例外をスローした場合、チェックされないUndeclaredThrowableExceptionがプロキシ・インスタンスでの呼出しによってスローされます。つまり、invokeメソッドに渡されたMethodオブジェクト上で、getExceptionTypesを呼び出して例外の型を取得しても、invokeメソッドから正常にスローされないことがあります。

直列化

java.lang.reflect.Proxyjava.io.Serializableを実装するので、この項で説明するように、プロキシ・インスタンスを直列化できます。ただし、プロキシ・インスタンスにjava.io.Serializableに代入できない呼出しハンドラが含まれている場合は、そのようなインスタンスがjava.io.ObjectOutputStreamに書き込まれるとjava.io.NotSerializableExceptionがスローされます。プロキシ・クラスの場合、java.io.Externalizableを実装することは、直列化の観点ではjava.io.Serializableを実装することと同じ効果を持ちます。ExternalizableインタフェースのwriteExternalおよびreadExternalメソッドが、直列化処理の一部としてプロキシ・インスタンス(または呼出しハンドラ)上で呼び出されることはありません。プロキシ・クラスのClassオブジェクトは、すべてのClassオブジェクトと同様に常に直列化可能です。

プロキシ・クラスは、直列化可能フィールドおよび0LserialVersionUIDを持ちません。つまり、プロキシ・クラスのClassオブジェクトがjava.io.ObjectStreamClassのstatic lookupメソッドに渡されるとき、返されるObjectStreamClassインスタンスは次の特性を持ちます。

オブジェクト直列化用のストリーム・プロトコルは、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.getProxyClassIllegalArgumentExceptionをスローすると、resolveClassIllegalArgumentExceptionを含む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をオーバーライドしてほかのオブジェクトに直接委譲されないようにプロキシ・メソッド呼出しの動作を実装でき、proxyHashCodeproxyEquals、および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クラスの実装は、最適化することよりわかりやすくすることを意図しています。たとえば、hashCodeequals、およびtoStringメソッドのためにMethodオブジェクトをキャッシュして比較する代わりに、文字列名でそれらを照合できます(これらのメソッド名のいずれもjava.lang.Object内でオーバーロードされないため)。


Copyright © 1993, 2020, Oracle and/or its affiliates. All rights reserved.