通过源代码研究ASP.NET MVC中的Controller和View(六)
2010-11-26 23:16 Ivony... 阅读(4944) 评论(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。
先来看看这两个类型分别代表什么,从名称来看,应该是控制器和行为的描述符,那么具体描述了一些什么东西呢?
GetCustomAttributes和IsDefined三个方法是用于实现ICustomAttributeProvider(实现这个接口用于获取附着在描述对象的特性,System.Reflection下大部分描述元数据的类型都实现了这个接口,如Assembly、MethodInfo等)的,除此之外,主要就是FindAction和GetCanonicalActions(获取经典的行为?)。
继续来看看ActionDescriptor:
除去实现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:
这个东西看起来的确就是一个参数描述符。思路大体上能够理顺了,那么接下来,是研究实现的时间。
看看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:
好了,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前段的逻辑了:
- 从被取了别名的(Aliased)方法中找别名(ActionNameAttribute.Name)与actionName相匹配的方法
- 再从没有取别名的方法中找方法名与actionName相匹配的方法
- 把这两个结果整合(AddRange)。
- 再运行SelectionFilter(选择筛选器?)
- 最后如果结果集里只有一个方法,那么返回,有多个则异常,没有则返回空。
最后来看看选择筛选器干了些什么:
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也是一个抽象类,但他的派生类很多:
不过这不要紧,因为我看到了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之后,如果方法组中还存在有多个方法,则会直接抛出异常。可以知道(最重要结论):
- 同名(方法名或别名)的方法一定要有不同性质的ActionMethodSelectorAttribute(没有也算一种性质)。
- 如果同一个性质的ActionMethodSelectorAttribute被应用到两个同名的方法,当这个Attribute验证通过时,将出错,这很危险,也是容易造成隐患的地方。
- MVC框架内所有的这些ActionMethosSelectorAttribute除了AcceptVerbsAttribute之外都是互斥的(不可能同时满足),这样的好处是只要两个同名方法没有附着一样类型的特性,就一定不会同时列入候选列表而抛出异常。但如果你自己写了一些ActionMethodSelector与现有的不互斥,你要特别注意会不会有一种特定的情况导致两个同名方法同时满足,这将是很难检出的隐患。
- 方法的签名在FindAction的过程中是被无视的,除非你自己写一个ActionMethodSelectorAttribute来判断方法签名与当前请求是否匹配。
- 综上所述,没事别整同名的方法。
这一篇就到这里了。
在结束之前,我分享一个非常搞笑的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到底是干啥用的,以及,它的名字是什么。。。。