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

目次

はじめに
ダイナミックプロキシ 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, 2013, Oracle and/or its affiliates. All rights reserved.