代码改变世界

通过源代码研究ASP.NET MVC中的Controller和View(六)

2010-11-26 23:16  Ivony...  阅读(4931)  评论(9编辑  收藏  举报

通过源代码研究ASP.NET MVC中的Controller和View(一)

通过源代码研究ASP.NET MVC中的Controller和View(二)

通过源代码研究ASP.NET MVC中的Controller和View(三)

通过源代码研究ASP.NET MVC中的Controller和View(四)

通过源代码研究ASP.NET MVC中的Controller和View(五)

 

上篇谈到Controller最终把执行的操作外包给了ActionInvoker,其默认实现大体上是这么一个过程:

  • 查找Action(FindAction)
  • 获取参数
  • InvokeActionMethod
  • InvokeActionResult

 

那我先从查找Action入手研究其逻辑,其相关代码如下:

      ControllerDescriptor controllerDescriptor = GetControllerDescriptor( controllerContext );
      ActionDescriptor actionDescriptor = FindAction( controllerContext, controllerDescriptor, actionName );

首先获取了一个Controllerdescriptor,然后借助ControllerDescriptor查找ActionDescriptor。

先来看看这两个类型分别代表什么,从名称来看,应该是控制器和行为的描述符,那么具体描述了一些什么东西呢?

image

GetCustomAttributes和IsDefined三个方法是用于实现ICustomAttributeProvider(实现这个接口用于获取附着在描述对象的特性,System.Reflection下大部分描述元数据的类型都实现了这个接口,如Assembly、MethodInfo等)的,除此之外,主要就是FindAction和GetCanonicalActions(获取经典的行为?)。

继续来看看ActionDescriptor:

image

除去实现ICustomAttributeProvider之外的三个方法,我看到还有这样几个方法:

  • Execute( ControllerContext, IDictionary<string,string> )
  • GetFilters() : FilterInfo
  • GetParameters() : ParameterDescriptor[]
  • GetSelectors() : ICollection<ActionSelector>

我发现了一个有趣的事实,ControllerDescriptor通过FindAction方法可以获得一个ActionDescriptor,而ActionDescriptor又可以GetParameters来获取一个ParameterDescriptor的数组。换言之,ControllerDescriptor是一个ActionDescriptor的抽象容器,而ActionDescriptor是一个ParameterDescriptor的抽象容器。从这些名称你能看出啥?

考虑到ControllerDescriptor.ControllerType的存在,我有理由相信,ControllerDescriptor是一个依赖具体类型的描述符,换言之这是一个TypeDescriptor,而从名称来看,ParameterDescriptor应该是参数的描述符,类型描述符包含操作描述符集合,操作描述符包含参数描述符集合。直接推论:ActionDescriptor应该是一个方法描述符(MethodDescriptor),至少是一个被抽象的方法的描述符。它可以传递一些parameter来被Execute,得到一个object。即使ActionDescriptor没有对应某个具体的方法,从GetParamters和Execute来看,它至少可以被当作一个方法来发现、绑定(Bind,利用ParameterDescriptor[])以及调用执行(Execute)。不妨顺便来看看ParameterDescriptor:

image

这个东西看起来的确就是一个参数描述符。思路大体上能够理顺了,那么接下来,是研究实现的时间。

看看GetControllerDescription的实现:

    protected virtual ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext )
    {
      Type controllerType = controllerContext.Controller.GetType();
      ControllerDescriptor controllerDescriptor = DescriptorCache.GetDescriptor( controllerType, () => new ReflectedControllerDescriptor( controllerType ) );
      return controllerDescriptor;
    }

首先是获取Controller的运行时类型,然后这个DescriptorCache.GetDescriptor从名字和调用上可以大体上猜测,这个方法会首先到缓存中根据controllerType查找有没有缓存的东东,如果没有,就调用后面的匿名方法创建实例返回并缓存,来证实一下:

  internal sealed class ControllerDescriptorCache : ReaderWriterCache<Type, ControllerDescriptor>
  {

    public ControllerDescriptorCache()
    {
    }

    public ControllerDescriptor GetDescriptor( Type controllerType, Func<ControllerDescriptor> creator )
    {
      return FetchOrCreateItem( controllerType, creator );
    }

  }

FetchOrCreateItem这个方法名进一步的证实了猜测,我们继续看这个方法的实现:

    protected TValue FetchOrCreateItem( TKey key, Func<TValue> creator )
    {
      // first, see if the item already exists in the cache
      _rwLock.EnterReadLock();
      try
      {
        TValue existingEntry;
        if ( _cache.TryGetValue( key, out existingEntry ) )
        {
          return existingEntry;
        }
      }
      finally
      {
        _rwLock.ExitReadLock();
      }

      // insert the new item into the cache
      TValue newEntry = creator();
      _rwLock.EnterWriteLock();
      try
      {
        TValue existingEntry;
        if ( _cache.TryGetValue( key, out existingEntry ) )
        {
          // another thread already inserted an item, so use that one
          return existingEntry;
        }

        _cache[key] = newEntry;
        return newEntry;
      }
      finally
      {
        _rwLock.ExitWriteLock();
      }
    }

结果已经非常明朗,_rwLock的EnterXXX和ExitXXX方法显然是进入和退出读锁以及写锁。去掉这些同步代码看起来就会是这样:

    protected TValue FetchOrCreateItem( TKey key, Func<TValue> creator )
    {

      TValue existingEntry;
      if ( _cache.TryGetValue( key, out existingEntry ) )
        return existingEntry;

      TValue newEntry = creator();
      _cache[key] = newEntry;
      return newEntry;
    }

现在答案就是一目了然的了。

 

缓存的逻辑并非主线,还是回到GetControllerDescriptor继续分析。根据之前被证实的猜测,最终创建ControllerDescriptor的,就是这个匿名方法:

() => new ReflectedControllerDescriptor( controllerType )

换言之,其实的GetControllerDescriptor的实现大体上就是这样:

    protected virtual ControllerDescriptor GetControllerDescriptor( ControllerContext controllerContext )
    {
      return new ReflectedControllerDescriptor( controllerContext.Controller.GetType() ) );
    }

创建ControllerDescriptor也就是利用Controller的运行时类型创建一个ReflectedControllerDescriptor的实例而已。这进一步证实了ControllerDescriptor其实是一个TypeDescriptor的猜测。

 

接下来看FindAction的实现:

    protected virtual ActionDescriptor FindAction( ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName )
    {
      ActionDescriptor actionDescriptor = controllerDescriptor.FindAction( controllerContext, actionName );
      return actionDescriptor;
    }

FindAction啥活儿也没干,直接把工作又外包给了刚创建的ControllerDescriptor对象,我们知道ControllerDescriptor其实是一个ReflectedControllerDescriptor的实例,所以来看看这个实例的实现:

    public override ActionDescriptor FindAction( ControllerContext controllerContext, string actionName )
    {
      if ( controllerContext == null )
      {
        throw new ArgumentNullException( "controllerContext" );
      }
      if ( String.IsNullOrEmpty( actionName ) )
      {
        throw new ArgumentException( MvcResources.Common_NullOrEmpty, "actionName" );
      }

      MethodInfo matched = _selector.FindActionMethod( controllerContext, actionName );
      if ( matched == null )
      {
        return null;
      }

      return new ReflectedActionDescriptor( matched, actionName, this );
    }

调用了_selector的FindActionMethod方法来得到一个方法信息(MethodInfo)然后用这个方法来创建一个ReflectedActionDescriptor的实例。看来刚才的猜测一点没错,ActionDescriptor的确是一个方法的描述符。那么,这个_selector又是什么?

    private readonly ActionMethodSelector _selector;

哈,又引入了一个新的类型ActionMethodSelector,从名字来看,这个类完全是为了Select一个Method而存在的。这个类型没有任何派生类,也不派生自任何类,并且还是一个密封类(sealed),职责也非常明确,就是选择ActionMethod,而这个 ActionMethod应该就是我们在控制器中写的什么Index或是 About方法。

还是来看看FindActionMethod的实现:

    public MethodInfo FindActionMethod( ControllerContext controllerContext, string actionName )
    {
      List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods( controllerContext, actionName );
      methodsMatchingName.AddRange( NonAliasedMethods[actionName] );
      List<MethodInfo> finalMethods = RunSelectionFilters( controllerContext, methodsMatchingName );

      switch ( finalMethods.Count )
      {
        case 0:
          return null;

        case 1:
          return finalMethods[0];

        default:
          throw CreateAmbiguousMatchException( finalMethods, actionName );
      }
    }

先调用了GetMatchingAliasedMethods方法,然后再将这个方法的结果与NonAliasedMethods[actionName]合并,最后RunSelectionFilters(运行选择筛选器)。最后看获取的方法恰好一个的话就返回。

这里的Matching和Aliased容易把人搞晕,求助谷歌大神,matching是一个形容词,相匹配的意思。aliased谷歌大神也没办法帮我,但我知道alias是别名的意思,推测aliased是alias的过去式,那就是已经alias的意思,或者被alias的意思。也许,就是被别名的意思吧。

所以GetMatchingAliasedMethod的解释就是:获取 相匹配的 被别名的 方法。

呃,,,先不看方法,因为我看到有一个很奇怪的对象叫做NonAliasedMethods,这个东西是哪来的?值是什么?

    public ILookup<string, MethodInfo> NonAliasedMethods
    {
      get;
      private set;
    }

哈,这玩意儿竟然是个ILookup,不常见啊,那么他的值是哪里来的,看看构造函数:

    public ActionMethodSelector( Type controllerType )
    {
      ControllerType = controllerType;
      PopulateLookupTables();
    }

然后:

    private void PopulateLookupTables()
    {
      MethodInfo[] allMethods = ControllerType.GetMethods( BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public );
      MethodInfo[] actionMethods = Array.FindAll( allMethods, IsValidActionMethod );

      AliasedMethods = Array.FindAll( actionMethods, IsMethodDecoratedWithAliasingAttribute );
      NonAliasedMethods = actionMethods.Except( AliasedMethods ).ToLookup( method => method.Name, StringComparer.OrdinalIgnoreCase );
    }

哈,在这里看到了两个熟悉的东西,AliasedMethods和NonAliasedMethods。他们分别是这么来的:

首先allMethods是ControllerType(就是传给ControllerDescriptor的Controller.GetType(),具体实现可以自己看源代码)的所有公开的实例方法集合。然后对这个集合进行了一次筛选,Array.FindAll其实就类似于Where方法,后面的那个IsValidActionMethod是筛选条件,这个方法的实现是这样的:

    private static bool IsValidActionMethod( MethodInfo methodInfo )
    {
      return !(methodInfo.IsSpecialName ||
               methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom( typeof( Controller ) ));
    }

那么这里定义了几种情况不是合法的ActionMethod(会被筛掉):

  • 是特殊的名称(编译器生成的方法、构造函数等)
  • 方法的原始声明类型(假设一个类型A有一个虚方法virtual Test,被派生类B重写为override Test,则GetBaseDefinition获取到A中定义的虚方法Test,即为原始声明类型)是Controller或是Controller的基类(IsAssignableFrom)。

简单的说,编译器生成的方法和定义在Controller里面的方法,就不是合法的ActionMethod,除此之外,都是。

结合起来:ControllerType里面所有公开的实例方法,除去编译器生成的、构造函数、Controller及其基类定义的方法及他们的重写之外,剩下的都是ActionMethod(看来返回值没什么限制哦,但也许限制不在这里)。

 

然后,合法的ActionMethod(actionMethods)被分成两拨,一拨是满足IsMethodDecoratedWithAliasingAttribute的(AliasedMethods),另一拨是剩下的(Except)。来看看这个名字很长的方法的实现:

    private static bool IsMethodDecoratedWithAliasingAttribute( MethodInfo methodInfo )
    {
      return methodInfo.IsDefined( typeof( ActionNameSelectorAttribute ), true /* inherit */);
    }

如果你记心好的话,应该会记得这个IsDefine刚才出现过,没错,这是ICustomAttributeProvider接口的一个成员。他用于检查方法是否定义了(附着了)某个类型的特性,这里这个类型是ActionNameSelectorAttribute,后面的true表示如果定义了这个特性类的派生类(派生特性)也算在内。

 

那么这里的逻辑可以理清了,所有定义了ActionNameSelectorAttribute特性的方法,都是AliasedMethod(被别名的方法),除此之外,都是NonAliasedMethod(没被别名的方法)。

没有被别名的方法会被转换为一个ILookup对象,ILookup说白了,就是GroupBy的结果的可检索Key版本。ILookup首先是一个IEnumerable<IGrouping<TKey,TValue>>(继承于它),其次,ILookup提供了一个索引器,用于获取Key等于特定值的IGrouping。下图说明了ILookup:

image

好了,ILookup并不是重点,我看到这里作为Key的是method.Name(方法名),并且传入了一个StringComparer.OrdinalIgnoreCase,不区分大小写的字符串比较器。也就是说这里的Key将不区分大小写。

回到FindActionMethod方法,那么NonAliasedMethods[actionName]就可以理解了,由于ILookup的Key是method.Name,所以NonAliasedMethods[actionName]就是获取所有名字和actionName一样的方法(不区分大小写)。

那么继续来看看GetMatchingAliasMethods的实现:

    internal List<MethodInfo> GetMatchingAliasedMethods( ControllerContext controllerContext, string actionName )
    {
      // find all aliased methods which are opting in to this request
      // to opt in, all attributes defined on the method must return true

      var methods = from methodInfo in AliasedMethods
                    let attrs = (ActionNameSelectorAttribute[]) methodInfo.GetCustomAttributes( typeof( ActionNameSelectorAttribute ), true /* inherit */)
                    where attrs.All( attr => attr.IsValidName( controllerContext, actionName, methodInfo ) )
                    select methodInfo;
      return methods.ToList();
    }

LINQ表达式的描述性很强,我很喜欢,这段LINQ表达式直接描述是这样的:

从AliasedMethod集合中获取一个个methodInfo,获取methodInfo的ActionNameSelectorAttribute特性集合并命名为attrs,从中所有这些methodInfo中筛选出attrs集合中每一项都满足IsValidName的项。

简单的说,选择AliasedMethod中,所有ActionNameSelectorAttribute特性都满足IsValidName的methodInfo,那么,IsValidName又是什么逻辑?

这个方法在ActionNameSelectorAttribute中是一个抽象方法,这个类只有一个实现类ActionNameAttribute,所以这个方法也就只有一份实现(至少在 MVC框架里):

  
ActionNameAttribute:

    public override bool IsValidName( ControllerContext controllerContext, string actionName, MethodInfo methodInfo )
    {
      return String.Equals( actionName, Name, StringComparison.OrdinalIgnoreCase );
    }

那么这里就是简单的比较了一下actionName和自己的Name属性。这个特性干什么用的基本上也就能推导出来了,如果你想给方法取一个别名(不用方法名作为actionName),就可以应用这个特性,然后取一个你喜欢的名字。

 

这里的实现似乎存在一个非常明显的Bug,如果我为一个方法取了两个别名,那么这个方法应该就不可能被映射到了。因为这里的判断逻辑是所有(All)的Attribute都要IsValidName,换言之这个actionName要同时等于两个别名,才会被选择,这显然不可能。所以这里的All应该改为Any才对。

不过事实上,一个方法不能被附着两个ActionNameAttribute,因为这个特性是不能多次应用的(在这个类型和基类的AttributeUsage定义了AllowMultiple = false),所以不可能出现两个这样的特性。

 

OK,至此,已经可以完全了解FindActionMethod前段的逻辑了:

  1. 从被取了别名的(Aliased)方法中找别名(ActionNameAttribute.Name)与actionName相匹配的方法
  2. 再从没有取别名的方法中找方法名与actionName相匹配的方法
  3. 把这两个结果整合(AddRange)。
  4. 再运行SelectionFilter(选择筛选器?)
  5. 最后如果结果集里只有一个方法,那么返回,有多个则异常,没有则返回空。

 

最后来看看选择筛选器干了些什么:

    private static List<MethodInfo> RunSelectionFilters( ControllerContext controllerContext, List<MethodInfo> methodInfos )
    {
      // remove all methods which are opting out of this request
      // to opt out, at least one attribute defined on the method must return false

      List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>();
      List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>();

      foreach ( MethodInfo methodInfo in methodInfos )
      {
        ActionMethodSelectorAttribute[] attrs = (ActionMethodSelectorAttribute[]) methodInfo.GetCustomAttributes( typeof( ActionMethodSelectorAttribute ), true /* inherit */);
        if ( attrs.Length == 0 )
        {
          matchesWithoutSelectionAttributes.Add( methodInfo );
        }
        else if ( attrs.All( attr => attr.IsValidForRequest( controllerContext, methodInfo ) ) )
        {
          matchesWithSelectionAttributes.Add( methodInfo );
        }
      }

      // if a matching action method had a selection attribute, consider it more specific than a matching action method
      // without a selection attribute
      return ( matchesWithSelectionAttributes.Count > 0 ) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
    }

首先定义了两个列表,With和Without Selection Attributes,然后遍历所有的方法,获取方法上附着的ActionMethodSelectorAttribute,如果方法上没有这个特性(attrs.Length == 0),那么归入matchesWithoutSelectionAttributes这一拨,如果方法上有这个特性,那么调用特性的IsValidForRequest,为true的归入matchesWithSelectionAttributes这一拨,其他的方法抛弃。

最后,如果With这一拨有任何方法,返回With这一拨,否则返回Without这一拨。

简单的说:

如果有方法附着了ActionMethodSelectorAttribute,而又IsValidForRequest的话,那么就返回这些方法。否则,返回没有附着ActionMethodSelectorAttribute的方法。

当然,ActionMethodSelectorAttribute也是一个抽象类,但他的派生类很多:

image

不过这不要紧,因为我看到了HttpPostAttribute,其实那就是[HttpPost]么,在MVC范例网站的AccountController里面就能看到:

    [HttpPost]
    public ActionResult LogOn( LogOnModel model, string returnUrl )
    {
      if ( ModelState.IsValid )
      {
        if ( MembershipService.ValidateUser( model.UserName, model.Password ) )
        {
          FormsService.SignIn( model.UserName, model.RememberMe );
          if ( !String.IsNullOrEmpty( returnUrl ) )
          {
            return Redirect( returnUrl );
          }
          else
    ......

我知道HttpPost是用来标识仅当请求是以Post方式提交的时候才调用这个方法,那么这个Attribute的IsValidForRequest的实现则可以简单的检查请求是不是POST提交过来的达到所需要的效果。其实现我瞄了一眼,比较麻烦,就不在这里展开了,还是尽快走主线逻辑吧,这些内容大家如果有兴趣完全可以自行研究。

写在最后,这里的逻辑非常值得注意,由于在SelectionFilters之后,如果方法组中还存在有多个方法,则会直接抛出异常。可以知道(最重要结论):

  1. 同名(方法名或别名)的方法一定要有不同性质的ActionMethodSelectorAttribute(没有也算一种性质)。
  2. 如果同一个性质的ActionMethodSelectorAttribute被应用到两个同名的方法,当这个Attribute验证通过时,将出错,这很危险,也是容易造成隐患的地方。
  3. MVC框架内所有的这些ActionMethosSelectorAttribute除了AcceptVerbsAttribute之外都是互斥的(不可能同时满足),这样的好处是只要两个同名方法没有附着一样类型的特性,就一定不会同时列入候选列表而抛出异常。但如果你自己写了一些ActionMethodSelector与现有的不互斥,你要特别注意会不会有一种特定的情况导致两个同名方法同时满足,这将是很难检出的隐患。
  4. 方法的签名在FindAction的过程中是被无视的,除非你自己写一个ActionMethodSelectorAttribute来判断方法签名与当前请求是否匹配。
  5. 综上所述,没事别整同名的方法。

 

这一篇就到这里了。

 

在结束之前,我分享一个非常搞笑的ActionMethodSelectorAttribute实现:

namespace System.Web.Mvc
{
  using System.Reflection;

  [AttributeUsage( AttributeTargets.Method, AllowMultiple = false, Inherited = true )]
  public sealed class *******Attribute : ActionMethodSelectorAttribute
  {
    public override bool IsValidForRequest( ControllerContext controllerContext, MethodInfo methodInfo )
    {
      return false;
    }
  }
}

这是MVC框架里面的一个类型的源代码,类型的的名字被打上了马赛克,不妨猜猜这个Attribute到底是干啥用的,以及,它的名字是什么。。。。