LoginSignup
39
23

More than 5 years have passed since last update.

動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる

Last updated at Posted at 2018-12-20

1. はじめに

Javaの標準ライブラリではjava.lang.reflect.Proxyクラスがあります。このクラスはJ2SE 1.3から追加されたもので、提供する機能は一般的にDynamicProxy(以下、日本語の「動的プロキシ」で表記)と呼ばれています。J2SE 1.3は2000年5月にリリースされたものなので動的プロキシもリリースから長い年月が経過しており、すでに枯れた技術となっていますが、今でも多数のJavaのフレームワークやライブラリが使用している一方、一般的なJavaプログラマの認知度はあまり高くないようです。

本記事では、Javaの基本的な文法を理解している方を対象に、動的プロキシを理解するために一般的なプロキシパターンを紹介して、動的プロキシの必要性を説明した上で、Javaの動的プロキシの使い方を説明します。また、Java以外の言語(C#, Python, EcmaScript)の動的プロキシの実装方法も紹介します。

本記事で掲載するJavaコードは、以下の環境で動作確認をしています。(Java以外の言語の動作環境は、随時記載します)また、掲載するJavaコードはコンパイルに必要な一部のimport句を省略しています。

名称 バージョン
OpenJDK 11.0.1

2. 「静的」なプロキシ(の続き)

動的プロキシである、java.lang.reflect.Proxyの使い方を理解するのはJavaを習得しているプログラマであれば、決して難しくはないでしょう。しかし、動的プロキシの必要性を理解し有効に使用するためには、「動的」の反対語となる「静的」なプロキシを理解する必要があります。これは「プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる」で解説をしました。

proxy_image_small.png

2-1. 静的プロキシのおさらい

プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる」では、共通のインターフェースを具象する複数のクラスについて、ひとつのプロキシで共通処理を定義できる点までを紹介しました。本記事では、そのアンチパターンから説明します。

staticproxy_manyclass_small.png

2-2. 静的プロキシの限界

静的なプロキシの適用には、アンチパターンがあります。

上記で示した「インターフェースに対する複数の具象クラスの共通処理を追加するプロキシ」の、さらに具体的な例を示します。

ある販売サイトのモデル層の実装で、「顧客」「注文」「商品」の3つのクラスを定義します。
このクラスは全てのメソッドの処理でRDBMSにアクセスをするために、メソッドの実行前にはRDBMSの接続を取得してトランザクションを開始する必要があります。そして、実行後にはトランザクションのコミットあるいはロールバックと接続を閉じる処理が必要です。

この3つのクラスはいずれも、データを作成するための「追加する」、データを更新するための「更新する」、データの検索するための「検索する」、詳細なデータを取得するための「詳細取得する」のメソッドがあれば事足りそうでした。ですので、この3つのクラスはModelというインターフェースの具象クラスとします。そして、RDBMS処理に関するプロキシとなるTransactionBarrierというクラスをModelの具象クラスで定義します。

これらのクラスの関係を表したクラス図です。

ecmodel_proxy_class_small.png

上記のクラス設計である程度までは、開発がうまくいっていましたが、ある日問題が起きます。

「顧客」の情報は、顧客情報保護のため一括削除が必要となりました。

そこで「顧客」に「一括削除する」というメソッドを追加しますが、この「一括削除する」というメソッドにもTransactionBarrierによるRDBMSの事前事後処理が必要です。そのため、Modelインターフェースにも「一括削除する」というメソッドを追加します。

すると、「一括削除する」というメソッドが必要ない「注文」と「商品」にも、Modelインターフェースに追加した「一括削除する」を追加しなければならなくなります。しかし、実際には「注文」と「商品」は「一括削除する」は呼び出してほしくないため、「注文」と「商品」は「一括削除する」メソッドには@deprecatedタグをつけた上でこのメソッドが呼ばれたら無条件でUnsupportedOperationExceptionをthrowしなければならなくなりました。

ecmodel_proxy_class_unsupport_small.png

上記の例では、3つのクラスで一つの特殊なメソッド追加が発生しただけなので事態の深刻さが伝わりにくいのですが、実際のシステム開発ではプロジェクトによっては数十から数百のモデルが定義されます。すると、各モデルで独自のメソッドを定義する度に上記のような事例が頻発し、各クラスにはUnsuppportedOperationExceptionをthrowするメソッドが大量に追加する事になります。

このような状況に陥る理由は、本来一つのインターフェースの実装とさせてはいけないクラス群を、プロキシで共通処理を実装したいがために強引に一つのインターフェースを派生したために起こります。

本来は、以下のように「顧客」「注文」「商品」は、別々のインターフェースとプロキシを定義するべきでした。

ideal_ecmode_proxy_small.png

しかし、上記のクラス設計では、プロキシクラスはインタフェース毎に実装をしなければならず、非常に面倒です。
そもそも、上記のようにインターフェースとクラスが1対1の関係であるのに、それぞれに同じ処理を実装するプロキシを定義するのであれば、直接実装クラスに共通処理を実装しても実装の手間は変わらないという点で、プロキシを使用する利点すら失われます。

異なる複数のインターフェースでも、ひとつの実装で定義できるプロキシがあればいいのですが、通常のJavaの文法ではそのような実装をはできません。ですが、動的プロキシを使えば複数のインターフェースを横断して共通のプロキシを実装できます

3. 「動的な」プロキシ

それでは、動的プロキシであるjava.lang.reflect.Proxy(以下、「Proxy」と表記)を使用した実装例を紹介します。

3-1. java.lang.reflect.Proxyクラスの実装例

まずは、Proxyで仲介されるクラスと、そのインターフェースを定義します。このコードは、「プロキシパターン(Proxy Pattern)をできる限りわかりやすく解説してみる」で掲載した、MyInterfaceMyClassの実装と同じです。

MyInterface.java
public interface MyInterface {
    public String myMethod(String value);
}
MyClass.java
public class MyClass implements MyInterface {
    @Override
    public String myMethod(String value) {
        System.out.println("Method:" + value);
        return "Result";
    }
}

Proxyを使用するには、java.lang.reflect.InvocationHandlerインターフェースの具象クラスを定義しなけばなりません。InvocationHandlerの具象クラスではinvoke(Object, Method, Object[])メソッドを実装します。このinvokeメソッドは、具体的なプロキシの処理を実装するメソッドです。

InvocationHandlerの具象クラスの実装例のMyInvocationHandlerのコードです。MyInvocationHandlerのプロキシの処理では、事前処理でMyInvocationHandler:start、事後処理でMyInvocationHandler:exitを標準出力に出力します。

MyInvocationHandler.java
import java.lang.reflect.*;

/** java.lang.reflect.Proxyに指定する、プロキシの処理を実装するInvocationHandler実装クラス */
public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target)  {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("DynamicProxy:before");
        Object result = method.invoke(target, args);
        System.out.println("DynamicProxy:before");

        return result;
    }
}

上で定義したMyInvocationHandlerを利用するには、ProxynewProxyInstanceメソッドを使用してProxyのオブジェクトを取得します。
newProxyInstanceメソッドの第3引数に上で定義したMyInvocationHandlerクラスのオブジェクトを指定します。

    MyClass myClassObj  = new MyClass();
    MyInvocationHandler myInvocationHandler = new MyInvocationHandler(myClassObj);
    MyInterface proxiedObj = (MyInterface)Proxy.newProxyInstance(MyClass.class.getClassLoader(),
        new Class[] { MyInterface.class },
        myInvocationHandler);
    System.out.println(proxiedObj.myMethod("Argument"));

上記のコードを実行した結果です。

実行結果
DynamicProxy:before
Method:Argument
DynamicProxy:after
Result

3-2. ProxyクラスのnewProxyInstanceメソッド

まずは、ProxynewProxyInstanceメソッドの解説します。

newProxyInstanceによりProxyオブジェクトを取得できます。利用者は取得したObject型のオブジェクトを、第2引数で指定したインターフェースのクラス配列中のいずれかの型にキャストして使用します。このメソッドの返却値はObject型ですが、Proxyクラスの派生クラスであり、第二引数のすべてのインターフェースの具象クラスです。

    MyInterface proxiedObj = (MyInterface)Proxy.newProxyInstance(MyClass.class.getClassLoader(),
        new Class[] { MyInterface.class },
        myInvocationHandler);
  • newProxyInstance第1引数にはクラスローダーを指定します。クラスローダーは全てのクラスからgetClassLoader()メソッドで取得できるので、どのクラスのgetClassLoader()で取得しても良いのですが、Webアプリケーションのような多数のクラスローダーチェインを用いた環境で使用する事を想定して、なるべくアプリで独自に実装しているクラスを指定しておいた方が良いです。 ここでは動的プロキシで仲介されるクラスであるMyClassのクラスローダーを指定しています。
  • newProxyInstance第2引数にはインターフェースクラスの配列を指定します。 Javaではひとつのクラスで複数のインターフェースを実装できるので、複数のインターフェースを実装した動的プロキシを生成できるように、配列で指定します。
  • newProxyInstance第3引数にはInvocationHandlerの具象クラスのオブジェクトを指定します。 この引数で指定するオブジェクトが、実際に動的プロキシの内部で実行する処理です。 ここでは実際にInvocationHandlerの具象クラスのMyInvocationHandlerを実装しましたので、そのオブジェクトを指定しています。

3-3. InvocationHandlerのinvokeメソッド

次にMyInvocationHandlerで実装した、InvocationHandlerインターフェースのinvokeメソッドの解説します。

invokeメソッドでプロキシの処理を実装します。Proxyオブジェクトのインターフェースを具象するメソッドが呼ばれると、どのメソッドでも必ずこのinvokeメソッドを呼び出します。invokeメソッドには、Proxyオブジェクトのどのメソッドがどのような引数を指定して呼び出されたかを引数で渡します。

一般的なプロキシを実装するのであればinvokeメソッドの中で、仲介先のオブジェクトのメソッドを呼び出します。呼び出すメソッドはinvokeメソッドの引数で渡されますが、呼び出すオブジェクトはInvocationHandlerの具象クラス自身で保持します。

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  • invoke第1引数には、このinvokeを呼び出したProxyクラスのオブジェクトが渡されます。
  • invoke第2引数には、Proxyオブジェクトで呼ばれたメソッドを表すMethodクラスのオブジェクトが渡されます。invokeの処理の中では、この引数の値を利用して実際のオブジェクトのメソッドを呼び出します。
  • invoke第3引数には、Proxyオブジェクトが呼ばれたメソッドに指定された引数をObject配列で渡されます。 これもまた、この引数の値を利用して実際のオブジェクトのメソッドを呼び出します。

3-4. 動的プロキシとは何が「動的」なのか?

動的プロキシの実装を実行した結果を見れば、「2-2. プロキシクラスを定義する」と同じ実行結果を得られるため、動的プロキシがプロキシの動作しているという点は容易に理解できます。しかし、何故これを「動的」と呼ぶのでしょうか?

動的プロキシの「動的」性を理解するために、ProxynewPorxyInstanceを呼び出すコードを以下のように変えて実行します。MyInvocationHandlerは上で示した実装のままで構いません。

    ArrayList<String> list = new ArrayList<String>();
    MyInvocationHandler myInvocationHandler = new MyInvocationHandler(list);
    @SuppressWarnings("unchecked")
    List<String> proxiedObj = (List<String>)Proxy.newProxyInstance(MyInterface.class.getClassLoader(),
        new Class[] { List.class },
        myInvocationHandler);
    proxiedObj.add("hoge");

上記のコードを実行した結果です。

実行結果
DynamicProxy:before
DynamicProxy:after

上記の実行結果の出力は、proxiedObj.add("hoge");の呼出しにより、MyInvocationHandlerinvokeが呼び出されたことを示します。

ここで注目すべきは、これまではMyInterfaceProxyオブジェクトを取得できていたものが、MyInvocationHandlerを一切変更せずに、Listインターフェースのプロキシも取得できている点です。MyInvocationHandlerのコードにはMyInterfaceListの文字は一切なく、MyInvocationHandlerは実際にこれらのインターフェースには一切の依存をしていません。ProxynewProxyInstanceの第2引数のインターフェースクラスの指定により、Proxyの内部では新しいクラスをnewProxyInstance実行中に生成して、そのProxyオブジェクトを取得します。

動的プロキシと実装したクラスの関係をクラス図で示します。

dynamicproxy_class_small.png

緑色のクラスはJavaのプロセス起動前から用意されたクラスであるのに対し、赤色のクラスはJavaのプロセスが起動してProxynewProxyInstance呼び出されてから作成される(インスタンスではなく)クラスです。newProxyInstanceは(既に同じクラスを生成していればマップから取り出しますが)新しく生成したクラスのインスタンスを返却します。

なんと、newProxyInstanceメソッド中の処理では、ソースコードに存在しないクラスを生成します! 1

プロキシの「静的」「動的」とは、Javaのプロセスが実行されている期間において、事前から定義されていて最後まで不変なプロキシクラスを「静的」、プロセスを実行中の処理により途中から生成されるプロキシクラスを「動的」と呼んでいます。

4. より実践的な動的プロキシの実装

以上で、Proxyの使い方は終了です。

・・・とはいえ、これまで紹介した動的プロキシの実装例では、オブジェクトの利用者はProxynewProxyInstanceを呼び出して対象のオブジェクトを含むProxyオブジェクトを取得します。しかし、この実装はあまり便利とは言えません。

ファクトリパターンを適用した実装

そこで、GoFのデザインパターンにファクトリパターン(Factory Pattern)というのがあるので、このパターンを使用してProxyオブジェクトの取得するコードを簡便にしていきます。ファクトリパターンは、オブジェクトの作成で利用者に直接コンストラクタを呼ばせず、オブジェクトを作成する専用のクラスのメソッドから取得する方法です。

例えば、

ファクトリパターンの例①
    MyInterface myObj = MyFactory.getInstance();

のように、MyFactorygetInstance()メソッドの呼出しで、Proxyオブジェクトが取得できるようになれば、Proxyオブジェクト利用側の実装は大変に簡便になります。

ところが、MyFactorygetInstance()メソッドは呼び出し元から何の情報も渡さないため、このままでは、いつも同じInvocationHandlerを経由して同じクラス(ここではMyClassのオブジェクトを呼び出すProxyオブジェクトしか取得できません。引数でMyInterfaceクラスとMyClassオブジェクトとInvocationHandlerオブジェクトを渡せば、汎用性のあるファクトリにはなります。しかしよく考えてみると、InvocationHandlerは、プロキシが実行する処理の定義なので、ファクトリは内部で固定のInvocationHandlerの具象クラスを使用しても良いでしょう(InvocationHandlerの具象クラスを切り替えるのは、ファクトリクラスやメソッドの分別で可能です)。また、MyClassオブジェクトもMyInterfaceクラスに対する派生クラスがMyClassで固定であれば引数での指定をする必要はありません。

そこで、ここでは、以下のようにファクトリの引数に、インターフェースのみ指定してProxyオブジェクトを取得するファクトリを実装してきます。

ファクトリパターンの例②
    MyInterface myObj = MyFactory.getInstance(MyInterface.class);

上記のようなファクトリを実装すると仮定した場合には、ひとつ問題があります。

この引数にはインターフェースのクラスしか指定が無いので、インターフェースに対する派生クラスをファクトリの内部で取得し、その派生クラスのオブジェクトを作成する必要があります。一番簡単な実現方法は、ファクトリの中でインターフェースに対するクラスのマップを持つ方法です。この方法でも良いですし十分実践的なのですが、インタフェースとクラスの定義が数十件、あるいは数百件となるようなシステムでは、インターフェースとクラスを新たに定義する度に、都度、ファクトリ内のマップを追加する必要があり、少し面倒なのとグループ開発ではソースの競合が発生し得ます。

そこで、インターフェースの付加情報で派生クラスを指定する方法をここでは紹介します。最初に、以下のような、アノテーションを定義します。

MyAnnotation.java
/** インターフェースに対する実装クラスを指定するアノテーション */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    Class<?> concretizedClass();
}

このアノテーションを、先ほど定義したMyInterfaceに追加します。
アノテーションの引数で、ファクトリ内で選択する派生クラスを指定します。

MyInterface.java
/** アノテーションでインスタンス化する実装クラス定義を追加 */
@MyAnnotation(concretizedClass = MyClass.class)
public interface MyInterface {
    public String myMethod(String value);
}

それではようやく、ファクトリクラスとなるMyFactoryの実装です。InvocationHandlerの具象クラスはMyFactorygetInstanceメソッド内で、匿名クラスで定義しています。

MyFactory.java
/** 引数で指定したインターフェースのオブジェクトを作成するクラス */
public class MyFactory {
    /** 引数interfaceClassで指定したインターフェースの実装クラスのオブジェクトを取得するメソッド */
    public static <I> I getInstance(Class<I> interfaceClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException  {
        // MyInterfaceに対する実装クラスを、MyInterfaceのアノテーションから取得
        MyAnnotation myAnnotation = interfaceClass.getAnnotation(MyAnnotation.class);
        Class<?> objClass = myAnnotation.concretizedClass();

        // 実装クラスのオブジェクトを作成
        @SuppressWarnings("unchecked")
        I obj = (I)objClass.getDeclaredConstructor().newInstance();

        // InvocationHandlerを匿名クラスで定義してオブジェクトを作成
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("DynamicProxy:before");
                Object result = method.invoke(obj, args);
                System.out.println("DynamicProxy:after");
                return result;
            }
        };

        // プロキシオブジェクトを作成
        @SuppressWarnings("unchecked")
        I proxiedObj = (I)Proxy.newProxyInstance(interfaceClass.getClassLoader(),
            new Class[] { interfaceClass },
            invocationHandler);
        return proxiedObj;
    }
}

上記のMyFactory利用側の実装と実行結果です。

    MyInterface myObj = MyFactory.getInstance(MyInterface.class);
    System.out.println(myObj.myMethod("Argument"));
実行結果
DynamicProxy:before
Method:Argument
DynamicProxy:after
Result

さらに実践的なコードとするために

上記の改善で、実際に「使える」コードに近づきましたが、まだ改善の余地や、修正すべき問題点が残っています。

以下に列挙するので、実際に動的プロキシを適用する際の参考にしてください。

  • MyFactorygetInstanceメソッドは、内部で発生しうるNoSuchMethodExceptionをそのままthrowしている。実際に発生しえない例外はthrows句に含めないようにするべき
  • 引数で指定したinterfaceClassに対して、アノテーションで指定したobjClassinterfaceClassの具象クラスであるチェックをするべき
  • interfaceClassに対するobjClassの指定をインターフェースのアノテーションのみではなく、MyFactoryに直接指定できるようにするか、設定ファイルから設定できるようにするオプションを設けると便利
  • interfaceClassはメソッドの引数で指定するより、ジェネリックで指定する方がカッコいい
  • Object result = method.invoke(obj, args);で発生する例外をそのままthrowするか、getCause()で原因となったThrowableオブジェクトをthrowするかを決めるべき
  • getInstance()で毎回新しいProxyオブジェクトを生成しているが、Sigletoneで良いオブジェクトであれば都度生成しないようにする
  • Proxyの利用側は、直接MyFactoryを使用するのではなくDI(Devedency Injection)によりオブジェクトを取得できるようにする

なお、このようなインターフェースに対する派生クラスを決定して適切なオブジェクトを返却するパターンを、ファクトリパターンとは別にServiceLocatorパターンと呼びます。

5. 他の言語での動的プロキシ

これまで、Javaによる動的プロキシの紹介をしてきました。
他のプログラム言語では、動的プロキシはどのように提供されているかを本章では紹介します。

5-1.C#

(cfm-art様のご指摘により、標準のRealProxyで動的プロキシ機能を提供しているとのことです。改めて本章は追記いたします)

C#の標準ライブラリである.NET Frameworkには、動的プロキシに相当するクラスはありません。
しかし、外部プロジェクトのCastleにより外部ライブラリで動的プロキシの機能が提供されています。

名称 バージョン
.NET Framework 4.6.1
Castle.DynamicProxy2 2.2.0
DynamicProxyTest.cs
using Castle.Core.Interceptor;
using Castle.DynamicProxy;

namespace MyNamespace
{
    public interface IMyInterface
    {
        string MyMethod(int value);
    }

    public class MyClass : IMyInterface
    {
        public string MyMethod(int value)
        {
            Console.WriteLine("MyMethod:" + value);
            return "Result";
        }
    }

    public class MyInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            Console.WriteLine("MyInterceptor:before");
            object ret = invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments);
            Console.WriteLine("MyInterceptor:after");
            invocation.ReturnValue = ret;
        }
    }
}
    ProxyGenerator generator = new ProxyGenerator();

    IMyInterface proxy = generator.CreateInterfaceProxyWithTargetInterface(
        typeof(IMyInterface),
        new MyClass(),
        new MyInterceptor()) as IMyInterface;

    Console.WriteLine(proxy.MyMethod(1));
実行結果
MyInterceptor:before
MyMethod:1
MyInterceptor:after
Result

ProxyGeneratorで定義しているメソッドを見るとわかりますが、JavaのProxyより提供している機能は豊富です。Javaでは、Proxyクラスはインターフェースの具象クラスでのみ作成できますが、C#のProxyGeneratorではインターフェースに加えてクラスの派生クラスの動的プロキシも作成できます。

5-2.Python

Pythonも動的プロキシに相当するクラスはありません。
しかし、標準の特殊メソッドで動的プロキシ相当の機能を実現できます。

名称 バージョン
Python 3.7.1
dynamicProxyTest.py
class MyProxy(object):
    def __init__(self, obj):
        self._obj = obj

    def __getattr__(self, name):
        def func(*args):
            print(before)
            result = getattr(self._obj, name)(*args)
            print('after')
            return result
        return func

class MyClass(object):
    def myFunction(self, arg):
        print('myFunction ' + arg)
        return 'result'

myObj = MyClass()
myProxy = MyProxy(myObj)
print(myProxy.myFunction('arg'))
実行結果
before
myFunction arg
after
result

このMyProxyクラスは、Pythonの文法を知らない方のために、簡単に解説をします。

Pythonではクラス定義に対して、前後に"__"が付く特定の名称のメソッドの特殊メソッド(Special Method)があります。特殊メソッドは、オブジェクトの作成や属性の呼出し、あるいは特定の演算子の値で指定された場合などの、オブジェクトで発生した特定のイベントにより呼び出されます。

__getattr__メソッドは、そのクラスのオブジェクトで定義されていない属性(Attribute)を取得しようとしたときに呼び出されます。オブジェクトの利用側は、そのクラスに指定した名前の属性が定義されていなかった場合は、__getattr__の戻り値を属性値と取得します。

Pythonでは、クラス上の変数もメソッドも全て属性です。MyProxyクラスは__init__メソッドと__getattr__メソッドの二つの特殊メソッドしか定義していないため、このクラスのオブジェクトから属性を取得しようとする(つまり、何かしらのメソッドを呼び出そうとする)と、必ず__getattr__メソッドが呼び出されます。そこで、MyProxyが動的プロキシと同じ動作をするために、__getattr__メソッドの内部ではfuncという、プロキシの役割をする関数を定義してその関数を返却します。

呼出し元の実装は1行で、

myProxy.myFunction('hoge')

となっていますが、これはJavaのメソッド呼出しとは異なり、

  1. myProxyオブジェクトからmyFunctionという名称の属性を取得する。
  2. 1.で取得した属性の値を('hoge')で実行する。

と解釈する必要があります。myProxyオブジェクトからmyFunctionという名称の属性を取得しようとしたけれども、myProxyにはmyFunctionという名称の属性が無かったため、__getattr__メソッドによる戻り値を取得して実行します。

5-3.EcmaScript(JavaScript)

EcmaScriptでは6からProxyというクラスを提供してます。しかし、JavaのProxyとは使い方が違い、オブジェクトのイベントハンドラーの役割を担います。

ここでは、Proxyの中のイベントハンドラーの一つであるapplyという関数を紹介します。apply関数は、関数を呼出した際に呼出されるイベントハンドラーです。

名称 バージョン
node.js v6.11.3
dynamicProxyTest.js
function myFunction(arg1, arg2) {
    console.log("myFunction:" + arg1 + ", " + arg2);
    return "result";
}

const handler = {
    apply : function (target, thisArg, argumentsList) {
        console.log("before");
        var result = target(...argumentsList);
        console.log("after");
        return result;
    }
}

let proxy = new Proxy(myFunction, handler);
console.log(proxy("hoge", "hage"));
実行結果
before
myFunction:hoge, hage
after
result

6. 動的プロキシのアンチパターン

ここまで読んでいただければ、動的プロキシによる実装ができるようになったと思います。しかし、動的プロキシは自由度の高さと機能の特殊さ故に、使い方を誤ると機能的に要件は満たしていても、機能以外の問題を内包させる可能性がある技術でもあります。「5.他の言語での動的プロキシ」で紹介した言語は、本記事の執筆時点(2018年12月)ではいずれも広く使用されているメジャーなプログラム言語ですが、いずれも標準ライブラリでは動的プロキシを提供していない点を注目すべきです。極論を述べると、動的プロキシの使用そのものがアンチパターンであると言えます。 2

ここで、動的プロキシの特徴をまとめると、以下となります。

  1. 動的プロセス実行時に、存在しないソースコードのクラスを生成する。
  2. 動的プロキシの処理と、プロキシで仲介される処理を完全に分離する。
  3. 動的プロキシは仲介するインターフェースやクラスに依存せずに仲介をできる。

この特徴のなかで「2. 動的プロキシの処理と、プロキシで仲介される処理を完全に分離する」は、動的プロキシの利点でもあり欠点でもあります。動的プロキシで仲介されるオブジェクトを実装するクラスの開発者は、実際にアプリケーションが動作する環境では自分が作ったクラスが動的プロキシの仲介で呼び出されている事実を知らない可能性があります。しかし、そのクラスを動作させるには動的プロキシの仲介が必須であるとすると、自分が作ったクラスを別の環境で再利用したり試験のために独自の環境で動かすためには、プロキシ内部の処理を再現せねばならず難易度が大きくなります。また、エラーや障害の解決が困難になるかもしれません。

動的プロキシの最も簡単な代替策は、以下のようにラムダ式を使ってプロキシを定義する方法です。

    MyInterface myObj = new MyClass();
    MyProxy.invoke(() -> { nyObj.myMethod(); });

MyProxyinvokeメソッドでは、プロキシの処理を実装して、引数で渡されたRunnableオブジェクトを呼び出して、仲介した本来の処理を呼び出せます。この方法でも、インターフェースやクラスを横断したプロキシを実装できます。しかも、開発者はMyProxyの実装を見れば、自分が定義したクラスがどのような仲介処理によって呼び出されるかがわかります。

以上を踏まえて、動的プロキシの適用法のアンチパターンと思われる使用例を挙げていきます。しかし、これらのパターンは必ずしも使ってはいけないパターンではありません。本当に動的プロキシの適用が最適であるか?他により有効な代替案はないか?を検討した方が良いという意味でのアンチパターンです。プロキシの本来の意味は代理人です。そこで、以下に「〇〇の代理人」という題でアンチパターンを紹介してきます。

6-1. 片隅で仕事をする代理人

例えば、ある巨大なWebシステムの中にある、ユーザなどの限定した情報を扱うために定義したクラスの中の、情報を検索するメソッドの中の、入力値をチェックする処理の中で、動的プロキシを適用するのはあまり好ましくは無いでしょう。仮に入力値の項目が大量にあるために処理の正規化と共通化が必要であっても、大量の異なる処理の一部を共通化する方法は動的プロキシ以外にもあります。

動的プロキシは、処理を完全に分離してしまうが故に、処理の全体像をコードから読み取りづらくなる副作用があります。狭い領域の処理は規模が大きくなっても、全体を把握できるような実装をするのが望ましいのではないでしょうか。

6-2. 秘密主義の代理人

Proxyが使用するInvocationHandlerの具象クラスの中身が公開されていないにもかかわらず、開発者はその動的プロキシの処理を経由しないと実行できないクラスの実装をさせられたとします。自分が開発する実装の前提条件が秘密にされているのは、開発者にとってストレスとなるでしょう。もちろん、心理的な問題だけではなく、動的プロキシの処理が原因の問題がシステムに発生した場合、開発者はその調査に多くの工数をかける可能性があります。また、実装した処理の再利用が困難になるかもしれません。

6-3. 仕事を抱えすぎる代理人

動的プロキシの責務が曖昧になるケースです。当初は動的プロキシにログの出力やDB関連の初期化終期化の処理のようなシステムの業務以外の処理のみが実装されていたものが、HTTPリクエストボディのパース処理が入り、事前のチェック処理が入り、承認処理が入り・・・と動的プロキシの責務が増えてしまうケースです。

これは、動的プロキシに限った話ではありませんが、クラスの責務は明確にするべきです。また、修正をする際もそのポリシーは守り続けるべきです。必要であれば責務の異なる動的プロキシを多重に経由するべきです。

6-4. 代理人の氾濫

システムをコントローラ層、サービス層、ロジック層、データアクセス層のように多層構造とした場合や、オブジェクト同士が複雑に依存しているシステムで、全てのオブジェクトが代理人を仲介しているケースです。このような状況では、処理の上で必要なプロキシもあるのでしょうが、不要なプロキシも多数あると想像できます。プロキシの仲介は必要なものだけにするべきで、それ以外のプロキシの仲介が本当に必要なのかは慎重に検討するべきです。

6-5. 代理人もどき

InvocationHandlerの具象クラスの実装例を読んで気づいた方は多いと思いますが、Proxyはプロキシパターンの実装を強制していません。InvocationHandlerinvokeのメソッドの中で、引数で渡されたメソッドを実際に呼出すかどうかは、完全にInvocationHandlerの具象クラスを実装する側の任意に委ねられています。したがって、Proxyをプロキシパターン以外にも応用できます。

例えば、DTO(Data Transfer Object)はクラスメンバとそれに対応するsetメソッドとgetメソッドを定義するパターンです。これは、DTOの定義はsetメソッドとgetメソッドの宣言をするインターフェースのみで、内部の実装をInvocationHadlerで定義します。

MyDtoInvocationHadler.java
public class MyDtoInvocationHandler implements InvocationHandler {
    private Map<String, Object> map = new HashMap<String, Object>();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // getで始まるメソッドの処理
        if (method.getName().startsWith("get")) {
            return map.get(method.getName().substring(3));
        }
        // setで始まるメソッドの処理
        else if (method.getName().startsWith("set")) {
            // 省略
        }
    }
}

この適用が本当に有用であるかは疑問があります。DTOクラスの実装を省略できても、インターフェースの定義は必要で、そのインターフェースには各項目のsetメソッドとgetメソッドを定義しなければなりません。大抵の開発プロジェクトでは、DTOクラスやDBテーブルに対応するEntityクラスの実装は手書きではなく、設計書などから自動でコードを出力するツールを持っているでしょう。それであれば、出力するコードがクラスであるかインターフェースであるかはあまり問題にはなりません。逆に、特定のDTOに単純なクラスメンバのやり取り以外の実装を組み込む選択肢を開発者から奪うことになります。

6-6. 注文の多い代理人

料理店ではありません。代理人を仲介させるのに、インターフェースのアノテーションや設定ファイルに大量の情報を必要とする代理人です。「6-5. 代理人もどき」のようにProxyにプロキシ以外の処理を行わせようとする場合も起こりがちです。大量の情報を代理人の利用者へ注文をするのであれば、それは代理人ではなく利用者側で独自に実装すればよいのではないでしょうか?

7. さいごに

本記事の動的プロキシの解説は以上となります。より詳しい解説は後述の参考サイトを読んでください。

動的プロキシの理解は決して難しくはありませんが、「1. はじめに」で述べた通り、私の個人的な経験では動的プロキシの一般的なJavaプログラマの認知度は決して高くない状況と感じております。これは、以下の理由によるものと推測しています。

  • 初心者向けの文献では動的プロキシを紹介していない。
  • 「プロキシ」という名称がサーバサイドの「プロキシ」と混同しやすい。
  • 一般的なJavaの開発において動的プロキシが必要な局面は僅少である。(そもそも、動的プロキシでしか実現できない機能はない)

しかし、幾つかのJavaでの開発プロジェクトに携わった経験のあるプログラマの方であれば、自分の実装したコードをフレームワーク上で実行すると、スタックトレースに見覚えのないクラスの呼出しが表れているのを見て、不思議に感じた経験は無いでしょうか?そのような疑問を解決するには、動的プロキシを理解しておいた方が良いでしょう。また、Javaから他のプログラミング言語を学習する際には、Javaの動的プロキシの知識は無駄ではないと思います。

一方で、Proxyを含むJ2SE 1.3のリリースが2000年5月であったのに対し、2000年9月にはマーティン・ファウラー氏がPOJO(Plane Old Java Object)を提唱したのは無関係ではいのでしょうが、本記事では準備不足のため扱えませんでした。

本記事を読んでいただいた方にとって、動的プロキシの知識を得るきっかけとなれば幸いです。

参考サイト

https://docs.oracle.com/javase/jp/10/docs/api/java/lang/reflect/Proxy.html
https://docs.oracle.com/javase/jp/8/docs/technotes/guides/reflection/proxy.html
https://www.ibm.com/developerworks/jp/java/library/j-jtp08305/


  1. 厳密には、上記の参考サイトに示したJava公式ドキュメントではプロキシ・クラスのコードは、信頼されたシステム・コードによって生成されるためですとだけあり、newProxyInstnceメソッド内で新しいクラスを生成するとは明記していません。実際はVMの実装に依存しますし、Pythonのようなアプローチで動的プロキシを実現している可能性もあります。しかし、ProxyクラスはnewProxyInstnceメソッドの引数でインターフェースクラスが指定されるまでは、どのような動的プロキシクラスが必要かはわからないため、newProxyInstnceメソッド内で新しいクラスを生成すると解釈しています。 

  2. これは極論ですが、動的プロキシの使用は常にアンチパターンに陥る可能性を疑うくらい、慎重に検討をするべきです。 

39
23
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
23