Seasar DI Container with AOP

S2AOP.NETの概要

S2AOP.NETでは、AOPの機能を提供しています。AOPとは、Aspect Oriented Programming (アスペクト指向プログラミング) の略です。プログラム本来の目的とは異なる処理を内部に埋め込まず、外から織り込むように作ることです。 現在のオブジェクト指向になぜAOPが必要なのか説明します。

オブジェクト指向

現在のオブジェクト指向でプログラムを行う場合、下の図のように分けることが出来ます。

  

オブジェクト指向では顧客からの要求である機能(Core Concern)とロギング機能、宣言的トランザクション、DBコネクションの取得・解放、例外処理、セキュリティ機能や分散処理などの非機能要求 (Crosscutting Concern)が同じクラスの同じメソッドの中に実装されることがあります。

オブジェクト指向の問題点

上で挙げているように非機能要求が散在します。この問題点を例に挙げて説明します。
例)プログラムを作成したところ、何らかの欠陥が見つかりプログラムの動作がおかしく、どこでおかしくなっているのかは分からないので、 プログラム実行中の節目ごとにその途中経過を記録(ロギング)したい場合、ログを書き出すサンプルのコードを以下に示します。 太字で書いた場所がログを書き出す場所です。

public class IHelloImpl : IHello
{
    public void ShowMessage() 
    {
        logger.Log("BEGIN IHello#ShowMessage");
        Console.WriteLine("Hello World !!");
        logger.Log("END IHello#ShowMessage");
    }
}

このサンプルのようにロギングするということは、プログラムの複数箇所にその命令を追加する必要があります。また、編集ツールの検索機能などを使って該当箇所を探すとしても、プログラムの書き換えは最終的に手作業となります。これは、次のような問題を引き起こします。

  • 処理が複数箇所に分散します。後になって集めたい情報が増えたり、処理そのものが不要になったりすると、それに合わせてすべての場所を変更する必要が出てきます。
  • プログラマが、処理を追加するべきでない場所に追加してしまう可能性があります。
  • プログラマが、処理を追加するべき場所に追加し忘れる可能性があります。
  • プログラマが、処理として誤ったコードを追加してしまう可能性があります。
  • プログラマが、作業中に誤って本来のプログラムを壊してしまう可能性があります。

このロギングの例のように、複数のクラスにまたがった非機能要求の処理を「Crosscutting Concern」と呼びます。「Crosscuting Concern」を分散させないために、モジュール化して取り除き、実行時またはコンパイル時にバイトコードで組み込む仕組みが必要になります。先程のロ ギングの例をS2AOP.NETを使うと以下のようにXMLの設定をするだけです。ロギングを行う処理をプログラムの複数箇所に追加する必要がなくなります。

<?xml version="1.0" encoding="utf-8" ?> 
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
"http://www.seasar.org/dtd/components21.dtd">
<components>
    <component name="traceInterceptor" class="Seasar.Framework.Aop.Interceptors.TraceInterceptor"/>
    
    <component class="IHelloImpl" >
        <aspect>traceInterceptor</aspect>
    </component>
    <component name="AopClient" class="AopClient"/>
</components>

設定ファイルの詳細は、S2AOP.NETリファレンスの設定ファイルの説明を参照してください。

IHelloインターフェース
public interface IHello
{
    void ShowMessage();
}
IHelloを実装したHelloImplクラス
public class IHelloImpl : IHello
{
    public void ShowMessage() 
    {
        Console.WriteLine("Hello World !!");
    }
}
HelloImplクラスをコンストラクタ・インジェクションで受け取り、ShowMessageメソッドを呼び出します
public class AopClient
{
    private IHello hello;

    public AopClient(IHello hello) 
    {
        this.hello = hello;
    }

    public void Main()
    {
        hello.ShowMessage();
    }
}
AopClientをS2Containerから受け取り、実行します
SingletonS2ContainerFactory.ConfigPath = "Aop.dicon";
SingletonS2ContainerFactory.Init();

// S2Containerを作成します
IS2Container container = SingletonS2ContainerFactory.Container;

// AopClientをS2Containerから取得します
AopClient aopClient = (AopClient) container.GetComponent("AopClient");

aopClient.Main();
実行すると下記のようにログが出力されます
DEBUG 2006-03-01 00:11:45,406 [1136] BEGIN IHello#ShowMessage()
Hello World !!
DEBUG 2006-03-01 00:11:45,406 [1136] END IHello#ShowMessage() :

AOPを考える上でキーとなる概念


Advice(MethodInterceptor)

プログラム中に挿入されるコードを表します。Interceptorと呼ばれることもあります。

Joinpoint(MethodInvocation)

対象となるクラスとAdviceを結合するポイントを表します。AdviceはJoinpointから引数やメソッドの情報を取得することができます。

Pointcut

どこにJoinpointを設定するのかを定義します。

Aspect

AdviceとPointcutを関連付けます。

AOPのメリット

  • 「Core Concern」と「Crosscutting Concern」を分離することでメンテナンス性が向上します。
  • 業務ロジックからシステム的機能を「Crosscutting Concern」に排出した「Core Concern」は、シンプルなソースになります。本来のやりたかったことだけが記述されます。
  • トランザクションの自動化など、従来エンタープライズアプリケーションの知識が必要であった処理が、 普通の.NETのオブジェクトで可能になります。

S2AOP.NETのメリット

  • 設定をシンプルに行えます。
  • 実装しなければならない.NETインターフェースが1つです。
  • コンポーネントにどんなアスペクトが適用されるのかが明確です。
  • 基本的なAspect実装オブジェクトパターンが用意されているため、すぐに使用することが可能です。(独自にインターフェースや抽象クラスを実装することも可能)

注意点

  • アスペクトを適用するためにはコンポーネントをインターフェース型で受け取ります。
  • S2Containerから直接受け取るコンポーネントにアスペクトを適用するには、 コンポーネントがSystem.MarshalByRefObjectの派生クラスである必要があります。
  • pointcut属性を指定しない場合、pointcut属性に".*"と指定した場合は、 実装しているインターフェースのすべてのメソッドが対象になります。
  • thisポインタ経由の場合(インターフェース経由で呼ばれない場合)はアスペクトは適用されません。

S2AOP.NETリファレンス

作成すべきファイル

S2AOP.NET を使用するにはS2Container の設定ファイル(diconファイル)で行います。設定ファイルの配置場所は、とくに指定がありませんが、通常「Crosscutting Concern」と同じ場所に配置するか、設定を行うコンポーネントと同じ場所に配置します。

設定ファイルの説明

aspectタグ(AOPを使用する場合は必須)

アスペクトをコンポーネントに組み込みます。Interceptorの指定は、ボディでJScript.NET式を使うか、子タグでcomponentタグを使います。

注意点

aspectタグで指定されたコンポーネントは、コンテナの初期化時にコンテナから取得されます。そのため、aspectタグで指定されたコンポーネントのinstance属性がprototypeだったとしても、Interceptor のメソッドが呼び出される度に新しいインスタンスが作成されるわけではありません。

pointcut属性(任意)

カンマ区切りで対象となるメソッド名を指定することができます。pointcutを指定しない場合は、 コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。 メソッド名には正規表現(System.Text.RegularExpressions.Regex)も使えます。

設定例

pointcut 属性を指定してSystem.Collections.HashtableのAddメソッドとClearメソッドを対象とする場合以下のようになります。 pointcut属性を指定しない場合はSystem.Collections.Hashtableが実装しているインターフェース (ここではSystem.Collections.IDictionary)のメソッドが対象になります。

<component class="System.Collections.Hashtable">
    <aspect pointcut="Add,Clear">
        <component class="Seasar.Framework.Aop.Interceptors.TraceInterceptor"/>
    </aspect>
</component>
正規表現を使ってSystem.Collections.Hashtableが実装しているインターフェース (ここではSystem.Collections.IDictionary)のメソッドすべてを対象としたい場合は、以下のように設定します。
<component class="System.Collections.Hashtable">
    <aspect pointcut=".*">
        <component class="Seasar.Framework.Aop.Interceptors.TraceInterceptor"/>
    </aspect>
</component>

S2AOP.NETで用意されているInterceptor

S2AOP.NETでは、以下のInterceptorを用意しています。また独自のInterceptorを簡単に作成できるようになっています。

(1) TraceInterceptor

クラス名

Seasar.Framework.Aop.Interceptors.TraceInterceptor

説明

トレース処理を「Crosscutting Concern」として扱うためのInterceptorです。 HashtableクラスにTraceInterceptorを適用したdiconファイルは、以下のようになります。対象とするメソッドはAddとします。

<component class="System.Collections.Hashtable">
    <aspect pointcut="Add">
        <component class="Seasar.Framework.Aop.Interceptors.TraceInterceptor"/>
    </aspect>
</component>

詳しい使用方法はTraceInterceptorを参照してください。

(2) MockInterceptor

クラス名

Seasar.Framework.Aop.Interceptors.MockInterceptor

説明

Mockを使ったテストを簡単に行うためのInterceptorです。

(3) 独自実装によるInterceptor

説明

独自にInterceptorを作成する場合は、次のインターフェースまたは、抽象クラスを実装します。

Seasar.Framework.Aop.IMethodInterceptor
Seasar.Framework.Aop.Interceptors.AbstractInterceptor

インターフェースを実装する場合は、以下のInvokeメソッドを実装します。

public object Invoke(IMethodInvocation invocation)

抽象クラスを継承する場合は、以下のInvokeメソッドをオーバーライドします。

public override object Invoke(IMethodInvocation invocation)

AbstractInterceptor は、IMethodInterceptorを実装した抽象クラスです。AbstractInterceptorには、 Proxyオブジェクトを取得するCreateProxyメソッドとアスペクトを適用するコンポーネント定義を取得するGetComponentDefメソッドがあります。 アスペクトを適用したクラス名を必要とするInterceptor(例えば、ログ出力を行うInterceptor)を作成する場合は、 AbstractInterceptorを使用することで簡単にクラス名を取得することができます。

public object CreateProxy(Type proxyType)
protected IComponentDef GetComponentDef(IMethodInvocation invocation)

IMethodInvocation のプロパティTarget、Method、Argumentsで対象となるオブジェクト、メソッド、引数を取得できます。 proceed()を呼び出すと実際のメソッドが呼び出され実行結果を取得することができます。以下のような独自のInterceptorを作成したとします。

作成例
public class TestInterceptor : IMethodInterceptor
{
    public object Invoke(IMethodInvocation invocation)
    {
        Console.WriteLine("Before");    // 呼ぶ前はBefore
        
        object ret = invocation.Proceed();
        
        Console.WriteLine("After");     // 呼んだ後はAfter
        
        return ret;
    }
}

IMethodInvocation#Proceed()を呼ぶ前と後で2分され、呼ぶ前は Beforeの個所を実行し、呼んだ後はAfterの個所を実行します。 1つのコンポーネントに複数のアスペクトが定義されている場合は、以下のよう実行されます。

  1. Aspectの登録順にIMethodInterceptorのBefore部分が実行されます。
  2. 最後のIMethodInterceptorのBefore部分を実行した後にコンポーネント自身のメソッドが呼び出されます。
  3. Aspectの登録の逆順にIMethodInterceptorのAfter部分が実行されます。

詳しい使用方法は独自実装によるInterceptorを参照してください。

diconファイルを使用しないでアスペクトを組み込む方法

diconファイルの設定を行わずプログラム上でアスペクトを組み込むこともできます。作成方法は次のようになります。

  • Seasar.Framework.Aop.Impl.PointcutImpl のコンストラクタの引数で対象となるメソッド名を指定(複数可)します。 System.Collections.Hashtableのようにインターフェースを実装しているなら、new PointcutImpl(typeof(Hashtable))のようにTypeクラスを指定することで、 そのクラスが実装しているインターフェースのメソッドをすべて自動的に適用させることもできます。
  • Seasar.Framework.Aop.Impl.AspectImplのコンストラクタの第1引数にInterceptorを指定して、 第2引数にPointcutImplで作成したPointcutを指定します。
  • Seasar.Framework.Aop.Proxy.AopProxyのコンストラクタで、 対象となるクラス(この場合はSystem.MarshalByRefObjectの派生クラスである必要があります)、 もしくは対象となるクラスが実装しているインターフェースとAspectImplで作成したAspectの配列を指定します。
  • Seasar.Framework.Aop.Proxy.AopProxy#Create()でAspectが適用されたオブジェクトを取得できます。

System.Collections.HashtableクラスにTraceInterceptorをプログラム上で適用する場合は、次のようになります。 対象となるメソッドはgetTime()とします。

IPointcut pointcut = new PointcutImpl(new string[]{"Add"});
IAspect aspect = new AspectImpl(new TraceInterceptor(), pointcut);
AopProxy aopProxy = new AopProxy(typeof(IDictionary),
     new IAspect[]{aspect}, null, new Hashtable());
IDictionary proxy = (IDictionary) aopProxy.Create();
proxy.Add("aaa", "bbb");

Example

以下のサンプルを実行する場合は、セットアップを行う必要があります。

TraceInterceptor

TraceInterceptor を使用してSystem.Collections.ArrayListクラスと System.Collections.HashtableクラスのAddメソッドとClearメソッドが呼ばれた場合にトレースを出力させましょう。 作成するファイルは以下のとおりです。

  • コンポーネントを定義するdiconファイル(Trace.dicon)
  • 設定が正しく行われているか確認する実行ファイル(AopTraceClient.cs)

diconファイルの作成

  • TraceInterceptorをコンポーネント定義します。name属性をtraceInterceptorとします。
  • System.Collections.ArrayListクラスのコンポーネントの定義します。 aspectタグにInterceptorを指定します。
  • System.Collections.Hashtableクラスのコンポーネントの定義します。 pointcut属性にAddメソッドとClearメソッドを指定します。aspectタグにInterceptorを指定します。

Trace.dicon

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
    <component name="traceInterceptor"
        class="Seasar.Framework.Aop.Interceptors.TraceInterceptor"/>
    
    <component class="System.Collections.ArrayList>
        <aspect>traceInterceptor</aspect>
    </component>
    
    <component class="System.Collections.Hashtable">
        <aspect pointcut="Add, GetHashCode">
            traceInterceptor
        </aspect>
    </component>
    
    <component name="AopTraceInterceptor"
        class="Seasar.Examples.Reference.Aop.AopTraceClient" />
</components>

実行ファイルの作成

  • Seasar.Framework.Container.S2Container#Create()メソッドの最初の引数に、 作成したdiconファイル(Trace.dicon)のパスを指定してコンテナを作成します。
  • Seasar.Framework.Container.S2Container#GetComponent()メソッドの第1引数に コンポーネントに登録したクラスが実装しているインターフェースのTypeクラス (typeof(IList)、typeof(IDictionary)を指定してコンポーネントを取得します。
  • トレースがAspectされるか確認するために取得したコンポーネント(IList)のCountプロパティを実行します。
  • 同様に取得したコンポーネント(IDictionary)のAddメソッド、Clearメソッドを実行します。

AopTraceClient.cs

using System;
using System.Collections;
using Seasar.Framework.Container;
using Seasar.Framework.Container.Factory;

namespace Seasar.Examples.Reference.Aop
{
    public class AopTraceClient
    {
        private const string PATH = "Seasar.Examples/Reference/Aop/Trace.dicon";

        public void Main()
        {
            IS2Container container = S2ContainerFactory.Create(PATH);
            IList list = (IList) container.GetComponent(typeof(IList));
            int count = list.Count;

            IDictionary dictionary = (IDictionary) 
                container.GetComponent(typeof(IDictionary));
            dictionary.Add("aaa", "bbb");
            dictionary.GetHashCode();
        }
    }
}

実行結果

メソッドが呼ばれる前と後でトレースが出力されているのが確認できます。

DEBUG 2005-09-26 23:12:16,138 [2564] BEGIN System.Collections.ICollection#get_Count()
DEBUG 2005-09-26 23:12:16,138 [2564] END System.Collections.ICollection#get_Count() : 0
DEBUG 2005-09-26 23:12:16,138 [2564] BEGIN System.Collections.IDictionary#Add(aaa, bbb)
DEBUG 2005-09-26 23:12:16,138 [2564] END System.Collections.IDictionary#Add(aaa, bbb) : 
DEBUG 2005-09-26 23:12:16,138 [2564] BEGIN System.Object#GetHashCode()
DEBUG 2005-09-26 23:12:16,138 [2564] END System.Object#GetHashCode() : 23

このサンプルは、Seasar.ExamplesプロジェクトのSeasar.Examples/Reference/Aop以下に用意されています。


独自実装によるInterceptor

クラス名、メソッド名、引数とメソッドの処理時間を計測してトレースするInterceptorを作成しましょう。 また、そのInterceptorを使用して重い処理を行った時間をトレースさせましょう。作成するファイルは以下のとおりです。

  • クラス名、メソッド名、引数とメソッドの処理時間を計測して出力するInterceptor(MeasurementInterceptor.cs)
  • 重い処理を行うクラス(HeavyProcess.cs)
  • コンポーネントの定義を行うdiconファイル(Measurement.dicon)
  • 設定が正しく行われているか確認する実行ファイル(AopMeasurementClient.cs)

独自実装のIntercepterの作成

  • Seasar.Framework.Aop.Interceptors.AbstractInterceptorクラスを実装します。
  • Invoke(IMethodInvocation invocation)メソッドを実装します。
  • GetComponentDef(invocation).ComponentType.FullNameでクラスの完全限定名を取得します。
  • invocation.Method.Nameでメソッド名を取得します。
  • invocation.Argumentsで引数を取得します。
  • invocation.Proceed()で実際のメソッドが呼ばれるので、その前の時間を取得します。
  • invocation.Proceed()で実際のメソッドが呼ばれた後の時間を取得してfinallyで出力します。

MeasurementInterceptor.cs

using System;
using System.Text;
using Seasar.Framework.Aop.Interceptors;

namespace Seasar.Examples.Reference.Aop
{
    public class MeasurementInterceptor : AbstractInterceptor
    {
        public override object Invoke(Seasar.Framework.Aop.IMethodInvocation invocation)
        {
            long start = 0;
            long end = 0;
            StringBuilder buf = new StringBuilder(100);
            
            buf.Append(GetComponentDef(invocation).ComponentType.FullName);
            buf.Append("#");
            buf.Append(invocation.Method.Name);
            buf.Append("(");
            object[] args = invocation.Arguments;
            if(args != null && args.Length > 0)
            {
                foreach(object arg in args)
                {
                    buf.Append(arg);
                    buf.Append(", ");
                }
                buf.Length = buf.Length - 2;
            }
            buf.Append(")");
            try
            {
                start = DateTime.Now.Ticks;
                object ret = invocation.Proceed();
                end = DateTime.Now.Ticks;
                buf.Append(" : ");
                return ret;
            }
            catch(Exception ex)
            {
                buf.Append(" Exception:");
                buf.Append(ex);
                throw ex;
            }
            finally
            {
                Console.WriteLine(buf.ToString() + ((end - start) / 10000));
            }
        }
    }
}

重い処理を行うクラスの作成

  • 重い処理を行ったということにするために5秒間Sleepします。

HeavyProcess.cs

using System;

namespace Seasar.Examples.Reference.Aop
{
    public class HeavyProcess : MarshalByRefObject
    {
        public void Heavy()
        {
            try
            {
                System.Threading.Thread.Sleep(5000);
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.StackTrace);
            }
        }
    }
}
diconファイルの作成
  • 作成したMeasurementInterceptorをコンポーネント定義します。name属性をmeasurementとします。
  • HeavyProcessクラスのHeavy()メソッドにMeasurementInterceptorをaspectします。

Measurement.dicon

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
	<component name="measurement"
		class="Seasar.Examples.Reference.Aop.MeasurementInterceptor" />
	
	<component class="Seasar.Examples.Reference.Aop.HeavyProcess">
		<aspect pointcut="Heavy">measurement</aspect>
	</component>
	
	<component name="AopMeasurementInterceptor"
		class="Seasar.Examples.Reference.Aop.AopMeasurementClient" />
</components>

実行ファイルの作成

  • Seasar.Framework.Container.S2Container#Create()メソッドの第1引数に作成したdiconファイル (Measurement.dicon)のパスを指定してコンテナを作成します。
  • Seasar.Framework.Container.S2Container#GetComponent()メソッドの 第1引数にコンポーネントに登録したクラス名(typeof(HeavyProcess))を指定して取得します。
  • コンテナから取得したHeavyProcess#Heavy()メソッドを実行します。

AopMeasurementClient.cs

using System;
using Seasar.Framework.Container;
using Seasar.Framework.Container.Factory;

namespace Seasar.Examples.Reference.Aop
{
    public class AopMeasurementClient
    {
        private const string PATH = "Seasar.Examples/Reference/Aop/Measurement.dicon";

        public void Main()
        {
            IS2Container container = S2ContainerFactory.Create(PATH);
            HeavyProcess heavyProcess = (HeavyProcess) 
                container.GetComponent(typeof(HeavyProcess));
            heavyProcess.Heavy();
        }
    }
}

実行結果

クラス名、メソッド名、引数とメソッドの処理時間がトレースされているのが確認できます。

Seasar.Examples.Reference.Aop.HeavyProcess#Heavy() : 5007

このサンプルは、Seasar.ExamplesプロジェクトのSeasar.Examples/Reference/Aop以下に用意されています。