2009年6月21日日曜日

RouteHandlerはシングルインスタンス

いや~、サブジェクトの件、全然知りませんでした。

ASP.NET MVCはControllerクラスのAction実行でViewを返すじゃないっすか。ControllerはDefaultControllerFactoryがインスタンスを作るんですよね。で、IControllerさえ実装してればそれはつまりControllerであると。なので、IController.Executeだけを実装したサンプルを書いて、まずはそれを確認。

using System;
using System.Web.Mvc;

namespace RouteHandler.Controllers
{
  public class SimpleController : IController
  {
    private DateTime _createTime = DateTime.Now;
    public void Execute(System.Web.Routing.RequestContext requestContext)
    {
      requestContext.HttpContext.Response.Write(string.Format(@"<html>
<body>
<h1>Simple Controller</h1>
アクションやら完全無視!<br />
{0}
</body>
</html>", _createTime));
    }
  }
}

簡単なコードです。特にこれと言って変なところは無いですね。ただ、こういう使い方をするとActionなんて無いし、この中に定義したとしても、RouteDataにアクションへの情報を参照してInvokeするコードが無いので無視するだけです。ようはActionInvokerに相当する機能がないって事です。

わざわざ_createTimeを取っているのは、この後の確認作業で理由が判明する予定。SimpleControllerという名前なので、デフォルトのルーティング設定だけで、このコントローラは実行まで行くことが出来ます。動かしたのが↓この画面。

route1

画面を拡大してみると分かると思うけど、URLは単純に/simpleです。で、ですよ、Controllerは単純にIHttpHandlerなんだから今度はIHttphandlerを実装してしまえば、DefaultControllerFactoryを経由させないでも出来るってことデスよ。でも、その為にはRouteHandlerが必要になります。いきなりの展開で話が分かりにくいっすね。

ルーティングモジュールがFrontControllerとしてすべての要求を全部受け入れてくれるじゃないですか(そういう言い方しないですかね)。で、その中で登録されてるルーティング情報に従ってRouteDataに色々なデータを突っ込んでくれる。RouteCollectionExtensions.MapRouteが色々セットアップしてUrlRoutingHandler(UrlRoutingHandler クラス (System.Web.Routing))を使ってそこから先はMVCの方で好きなようにやりたまえと責任が委譲されます。MvcRouteHandlerとMvcHandlerがその後を引き継いで、ごにょごにょと進んでいくけど、UrlRoutingHandlerの所を自分で実装したものに差し替えちゃえば、ControllerFactoryなど経由せずIHttpHandlerを直に使えるっていう流れなんですが、こうなるとMVCじゃなくてただのRouting利用っすね...。まぁ、いいや。UrlRoutingHandlerはIRouteHandlerの実装なので、それを実装。

using System;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Libraries
{
  public class SimpleRouteHandler : IRouteHandler
  {
    private DateTime _createTime = DateTime.Now;

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      routeData.DataTokens["routeTime"] = _createTime;

      var httpHandler = new SimpleHttpHandler(requestContext);
      return httpHandler;
    }
  }
}

ただGetHttpHandlerを実装するだけデス。返すハンドラはSimpleHttpHandler。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Libraries
{
  public class SimpleHttpHandler : IHttpHandler
  {
    private DateTime _createTime = DateTime.Now;
    private DateTime _routeTime;
    
    public SimpleHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      _routeTime = (DateTime)routeData.DataTokens["routeTime"];
    }

    public void ProcessRequest(HttpContext context)
    {
      context.Response.Write(string.Format(@"<html>
<body>
<h1>Simple RouteHandler</h1>
HttpHandlerを直接使う<br />
RouteHandler : {0}<br />
HttpHandler : {1}<br />
</body>
</html>", _routeTime, _createTime));
    }

    public bool IsReusable
    {
      get { return false; }
    }
  }
}

ProcessRequestを実装しただけですが、せっかくRouteHandlerは自前なのでRouteDataを参照したいじゃないですか。なので、コンストラクタでRequestContextを受け取るようにしておいて、RouteDataを取り出しておきます。このRouteHandlerをRouteTableに登録。

      routes.Add(new Route("route", new SimpleRouteHandler()));

Global.asaxのRegisterRoutesに追加するだけです。

これを実行したのが↓これ。

route2 

時間2箇所だしてるので分かるかな~、とは思うんですが、RouteHandlerが生成された時間と、HttpHandlerが生成された時間が少しずれてます。もう少し時間をおいて試したのが↓こっち。

route3

HttpHandlerは毎回違う時間(IsReusable=falseかどうかは見ずに毎回newしてる)になってるけど、RouteHandlerは1個前の写真の時間と同じです。ピンと来た?ちなみに最初のSimpleControllerは毎回時間が更新されます。

そうなんですよ、RouteHandlerのインスタンスは使い回されるんですよ。しかも沢山のスレッドから同時に。なので、RouteHandlerの中でスレッドセーフじゃないクラスを使ったりするとちょっと切ない思いをすることになります。データベースにアクセスしようとかしたらダメですよ...。ロックなんてしたら、これまたスケールしなくなるし。

RouteHandlerにはシンプルなロジックだけを書くようにしましょう、という話でした。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。