ASP.NET MVC:Filter和Action的执行介绍
程序员文章站
2024-03-05 13:07:48
根据controller的名字正确的实例化了一个controller对象。回到mvchandler的beginprocessrequest方法,可以看到,当得到contro...
根据controller的名字正确的实例化了一个controller对象。回到mvchandler的beginprocessrequest方法,可以看到,当得到controller对象之后,首先判断它是不是iasynccontroller,如果是则会创建委托用来异步执行。通常情况下,我们都是继承自controller类,这不是一个iasynccontroller,于是会直接执行controller的execute方法。execute方法是在controller的基类controllerbase中定义的,这个方法除去一些安全检查,初始化了controllercontext(包含了controllerbase和request的信息),核心是调用了executecore方法,这在controllerbase是个抽象方法,在controller类中有实现:
复制代码 代码如下:
protected override void executecore() {
possiblyloadtempdata();
try {
string actionname = routedata.getrequiredstring("action");
if (!actioninvoker.invokeaction(controllercontext, actionname)) {
handleunknownaction(actionname);
}
}
finally {
possiblysavetempdata();
}}
这个方法比较简单,首先是加载临时数据,这仅在是child action的时候会出现,暂不讨论。接下来就是获取action的名字,然后invokeaction, 这里的actioninvoker是一个controlleractioninvoker类型的对象,我们来看它的invokeaction方法,
复制代码 代码如下:
public virtual bool invokeaction(controllercontext controllercontext, string actionname) {
if (controllercontext == null) {
throw new argumentnullexception("controllercontext");
}
if (string.isnullorempty(actionname)) {
throw new argumentexception(mvcresources.common_nullorempty, "actionname");
}
controllerdescriptor controllerdescriptor = getcontrollerdescriptor(controllercontext);
actiondescriptor actiondescriptor = findaction(controllercontext, controllerdescriptor, actionname);
if (actiondescriptor != null) {
filterinfo filterinfo = getfilters(controllercontext, actiondescriptor);
try {
authorizationcontext authcontext = invokeauthorizationfilters(controllercontext, filterinfo.authorizationfilters, actiondescriptor);
if (authcontext.result != null) {
// the auth filter signaled that we should let it short-circuit the request
invokeactionresult(controllercontext, authcontext.result);
}
else {
if (controllercontext.controller.validaterequest) {
validaterequest(controllercontext);
}
idictionary<string, object> parameters = getparametervalues(controllercontext, actiondescriptor);
actionexecutedcontext postactioncontext = invokeactionmethodwithfilters(controllercontext, filterinfo.actionfilters, actiondescriptor, parameters);
invokeactionresultwithfilters(controllercontext, filterinfo.resultfilters, postactioncontext.result);
}
}
catch (threadabortexception) {
// this type of exception occurs as a result of response.redirect(), but we special-case so that
// the filters don't see this as an error.
throw;
}
catch (exception ex) {
// something blew up, so execute the exception filters
exceptioncontext exceptioncontext = invokeexceptionfilters(controllercontext, filterinfo.exceptionfilters, ex);
if (!exceptioncontext.exceptionhandled) {
throw;
}
invokeactionresult(controllercontext, exceptioncontext.result);
}
return true;
}
// notify controller that no method matched
return false;}
这是一个非常核心的方法,有很多工作在这里面完成。asp.net mvc中有几个以descriptor结尾的类型,首先获得controllerdescriptor,这个比较简单,实际返回的是reflectedcontrollerdescriptor对象。第二步实际上是调用了reflectedcontrollerdescriptor的findaction方法,获得actiondescriptor,actiondescriptor最重要的属性是一个methodinfo,这就是当前action name对应的action的方法。findaction方法内部实际上是调用了actionmethodselector的findactionmethod来获得methodinfo,可以想象,这个方法将会反射controller的所有方法的名字,然后和action name匹配,实际上,asp.net还支持一些额外的功能,主要是: 1.通过actionnameattribute属性重命名action的名字;2.支持actionmethodselectorattribute对action方法进行筛选,比如[httppost]之类的。下面简单看下actionmethodselector的实现,大致分为4步,首先是在构造函数中调用了如下方法反射controller中的所有action方法:
复制代码 代码如下:
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);
}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);
} }
这个方法是很清晰的,找到重命名之后符合的,本身名字符合的,然后所有的方法判断是否满足actionmethodselectorattribute的条件,最后或者返回匹配的methodinfo,或者抛出异常,或者返回null。三个步骤的实现并不困难,不再分析下去。
第三步是得到filter。 filterinfo filterinfo = getfilters(controllercontext, actiondescriptor);实际调用的是:
filterproviders.providers.getfilters(controllercontext, actiondescriptor);这里的代码风格和之前的不太一样,特别喜欢用各种委托,读代码有点困难,估计不是同一个人写的。下面的分析都直接给出实际执行的代码。首先看下filterprovider的构造函数:
复制代码 代码如下:
static filterproviders() {
providers = new filterprovidercollection();
providers.add(globalfilters.filters);
providers.add(new filterattributefilterprovider());
providers.add(new controllerinstancefilterprovider());
}
回忆下asp.net给action加上filter的方法一共有如下几种:
1. 在application_start注册全局filter
2. 通过属性给action方法或者controller加上filter
3. controller类本身也实现了iactionfilter等几个接口。通过重写controller类几个相关方法加上filter。
这三种方式就对应了三个filterprovider,这三个provider的实现都不是很困难,不分析了。到此为止,准备工作都好了,接下来就会执行filter和action,asp.net的filter一共有4类:
filter type | interface | description |
authorization | iauthorizationfilter | runs first |
action | iactionfilter | runs before and after the action method |
result | iresultfilter | runs before and after the result is executed |
exception | iexceptionfilter | runs if another filter or action method throws an exception |
复制代码 代码如下:
protected virtual authorizationcontext invokeauthorizationfilters(controllercontext controllercontext, ilist<iauthorizationfilter> filters, actiondescriptor actiondescriptor) {
authorizationcontext context = new authorizationcontext(controllercontext, actiondescriptor);
foreach (iauthorizationfilter filter in filters) {
filter.onauthorization(context);
if (context.result != null) {
break;
}
}
return context;}
注意到在实现iauthorizationfilter接口的时候,要表示验证失败,需要在onauthorization方法中将参数context的result设置为actionresult,表示验证失败后需要显示的页面。接下来如果验证失败就会执行context的result,如果成功就要执行getparametervalues获得action的参数,在这个方法内部会进行model binding,这也是asp.net的一个重要特性,另文介绍。再接下来会分别执行invokeactionmethodwithfilters和invokeactionresultwithfilters,这两个方法的结构是类似的,只是一个是执行action方法和iactionfilter,一个是执行actionresult和iresultfilter。以invokeactionmethodwithfilters为例分析下:
复制代码 代码如下:
protected virtual actionexecutedcontext invokeactionmethodwithfilters(controllercontext controllercontext, ilist<iactionfilter> filters, actiondescriptor actiondescriptor, idictionary<string, object> parameters) {
actionexecutingcontext precontext = new actionexecutingcontext(controllercontext, actiondescriptor, parameters);
func<actionexecutedcontext> continuation = () =>
new actionexecutedcontext(controllercontext, actiondescriptor, false /* canceled */, null /* exception */) {
result = invokeactionmethod(controllercontext, actiondescriptor, parameters)
};
// need to reverse the filter list because the continuations are built up backward
func<actionexecutedcontext> thunk = filters.reverse().aggregate(continuation,
(next, filter) => () => invokeactionmethodfilter(filter, precontext, next));
return thunk();
}
这段代码有点函数式的风格,不熟悉这种风格的人看起来有点难以理解。 用函数式编程语言的话来说,这里的aggregate其实就是foldr,
foldr::(a->b->b)->b->[a]->b
foldr 接受一个函数作为第一个参数,这个函数的参数有两个,类型为a,b,返回类型为b,第二个参数是类型b,作为起始值,第三个参数是一个类型为a的数组,foldr的功能是依次将数组中的a 和上次调用第一个参数函数(f )的返回值作为f的两个参数进行调用,第一次调用f的时候用起始值。对于c#来说,用面向对象的方式表示,是作为ienummerable的一个扩展方法实现的,由于c# 不能直接将函数作为函数的参数传入,所以传入的是委托。说起来比较拗口,看一个例子:
复制代码 代码如下:
static void aggtest()
{
int[] data = { 1, 2, 3, 4 };
var res = data.aggregate("string", (str, val) => str + val.tostring());
console.writeline(res);
}
最后输出的结果是string1234. 回到invokeactionmethodwithfilters的实现上来,这里对应的类型a是iactionfilter,类型b是func<actionexecutedcontext>,初始值是continuation。假设我们有3个filter,[f1,f2,f3],我们来看下thunk最终是什么,
第一次: next=continue, filter=f1, 返回值 ()=>invokeactionmethodfilter(f1, precontext, continue)
第二次:next=()=>invokeactionmethodfilter(f1, precontext, continue), filter=f2
返回值:()=>invokeactionmethodfilter(f2, precontext,()=> invokeactionmethodfilter(f1, precontext, continue)),
最终: thunk= ()=>invokeactionmethodfilter(f3,precontext,()=>invokeactionmethodfilter(f2, precontext, ()=>invokeactionmethodfilter(f1, precontext, continue)));
直到 return thunk()之前,所有真正的代码都没有执行,关键是构建好了thunk这个委托,把thunk展开成上面的样子,应该比较清楚真正的调用顺序什么样的了。这里花了比较多的笔墨介绍了如何通过aggregate方法构造调用链,这里有一篇文章专门介绍了这个,也可以参考下。想象下,如果filter的功能就是先遍历调用f的executing方法,然后调用action方法,最后再依次调用f的executed方法,那么完全可以用迭代来实现,大可不必如此抽象复杂,关键是asp.net mvc对于filter中异常的处理还有一些特殊之处,看下invokeactionmethodfilter的实现:
复制代码 代码如下:
internal static actionexecutedcontext invokeactionmethodfilter(iactionfilter filter, actionexecutingcontext precontext, func<actionexecutedcontext> continuation) {
filter.onactionexecuting(precontext);
if (precontext.result != null) {
return new actionexecutedcontext(precontext, precontext.actiondescriptor, true /* canceled */, null /* exception */) {
result = precontext.result
};
}
bool waserror = false;
actionexecutedcontext postcontext = null;
try {
postcontext = continuation();
}
catch (threadabortexception) {
// this type of exception occurs as a result of response.redirect(), but we special-case so that
// the filters don't see this as an error.
postcontext = new actionexecutedcontext(precontext, precontext.actiondescriptor, false /* canceled */, null /* exception */);
filter.onactionexecuted(postcontext);
throw;
}
catch (exception ex) {
waserror = true;
postcontext = new actionexecutedcontext(precontext, precontext.actiondescriptor, false /* canceled */, ex);
filter.onactionexecuted(postcontext);
if (!postcontext.exceptionhandled) {
throw;
}
}
if (!waserror) {
filter.onactionexecuted(postcontext);
}
return postcontext;
}
代码有点长,首先就是触发了filter的onactionexecuting方法,这是方法的核心。接下来的重点是 postcontext = continuation(); 最后是onactionexecuted方法,结合上面的展开式,我们可以知道真正的调用顺序将是:
复制代码 代码如下:
f3.executing->f2.executing->f1.exectuing->invokeactionmethod->f1.executed->f2->executed->f3.executed.
那么,源代码中的注释 // need to reverse the filter list because the continuations are built up backward 的意思也很明了了。需要将filter倒序排一下之后才是正确的执行顺序。
还有一类filter是当异常发生的时候触发的。在invokeaction方法中可以看到触发它的代码放在一个catch块中。iexceptionfilter的触发流程比较简单,不多做解释了。唯一需要注意的是exceptionhandled属性设置为true的时候就不会抛出异常了,这个属性在各种context下面都有,他们是的效果是一样的。比如在onactionexecuted方法中也可以将他设置为true,同样不会抛出异常。这些都比较简单,不再分析其源代码,这篇文章比较详细的介绍了filter流程中出现异常之后的执行顺序。
最后说下action method的执行,前面我们已经得到了methodinfo,和通过data binding获得了参数,调用action method应该是万事俱备了。asp.net mvc这边的处理还是比较复杂的,reflectedactiondescriptor会去调用actionmethoddispatcher的execute方法,这个方法如下:
复制代码 代码如下:
public object execute(controllerbase controller, object[] parameters) {
return _executor(controller, parameters);
}
此处的_executor是
delegate object actionexecutor(controllerbase controller, object[] parameters);_exectuor被赋值是通过一个方法,利用expression拼出方法体、参数,代码在(actionmethoddispatcher.cs):
static actionexecutor getexecutor(methodinfo methodinfo)此处就不贴出了,比较复杂。这里让我比较费解的是,既然methodinfo和parameters都有了,直接用反射就可以了,为什么还要如此复杂,我将上面的execute方法改为:
复制代码 代码如下:
public object execute(controllerbase controller, object[] parameters) {
return methodinfo.invoke(controller, parameters);
//return _executor(controller, parameters);
}
运行结果是完全一样的。我相信mvc源代码如此实现一定有其考虑,这个需要继续研究。
最后附上一张函数调用图,以便理解,仅供参考。图片较大,点击可看原图。