2016年6月24日金曜日

DispatchProxy

.NET Core には、System.Reflection.DispatchProxy という型が追加されています。
こいつが何かというと、インターフェイスベースの AOP を実現するプロキシを生成するための仕組みです(Java の Proxy に近いといえばわかる人もいるでしょうか)。
DispatchProxy を使用して特定のインターフェイスのプロキシを作成すると、返されたオブジェクトに対するメソッド呼び出しに対する割り込みがかけられるようになります。

この説明でピンと来る人はいないでしょうから、もう少し丁寧に説明していきます。

使い方

まず、割り込み処理を実行するプロキシクラスを作ります。これは、System.Reflection.DispatchProxy 抽象クラスを継承したクラスです。
基底型である DispatchProxy クラスには、以下のような抽象メソッドが定義されています。

protected abstract object Invoke(MethodInfo targetMethod, object[] args);

どう見ても、メソッド呼び出しに割り込んで、処理を実装しろと言っていますね。この中で、

  • 割り込み処理を行う
  • 本来のディスパッチ先のメソッドを呼び出す
  • メソッド呼び出しの戻り値を返す(targetMethod の戻り値型が void なら null で OK)

などをすればOKです。
後は、やりたいことに応じて、このプロキシにプロパティやメソッドを生やしてください。
ただし、このプロキシ型はpublicでなければならず、さらにpublicなデフォルトコンストラクターが必要です(後述しますが、つらい)。
そして、DispatchProxy.Create<T, TProxy>() 静的メソッドで、プロキシ型のインスタンスを作成します。このとき、任意のインターフェイス型と、先ほど作成したプロキシ型を指定します。

IMyModel obj = DispatchProxy.Create<IMyModel, MyAopProxy>();

この戻り値のobjのメンバーを呼び出すと、MyAopProxyInvoke(MethodInfo,object[])が呼び出されます。やった。

さっそく例を見てみましょう。
昔から、AOPと言えばトランザクション管理とログ出力と相場が決まっています。
たとえば、IMyModelというインターフェイスと、実装であるMyModelがあり、呼び出しのトレースを出力する TracingAopProxy を作るとしましょう。
まず、プロキシクラスを以下のように記述します。追加のプロパティはAPからは呼んでほしくないので、internalにしてます。

public sealed class TracingAopProxy : DispatchProxy
{
    // トレース出力先
    private TraceSource _trace;
    // 実際の処理を行うオブジェクト。
    private object _model;
    // インターフェイスメソッドと
    private Dictionary<MethodInfo, MethodInfo> _dispatchTable;

    public TracingAopProxy() { }

    // コンストラクターが引数を宣言できないので、初期化メソッドを用意。
    internal void Initialize(Type interfaceType, object model, string sourceName)
    {
        _model = model;
        _trace = new TraceSource(sourceName);
        _dispatchTable = new Dictionary<MethodInfo, MethodInfo>(new MethodInfoEqualityComparer());
        foreach (var method in interfaceType.GetRuntimeMethods())
        {
            // .NET Core には GetInterfaceMap() が実装されていないので、頑張って比較。
            // 厳密にやるなら、ReturnType と CallingConvention も。
            // インターフェイスの明示的な実装とかあるので、public メソッドかどうかは問わない。
            var target = model.GetType().GetRuntimeMethods().Single( m =>
                             m.Name == method.Name
                             && m.GetGenericArguments().SequenceEqual(method.GetGenericArguments())
                             && m.GetParameters().Select(p => p.ParameterType).SequenceEqual(method.GetParameters().Select(p => p.ParameterType))
                         );
            _dispatchTable.Add(method, target);
        }
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        _trace.TraceEvent(TraceEventType.Verbose, "Enter: {0}", targetMethod);
        try
        {
            var result = _dispatchTable[targetMethod].Invoke(_model, args);
            _trace.TraceEvent(TraceEventType.Verbose, "Success: {0}", targetMethod);
            return result;
        }
        catch(TargetInvocationException ex)
        {
            _trace.TraceEvent(TraceEventType.Error, "Error: {0}", ex.InnerException);
            throw;
        }
    }

    // .NET Core では MethodInfo.Equals が参照比較で、うまく比較できないことがあるので必要
    private class MethodInfoEqualityComparer : EqualityComparer<MethodInfo>
    {
         public MethodInfoEqualityComparer() { }

         public override bool Equals(MethodInfo left, MethodInfo right)
         {
              return left?.MetadataToken == right?.MetadataToken;
         }

         public override int GetHashCode(MethodInfo obj)
         {
              return (obj?.MetadataToken.GetHashCode()).GetValueOrDefault();
         }
    }
}

で、以下のようなラッパーメソッドを作ってあげればOKです。

public static T WithTrace<T, TImpl>(TImpl model)
    where TImpl : T
{
    var proxy = DispatchProxy.Create<T, TracingProxy>();
    ((TracingProxy)(object)proxy).Initialize(typeof(T), model, "the.source");
    return proxy;
}

ここでのポイントは、DispatchProxy.Create<T, TProxy>() の戻り値の型は T 型ですが、同時に TProxy の派生クラスでもあるということです。
そのため、TProxy 型にキャストすることで、追加の初期化処理を呼び出すことができます(上記の例では、C# コンパイラーをごまかすために、いったんobjectにキャストしています。理由は本筋ではないので割愛します)。

内部動作の説明

DispatchProxy.Create<T, TProxy>() は、ざっくりいうと以下のような処理をやってくれます(他にも細かいことをいろいろやっていますので、興味のある方はソースを見るといいでしょう)。
1. TProxy クラスを継承したプロキシクラスを動的に生成します。
2. 動的に生成したクラスに、指定したインターフェイス(T型)を実装させます。
3. 動的に生成したクラスに、インターフェイスが宣言しているすべてのメソッドを宣言します。
4. 現在のメソッド情報をMethodInfoとして取得するILを出力します。
5. 実引数を格納するobject[]をインスタンス化するILを出力します。
6. 実引数をobject[]につめるILを出力します。
7. Invoke(MethodInfo, object[])を呼び出すILを出力します。
8. メソッドが戻り値を持つ場合、Invoke(MethodInfo, object[])の戻り値をキャストして戻します。

注意点

  • プロキシ型はpublicである必要があります。ライブラリやフレームワークを作るお仕事の人は「まじかよ……」と思ったかと思いますが、そう思った方は我慢せずにIssueを投げてみるといいと思います(自分は、後述するように他にもっと気になったことがあったので投げてないです)
  • 巷のよくある AOP の仕組みと同様に、インスタンスメソッドだけに割り込めます。
  • プロキシ型のInvoke(MethodInfo, object[])の実装によりますが、上記の例のように素直に実装してしまうと、呼び出し先で発生した例外がTargetInvocationExceptionになったりして透過的な割り込みではなくなります。気を付けましょう。式木か何かでMethodInfo.Invokeせずに呼び出すとか。
  • DispatchProxy.Create() で返されたオブジェクトに対してリフレクションを実行した場合、なぜかインターフェイスが実装しているはずのプロパティとイベントの情報が取れません。
    たとえば、
public interface IFoo
{
    string Bar { get; set; }
}

というインターフェイスに対して、

IFoo foo = DispatchProxy.Create<IFoo, TracingAopProxy());

とすると、

foo.Bar = "A";

というコードは(もちろん)コンパイルするし動作するにもかかわらず、

foo.GetType().GetRuntimeProperty("Bar")

nullを返します。なので、このオブジェクトをリフレクションベースのツールやライブラリに渡すと、うまく動作しないかもしれません。
対処方は今のところありません(次回に続く)

0 件のコメント:

コメントを投稿