IL EmitによるAOP (4)
s2dotnetでのAOPを実現する機能の概略に関してもう一度書いてみます。
1.定義されたアスペクト、アドバイスからそれぞれのオブジェクトを生成する
2.アスペクトの適用対象となる型の全てのメッセージをインターセプトする
3.メッセージをインターセプトした際にアスペクトのアドバイス、ポイントカットを評価する
4.アドバイス自身を実行する
5.必要であれば元々の型のメッセージを実行する
s2dotnetではAopProxyクラスが中心になってこれらの機能を実現していると既に書きました。一方CodeProjectの記事「Dynamic Proxy Creation Using C# Emit」ではAOPに関してはまるきり触れていませんでしたが今までのエントリでも解説してきたIProxyInvocationHandlerインタフェースの実装クラスは実は上記機能の中の2.と5.の機能を既に実現しているのです。従ってあとは3.と4.の機能があればs2dotnetにおけるAOP機能を実現できる訳です。
s2dotnetの場合周りの機能は既に整っておりIProxyInvocationHandlerの実装クラスをスクラッチしても良いのですがせっかくAopProxyという見本があるのですからこれをベースにプロキシの機能の部分だけをすげ替えてやることにしました。クラス名はAopProxyを継承していないものの同クラスがベースの動的なプロキシということで仮にDynamicAopProxyクラスとして書いてみたのが以下のクラスです。
[Serializable] public class DynamicAopProxy : IProxyInvocationHandler { private object target; private IAspect aspects; private Type type; private Hashtable parameters; public DynamicAopProxy(Type type) : this(type, null, null, null) {} public DynamicAopProxy(Type type, IAspect aspects) : this(type, aspects, null, null) {} public DynamicAopProxy(Type type, IAspect aspects, Hashtable parameters) : this(type, aspects, parameters, null) {} public DynamicAopProxy(Type type, IAspect aspects, Hashtable parameters, object target) : base() { this.type = type; this.aspects = aspects; this.parameters = parameters; this.target = target; } public object Create() { return ProxyFactory.GetInstance().Create(this, this.type); } public object Create(Type argTypes, object args) { ConstructorInfo constructor = TypeUtil.GetConstructorInfo(this.type, argTypes); target = TypeUtil.NewInstance(constructor, args); return this.Create(); } public object Create(Type argTypes, object args, Type targetType) { ConstructorInfo constructor = TypeUtil.GetConstructorInfo(targetType, argTypes); this.target = TypeUtil.NewInstance(constructor, args); return this.Create(); } public object Invoke(object proxy, MethodInfo method, object args) { if ( this.target == null) { if (! this.type.IsInterface ) this.target = Activator.CreateInstance(type); if ( this.target == null ) this.target = new object(); } ArrayList interceptorList = new ArrayList(); if (aspects != null) { foreach (IAspect aspect in aspects) { IPointcut pointcut = aspect.Pointcut; if (pointcut == null || pointcut.IsApplied(method)) { interceptorList.Add(aspect.Interceptor); } } } if (interceptorList.Count == 0) { return method.Invoke(target, args); } else { IMethodInterceptor interceptors = (IMethodInterceptor[]) interceptorList.ToArray(typeof(IMethodInterceptor)); IMethodInvocation invocation = new MethodInvocationImpl(this.target, method, args, interceptors, this.parameters); return interceptors[0].Invoke(invocation); } } }
RealProxyを拡張していたのを廃止してIProxyInvocationHandlerインタフェースを実装した訳ですが特徴的なのは以下の2つの修正点です。
public object Create() { return ProxyFactory.GetInstance().Create(this, this.target.GetType()); }
AopProxyの実装では透過プロキシを取得していたのをProxyFactoryを介してターゲットの型を拡張した別な型を生成してそのインスタンスを返すように変更しています。このメソッドで返されたオブジェクトは透過プロキシ(TransparentProxy)ではありませんので参照をどのように持ち回しても編み込んだAspectの情報が無効になってしまうことはありません。
public object Invoke(object proxy, MethodInfo method, object[] args) { if ( this.target == null) { if (! this.type.IsInterface ) this.target = Activator.CreateInstance(type); if ( this.target == null ) this.target = new object(); } 〜以下略〜 }
元々の実装ではRealProxy#Invokeメソッドをオーバライドしていましたが本実装では新たなインタフェースの実装に変わっています。しかしながらそれ以外はアスペクトとポイントカットを評価してインターセプタか元々の型のメソッドを実行する部分は殆ど変わりがありません。
これでIL Emit方式によるAOP機能が曲がりなりにも実現できることになります。(ProxyFactory等の付随するクラスは当然ですがどこかのネームスペースに配置しておく必要があります。)
同クラスですがs2dotnetを使用して以下のようなコードで実際にテストを行ってみました。(対象となるTestImplクラスは前回のエントリで使用したサンプルのインタフェースであるITestとその実装クラスであるTestImplをそのまま使用しています)
IMethodInterceptor interceptor = new TraceInterceptor();
IPointcut pointcut = new PointcutImpl(new string{".*"});
IAspect aspects = new AspectImpl(interceptor, pointcut);
DynamicAopProxy proxy = new DynamicAopProxy(new IAspect{aspects}, null, new TestImpl());
ITest test = proxy.Create();
test.TestFunctionOne();
test.TestFunctionTwo(new Object(), new Object());
アスペクトをコードで指定する方法はs2dotnetにおける一般的なコーディングを参考にしました。
実行結果は以下のようなコンソール出力になりました。
BEGIN DynamicProxy.ITest#TestFunctionOne() In TestImpl.TestFunctionOne() END DynamicProxy.ITest#TestFunctionOne() : BEGIN DynamicProxy.ITest#TestFunctionTwo(System.Object, System.Object) In TestImpl.TestFunctionTwo( Object a, Object b ) END DynamicProxy.ITest#TestFunctionTwo(System.Object, System.Object) :
s2dotnetのAOP相当の機能は問題無く実現されました。これで全く問題無いように思っていたのですがいざDIコンテナに仕込んでコンストラクタ、プロパティ等のインジェクションを行おうとしたのですが全く依存性が注入されません。最初は全く解らなかったのですがふと考えて見るとProxyFactoryで生成した型は名前も別な全く違う型のインスタンスであり依存性注入の為に設定ファイルで指定した型名はあてにならないのです。
"aaa" 111
設定ファイルで定義した型はTestImplですがProxyFactoryから取り出した型はTestImplProxyとなり型名が違うので型名をより所にしている全ての依存性注入のための操作は失敗することになります。これを解消するためにはs2dotnetの型を管理している部分を本家Sesar2と同じようにコンポーネントの型とAOPで拡張された型の二つを管理できるようにする必要があります。これだけなら良いのですがインスタンスを生成する際にAOP拡張型をあたかもコンポーネントの型のように見せる必要もあります。これは例えば元の型と全く同じシグネチャを持つコンストラクタを拡張した型の実装にも定義しておく必要があるということです。
今回の取り組みを始めた時は既に型を生成する仕組みも用意されているし最初は楽勝に思えたのですがやはり先人の方々が苦労されていること、簡単に解決とはいかないようです。少なくとも元になる型から継承する必要は無いににしろ同じシグネチャのコンストラクタ、メソッド、プロパティが揃っていないと厳しそうですね。
ILのEmitについてもう少し勉強して拡張元の情報を自由自在に組み込めるようになったらまたチャレンジする予定ですが今週中に具体例を示すことはできそうに有りません。s2dotnetな方々(特にコミッタのsugimotoさん)には変に期待を持たせてしまってすんませんでした。