Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

Spring.NETを使用したASP.NETとDIの連携

ASP.NETでの開発をサポートするSpring.Webの機能

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/11/17 12:00

次期リリースであるSpring.NET 1.0には、DI・AOP機能の他にASP.NETでの開発を強力にサポートする機能が多く含まれています。本稿では、Spring 1.1 Web Preview版を使用し、サンプルアプリケーションを通じてその機能を紹介します。

はじめに

 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版をインストールします。

 Spring 1.1 Web Preview版を日本語の環境で動作させるには、一箇所ソースコードを修正する必要があります。ソースコード「インストール先\src\Spring\Spring.Web\Web\Localization\Resolvers \DefaultWebLocaleResolver.cs」を、以下のように修正してください。
51行目修正前
return new CultureInfo(context.Request.UserLanguages[0]);
 を
51行目修正後
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を行わない場合
DIを行わない場合

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

DIする場合
DIする場合

 以上のようにDIを使用することでテストの自動化を容易にし、アプリケーション開発の安定に大きな力を発揮することができます。

サンプルアプリケーション

 「SpringWebSample」は、Web上からアルバムの購入を行う簡単なWebアプリケーションです。

サンプル画面
サンプル画面

 このサンプルには、通常のDIやAOPの他にもWebページのDIやマスターページなど、Spring.NETの特徴を色々と取り入れています。

 実行するには、「SpringWebSample」をダウンロードし、その中にある「Spring.Example.Web」フォルダを仮想ディレクトリに設定してください。スタートページは「Spring.Example.Web\Page\AlbumListPage.aspx」です。

 また、本サンプルに含まれているSpring.NETアセンブリは、先に説明した修正を行っています。

ロジック部の作成

 データストアから、アルバム一覧を取得するインターフェースと注文を格納するインターフェースを作成します。

IProductDao.cs
/// <summary>製品データアクセス</summary>
public interface IProductDao {
    /// <summary>全アルバムを取得する</summary>
    /// <returns>アルバムのリスト</returns>
    IList GetAlbums();
}
IOrderDao.cs
/// <summary>注文データアクセス</summary>
public interface IOrderDao {
    /// <summary>格納する</summary>
    /// <param name="sale">販売</param>
    /// <returns>注文ID</returns>
    int Store( Sale sale );
}

 次に、これらのデータアクセスインターフェースを使用する注文サービスクラスを実装します。

OrderService.cs
/// <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;
    }
}

 IOrderDaoIProductDaoの実装クラスがコンテナから注入されるため、実装の意識せずにOrderServiceクラスを作成することができます。

 またテストには、IOrderDaoIProductDaoそれぞれモッククラスを容易に設定することができます。

ロジック部のオブジェクト定義ファイル

 上記クラスの設定(IOrderDaoの実装とIProductDaoの実装をコンストラクタ引数に指定する)をXMLファイルで記述できます。

Data.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に注入します。

Service.xml
<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ファイルに定義します。

Aspects.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クラスの設定内容を以下に修正します。

「Service.xml」の修正
<!-- 注文サービス -->
<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を作成することができます。ProxyFactoryObjectinterceptorNamesプロパティに、以下のインターフェースを実装したクラスを設定します。

インターセプトの種類
インターフェース名内容
IMethodInterceptorメソッド呼び出しのインターセプト
IMethodBeforeAdviceメソッド呼び出しの前をインターセプト
IAfterReturningAdviceメソッド呼び出しの後をインターセプト
IThrowsAdvice例外をインターセプト

 IMethodInterceptorを使用した簡単なサンプルは以下のようになります。

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クラスを使用してメソッド名を指定する方法もあります。

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ページを作成します。

MovieListPage.cs(一部抜粋)
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属性など、ページクラスの作成をシンプルにする様々な機能が実装されています。

 ページのライフサイクルの基本的なパターンは、以下のようになります。

  1. OnInitializeDataModelメソッドをオーバーライドし、画面に使用するデータモデルを取得する(初回時のみコールされる)。
  2. 取得したデータモデルをDataModel属性がマークされているプロパティに設定する(以後ポストバックしてもデータが保持されている)。
  3. OnInitializeControlsメソッドをオーバーライドし、データモデルをコントロールにバインドする。
  4. SetResultメソッドを使用して、他ページ(オブジェクト定義ファイルに設定する)に遷移する。

 このように、複雑になりがちなWebページのライフサイクルをシンプルに管理できるようになります。さらにSpring.NETでは、ASP.NET 2.0から実装される「マスターページ」も実現することができます。

UI部のオブジェクト定義ファイル

Web.xml
<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クラスには、以下のセッタープロパティが用意されています。

Spring.Web.UI.Pageクラスの主なプロパティ
プロパティ名内容
ResultSetResultメソッドで使用するリダイレクト先のページとID
MasterマスターページのオブジェクトID
CssRootスタイルシートのルートパス
ImagesRootイメージファイルのルートパス
ScriptsRootスクリプトファイルのルートパス

 MasterCssRootImagesRootなどは全ページ共通の設定になるため、abstract/parent属性を使用して管理しています。

 また、ResultsプロパティにはSetResultメソッドで遷移するページを設定します。これにより、ページ遷移もソースコードから取り除くことができます。

マスターページ

 先に触れましたが、Spring.NETではマスターページを作成することができます。この機能により、全ページ共通のテンプレートを作成できるため、管理が非常に簡単になります。

Template.aspx(一部抜粋)
<%@ 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>
AlbumListPage.aspx(一部抜粋)
<%@ 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コンテナの設定を記述します。

Web.config(一部抜粋)
<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を試してみてください。

参考資料

  1. Spring.NETホーム(英語)
  2. Spring.NETサポートフォーラム(英語)
  3. Spring.NET JP
  4. CodeZine 『S2Container.NETのDIとAOPを活用し生産性の高いシステムを構築する』 青木淳夫 著、2005年10月
  5. @IT 『Spring Frameworkで理解するDI』 山本大 著、2005年4月

 

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5