はじめに
Javaのオープンソースフレームワークに「Spring Framework」というものがあります。DIコンテナとAOPの機能を提供するフレームワークで、世界中で広く利用されています。
今回は、これを.NETに移植した「Spring.NET」というフレームワークを紹介します。現在の最新バージョンは「1.0.0」ですが、2005年末にリリース予定の「1.1」のプレビュー版「Spring 1.1 Web Preview 」で、Spring.Webを使用した機能を紹介します。Spring.Webには、ASP.NETでの開発を強力にサポートする機能が多く含まれています。
対象読者
- .NETで開発されている方
- TDD(テスト駆動型開発)に興味を持っている方
- アプリケーション・アーキテクチャに興味を持っている方
必要な環境
Microsoft Visual Studio .NET 2003上で動作確認しています。
Spring.NETの入手とセットアップ
Spring 1.1 Web Preview版をインストールします。
return new CultureInfo(context.Request.UserLanguages[0]);
return CultureInfo.CreateSpecificCulture(context.Request.UserLanguages[0]);
Visual Studioでビルドしたファイルは、「インストール先\build\VS.Net\」以下に作成されます。
Spring.NETによるメリット
『S2Container.NETのDIとAOPを活用し生産性の高いシステムを構築する』でも紹介されていますが、DIのメリットは、インターフェースと実装を分離して変更容易性を大きく向上することです。またクラス間の依存をコードから取り除くことで、Mockの使用を容易にします。これは、TDD(テスト駆動型開発)には大きな力を発揮します。
例として、DBアクセスと通信処理があるアプリケーションで考えます。DBアクセスロジックのテストは、DBの初期化など重い処理が必要になります。それでもなんとか自動化することができます。しかし、通信ロジックは、外部環境に完全に依存してしまうため、テストの自動化は大変困難です。両クラスを呼び出すサービスクラスも、通信ロジックへ直接依存してしまうため、テストの自動化ができなくなります。

DIでは、この問題を解決することができます。サービスクラスと、DBアクセスクラスや通信ロジッククラスとの依存を外部コンテナ(DIコンテナ)に任せることにより、通信ロジック以外を自動テストすることが可能になります。

以上のようにDIを使用することでテストの自動化を容易にし、アプリケーション開発の安定に大きな力を発揮することができます。
サンプルアプリケーション
「SpringWebSample」は、Web上からアルバムの購入を行う簡単なWebアプリケーションです。
このサンプルには、通常のDIやAOPの他にもWebページのDIやマスターページなど、Spring.NETの特徴を色々と取り入れています。
実行するには、「SpringWebSample」をダウンロードし、その中にある「Spring.Example.Web」フォルダを仮想ディレクトリに設定してください。スタートページは「Spring.Example.Web\Page\AlbumListPage.aspx」です。
また、本サンプルに含まれているSpring.NETアセンブリは、先に説明した修正を行っています。
ロジック部の作成
データストアから、アルバム一覧を取得するインターフェースと注文を格納するインターフェースを作成します。
/// <summary>製品データアクセス</summary> public interface IProductDao { /// <summary>全アルバムを取得する</summary> /// <returns>アルバムのリスト</returns> IList GetAlbums(); }
/// <summary>注文データアクセス</summary> public interface IOrderDao { /// <summary>格納する</summary> /// <param name="sale">販売</param> /// <returns>注文ID</returns> int Store( Sale sale ); }
次に、これらのデータアクセスインターフェースを使用する注文サービスクラスを実装します。
/// <summary>注文サービス</summary> public class OrderService : IOrderService { IOrderDao _orderDao; IProductDao _productDao; // コンストラクタインジェクション public OrderService( IOrderDao orderDao, IProductDao productDao ) { _orderDao = orderDao; _productDao = productDao; } public IList GetAlbums() { return _productDao.GetAlbums(); } public SaleConfirmation Order( Sale sale ) { int ID = _orderDao.Store( sale ); SaleConfirmation confirmation = new SaleConfirmation( sale, ID.ToString( "00000000" ) ); return confirmation; } }
IOrderDao
とIProductDao
の実装クラスがコンテナから注入されるため、実装の意識せずにOrderService
クラスを作成することができます。
またテストには、IOrderDao
、IProductDao
それぞれモッククラスを容易に設定することができます。
ロジック部のオブジェクト定義ファイル
上記クラスの設定(IOrderDao
の実装とIProductDao
の実装をコンストラクタ引数に指定する)をXMLファイルで記述できます。
<objects> <!-- 製品DAO(IProductDaoの実装クラス)--> <object id="productDao" type="Spring.Example.Core.Data.Impl.MemoryProductDao,Spring.Example.Core"/> <!-- 注文DAO(IOrderDaoの実装クラス)--> <object id="orderDao" type="Spring.Example.Core.Data.Impl.
MemoryOrderDao,Spring.Example.Core"/> </objects>
<objects>
タグの中に<object>
タグでオブジェクトを登録します。<onject>
タグには、主に以下の属性を指定することができます。
属性名 | 内容 |
id | オブジェクトのID |
type | クラス名,アセンブリ名 |
singleton | シングルトンで生成("true" ,"false" ) |
parent | 設定を引き継ぐ親オブジェクトのID |
abstract | オブジェクトの生成を拒否("true" ,"false" ) |
そして、「Data.xml」で設定したオブジェクトをOrderServiceに注入します。
<objects> <!-- 注文サービス(IOrderServiceの実装クラス) --> <object id="orderService" type="Spring.Example.Core.Service.OrderService,Spring.Example.Core"> <!-- コンストラクタインジェクション --> <constructor-arg index="0" ref="orderDao" /> <constructor-arg index="1" ref="productDao" /> </object> </objects>
DIの方法として、現在Spring.NETは、コンストラクタインジェクションとセッターインジェクションの2種類をサポートしてます。上記例では、コンストラクタインジェクション(<constructor-arg>
タグ)を使用しています。
ref
属性で注入するオブジェクトIDを設定します。また、例ではindex
属性でターゲットを指定していますが、他にname
属性で引数名を指定する方法もあります。
AOPの設定
Spring.NETには、メソッドの戻り値をキャッシュするAOPを提供しています。CacheAttribute
属性を使用して、対象となるメソッドをマーキングすることができます。
// 60秒間キャッシュする [ Cache( TimeToLive = 60, SlidingExpiration = true, Priority = CachePriority.Low ) ] public IList GetAlbums() { return _productDao.GetAlbums(); }
次に、実際にキャッシュ処理を行うAOPをXMLファイルに定義します。
<!-- ASP.NET キャッシュ --> <object id="aspNetCacheAdvice" type="Spring.Web.Advice.AspNetCacheAdvice, Spring.Web"/> <!-- キャッシュアスペクト --> <object id="cacheAspect" type="Spring.Aop.Support.DefaultPointcutAdvisor, Spring.Aop"> <property name="Pointcut"> <!-- CacheAttribute属性がマークされているメソッドを対象とする --> <object type="Spring.Aop.Support.AttributeMatchMethodPointcut,Spring.Aop"> <property name="Attribute" value="Spring.Attributes.CacheAttribute, Spring.Core"/> </object> </property> <!-- ASP.NETキャッシュを使用する --> <property name="Advice" ref="aspNetCacheAdvice"/> </object>
上記設定の内容は、ASP.NETのキャッシュに保存するAspNetCacheAdvice
クラスを、CacheAttribute
属性がマークされているメソッドにインターセプトさせるというものです。
そして、OrderService
クラスの設定内容を以下に修正します。
<!-- 注文サービス --> <object id="orderServiceTarget" type="Spring.Example.Core.Service.OrderService,Spring.Example.Core"> <constructor-arg index="0" ref="orderDao" /> <constructor-arg index="1" ref="productDao" /> </object> <!-- 注文サービス プロキシ --> <object id="orderService" type="Spring.Aop.Framework.ProxyFactoryObject" > <property name="target" ref="orderServiceTarget" /> <!-- cacheAspectをインターセプトさせる--> <property name="interceptorNames" value="cacheAspect" /> </object>
ProxyFactoryObject
というクラスを使用して、プロキシを作成することができます。
上記の設定では、OrderService
クラスのプロキシ(キャッシングする機能が追加されている)を生成しています。
現状、この機能はASP.NETのキャッシュを使用する方法しかありませんが、次回リリースからは、EnterpriseLibraryの「Caching Application Block」を使用する方法もサポートされる予定です。
インターセプトの種類
もちろん、カスタムのAOPを作成することができます。ProxyFactoryObject
のinterceptorNames
プロパティに、以下のインターフェースを実装したクラスを設定します。
インターフェース名 | 内容 |
IMethodInterceptor | メソッド呼び出しのインターセプト |
IMethodBeforeAdvice | メソッド呼び出しの前をインターセプト |
IAfterReturningAdvice | メソッド呼び出しの後をインターセプト |
IThrowsAdvice | 例外をインターセプト |
IMethodInterceptor
を使用した簡単なサンプルは以下のようになります。
public class TraceInterceptor : IMethodInterceptor { public object Invoke(IMethodInvocation invocation) { Console.WriteLine( string.Format( "TraceInterceptor Call: メソッド名{0}", invocation.Method.Name ) ); object returnValue = invocation.Proceed(); Console.WriteLine( string.Format( "TraceInterceptor Return: {0}", returnValue ) ); return returnValue; } }
キャッシングの例ではカスタム属性を使用してアスペクト対象のメソッドをマークしていますが、他にもRegexpMethodPointcutAdvisor
クラスを使用してメソッド名を指定する方法もあります。
<object id="traceAspect" type="Spring.Aop.Support.RegexpMethodPointcutAdvisor, Spring.Aop"> <!-- 正規表現を使用してメソッド名を指定する --> <property name="pattern" value="Get*"/> <property name="advice"> <object type="Sample.TraceInterceptor"/> </property> </object>
Webページの作成
OrderServiceをDIするWebページを作成します。
public class AlbumListPage : Spring.Web.UI.Page { /// <summary>アルバム一覧</summary> /// <remarks> /// DataModel属性を使用すると、ポストバックの間保存される /// </remarks> [ DataModel ] public IList Albums { get { return _albums; } set { _albums = value; } } /// <summary>注文サービス</summary> /// <remarks>DIからセッターインジェクションされる</remarks> public IOrderService OrderService { get { return _orderService; } set { _orderService = value; } } /// <summary>データモデルの初期化を行う</summary> /// <remarks>IsPostBack = false の時に呼ばれる</remarks> /// <param name="e"></param> protected override void OnInitializeDataModel(EventArgs e) { base.OnInitializeDataModel (e); // サービスからアルバム一覧を取得する Albums = OrderService.GetAlbums(); } /// <summary>コントロールの初期化を行う</summary> /// <remarks>毎回呼ばれる</remarks> /// <param name="e"></param> protected override void OnInitializeControls(EventArgs e) { base.OnInitializeControls (e); if( !IsPostBack ) { // データモデルをRepeaterにバインドする Repeater.DataSource = Albums; Repeater.DataBind(); } } /// <summary>購入ボタンイベント</summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnBuy_Click(object sender, System.EventArgs e) { Session[ "Sale" ] = MakeSale(); // 構成ファイルで指定されている // 「displayOrder」ページに遷移する SetResult( "displayOrder" ); } }
ページをコンテナで管理するためには、Spring.Web.UI.Page
クラスを派生します。このクラスには、DataModel
属性など、ページクラスの作成をシンプルにする様々な機能が実装されています。
ページのライフサイクルの基本的なパターンは、以下のようになります。
OnInitializeDataModel
メソッドをオーバーライドし、画面に使用するデータモデルを取得する(初回時のみコールされる)。- 取得したデータモデルを
DataModel
属性がマークされているプロパティに設定する(以後ポストバックしてもデータが保持されている)。 OnInitializeControls
メソッドをオーバーライドし、データモデルをコントロールにバインドする。SetResult
メソッドを使用して、他ページ(オブジェクト定義ファイルに設定する)に遷移する。
このように、複雑になりがちなWebページのライフサイクルをシンプルに管理できるようになります。さらにSpring.NETでは、ASP.NET 2.0から実装される「マスターページ」も実現することができます。
UI部のオブジェクト定義ファイル
<objects> <!-- マスターページ --> <object id="masterTemplate" type="~/Page/Template.aspx"/> <!-- ローカライズオブジェクト --> <object id="localeResolver" type="Spring.Web.Localization.Resolvers.SessionLocaleResolver,Spring.Web"/> <object id="localizer" type="Spring.Web.Localization.Localizers.ResourceSetLocalizer,
Spring.Web"/> <!-- 全ページ共通の設定 --> <object id="standardPage" abstract="true"> <!-- マスターページ --> <property name="Master" ref="masterTemplate"/> <!-- ローカライズ --> <property name="LocaleResolver" ref="localeResolver"/> <property name="Localizer" ref="localizer"/> <!-- スタイルシートルート --> <property name="CssRoot" value="Css"/> <!-- イメージファイルルート --> <property name="ImagesRoot" value="Images"/> <!-- スクリプトファイルルート --> <property name="ScriptsRoot" value="Scripts"/> </object> <!-- アルバム一覧ページ --> <object type="~/Page/AlbumListPage.aspx" parent="standardPage"> <property name="OrderService" ref="orderService"/> <property name="Results"> <dictionary> <entry key="displayOrder" value="redirect:OrderPage.aspx"/> </dictionary> </property> </object> </objects>
OrderServiceクラス
をセッターインジェクション(<property>
タグ)で設定しています。
また、Spring.Web.UI.Page
クラスには、以下のセッタープロパティが用意されています。
プロパティ名 | 内容 |
Result | SetResult メソッドで使用するリダイレクト先のページとID |
Master | マスターページのオブジェクトID |
CssRoot | スタイルシートのルートパス |
ImagesRoot | イメージファイルのルートパス |
ScriptsRoot | スクリプトファイルのルートパス |
Master
やCssRoot
、ImagesRoot
などは全ページ共通の設定になるため、abstract
/parent
属性を使用して管理しています。
また、Results
プロパティにはSetResult
メソッドで遷移するページを設定します。これにより、ページ遷移もソースコードから取り除くことができます。
マスターページ
先に触れましたが、Spring.NETではマスターページを作成することができます。この機能により、全ページ共通のテンプレートを作成できるため、管理が非常に簡単になります。
<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <%@ Page language="c#" Codebehind="Template.aspx.cs" AutoEventWireup="false" Inherits="Spring.Example.Web.Page.Template" %> <HTML> <head> <!-- リソースファイルから取得 --> <title> <%= GetMessage("default.title") %> </title> <LINK href="<%= CssRoot %>/default.css" type=text/css rel=stylesheet> </head> <body> <form id="Form1" method="post" runat="server"> <div id="logo"> <img src="<%= ImagesRoot %>/title.png"> </div> <BR> <div id="container"> <div id="content"> <!-- コンテンツの注入場所 --> <spring:ContentPlaceHolder id="body" runat="server" /> </div> </div> </form> </body> </HTML>
<%@ Page language="c#" Codebehind="AlbumListPage.aspx.cs" AutoEventWireup="false" Inherits="Spring.Example.Web.Page.AlbumListPage" %> <%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <HTML> <body> <!-- コンテンツの注入先をcontentPlaceholderIdで指定 --> <spring:Content id="body" contentPlaceholderId="body" runat="server"> <asp:Repeater id="Repeater" runat="server"> <!-- 省略 --> </asp:Repeater> </spring:Content> </body> </HTML>
「Template.aspx」にある<spring:ContentPlaceHolder>
タグで、他ページのコンテンツを注入します。注入するコンテンツは、<spring:Content>
タグで囲み、contentPlaceholderId
属性で、ContentPlaceHolder
のIDを設定します。
コンテナの設定
最後に、「Web.config」にSpringコンテナの設定を記述します。
<configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" /> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler,Spring.Core" /> </sectionGroup> </configSections> <appSettings> <!-- log4net設定ファイルへのパス --> <add key="Log4NetConfigFile" value="~/Config/Log4NET.xml"/> </appSettings> <spring> <context type="Spring.Context.Support.WebApplicationContext,
Spring.Web"> <!-- 参照するオブジェクト設定ファイルへのパス --> <resource uri="~/Config/Aspects.xml" /> <resource uri="~/Config/Web.xml" /> <resource uri="~/Config/Data.xml" /> <resource uri="~/Config/Service.xml" /> <resource uri="~/Config/Log4net.xml" /> </context> </spring> <system.web> <httpHandlers> <!-- DIするために使用するSpring.NETカスタムのHTTPハンドラ --> <!-- Webページ用 --> <add verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web" /> <!-- Webサービス用 --> <add verb="*" path="*.asmx" type="Spring.Web.Services.WebServiceHandlerFactory,
Spring.Web"/> </httpHandlers> <!-- 省略 --> </system.web> </configuration>
<resource>
タグで、オブジェクトを定義している設定ファイルへの参照を追加します。それ以外の部分は、上記内容を変更する必要はありません。
最後に
Spring.NETは現在も発展途上のプロダクトです。今回は次期リリースでのWeb機能を紹介しましたが、今後のバージョンで以下の機能がリリースされる予定です。
- Release 1.1 (Q4 2005)
- Web
- Services
- .NET 2.0 Support
- Enterprise Library Integration
- IBatis.NET Integration
- Release 1.2 (Q1 2006)
- Data Access(ADO.NET abstraction)
- Smart Client (WinForms)
- Windows Services
- Console Applications
Spring.NETの強みは、Enterprise LibraryやIBatis.NET、NHibernateなど、他フレームワークとの連携もサポートし、これらの接着剤として利用できることがあります。
興味を持った方は、ぜひSpring.NETとDIを試してみてください。
参考資料
- Spring.NETホーム(英語)
- Spring.NETサポートフォーラム(英語)
- Spring.NET JP
- CodeZine 『S2Container.NETのDIとAOPを活用し生産性の高いシステムを構築する』 青木淳夫 著、2005年10月
- @IT 『Spring Frameworkで理解するDI』 山本大 著、2005年4月