.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
のメンバーを呼び出すと、MyAopProxy
のInvoke(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 件のコメント:
コメントを投稿