全新的 ASP.NET 动态语言可扩展模型
介绍:为什么需要新的编译模型?
传统 ASP.NET 页面编译的步骤
1. Parsing.
对 .aspx 文件进行解析。
2. 构建 CodeDOM 树
根据解析结果构建出 CodeDOM 树(不依赖于具体语言的)
3. 代码生成
根据 @Page 指令中定义的语言,选择合适的 CodeDOM provider,
让这个特定的 provider 根据 CodeDOM 树产生派生类的源代码。
4. 编译
CodeDOM provider 编译生成的代码为 dll.
5. 执行
ASP.NET 加载产生的程序集,实例化其中的代码类,并用它来执行 HTTP 请求。
在使用 Codebehind 的情况下,会有一个 partial class 参与以上过程的运作。
.ascx 和 .master 的运作机制跟这个情形类似。
由以上的运作过程可以发现,如果需要其他语言支持 asp.net,则该语言需要
提供相应的 CodeDOM Provider,并且要能创建 System.Web.UI.Page 的子类。
由于 IronPython 是非强类型的,这样的工作显得意义不大。
因此需要通过新的途径来实现 IronPython 对 asp.net 的支持:
“不需编译”的页面
asp.net 2.0 已经支持了不需编译页面模型。声明方法如下:
<%@ Page CompilationMode="Never" %>
如果不指定该属性值,则默认情况下是 "Auto".
不需编译的页面跟普通页面的区别是,他们是数据驱动的,因此,通常会具有
更好的性能。
因为不编译,这种页面有个限制:
即不能包含用户代码。只可以包含静态 HTML 和服务器端控件。
而对动态语言的支持也正是从这里入手,从中解除这些限制,并提供对动态
语言的支持。
不需编译的页面如何工作
1. Parsing
2. 控件生成器(Control Builder)树的构建
拿 <asp:textbox> 来说,这时解析的结果并不是生成一小段定义该 TextBox
控件的代码,而是创建一个相应的控件生成器节点。
3. 执行
ASP.NET 请求控件生成器树进行初始化,包括其中的控件。
在这种情况下,并不会产生 Page 类的子类。响应请求的是 Page 类本身(也可能
是一个自定义的基类,如果指定了 inherits 属性)。
支持动态语言的全新 ASP.NET 模型
该模型是从不需编译的 asp.net 模型修改而来。
首先,因为不需编译的模型在发现用户代码时就会出错,因此要对这个行为进行
修改,而这是在 System.Web.dll 中实现的。当你安装了该动态语言的支持时,就
使用了一个新版本的 System.Web.dll
对 ASP.NET 的改变是 PageParserFilter 这个API. 在这里我们给了外部代码得以执行
的一个钩子。
在新的模型中,我们在 web.config 中注册了 PageParserFilter 类,以便自定义 parsing
的行为,并在不需编译的页面中开启对用户代码的支持:
<pages
pageParserFilterType="Microsoft.Web.IronPython.UI.NoCompileCodePageParserFilter"
... />
PageParserFilter 的功能如下:
1. 如果页面使用了动态语言(当前只支持 IronPython),它需要确认页面是否继承
自某个基类。
2. 如果 parser 遇到了一个代码片段(<% ... %>, <%= ... %>, 或 <%# ... %>),
它将这些代码片段替换为特定的控件,而这些控件包含其中的代码。这样,从
asp.net 的观点来看,的确是没有代码了,只是代码片段变成了控件。这个做法
使得我们突破了不需编译的页面不能包含代码这个限制。
3. 如果 parser 发现一个事件处理函数的定义,(如 onClick="MethodName"),同样,
也将他替换为一个特定的控件。
4. 如果 parser 发 <script runat="server">,则将此元素当作页面级别的属性处理。
在接下来的步骤中,将会把它用字符串的方式传递给自定义基类。
自定义的 HTTP Module
该模型中实现了一个自定义的 HTTP Module, 需要在 web.config 中注册如下:
<httpModules>
<add name="PythonModule"
type="Microsoft.Web.IronPython.DynamicLanguageHttpModule" />
</httpModules>
注册这个 HTTP module 的作用在于,能够及早的挂钩到 application domain cycle 中,
并向动态语言环境中注册各种组件。(application domain 是当前进程中的一块区域,
当前 Web 程序中的所有代码都在其中执行).
这个 HTTP module 还提供 Global.asax 文件对动态语言的类似支持,这个会在稍后讨论。
自定义的页面基类
新的模型中,所有页面继承自一个叫做 ScriptPage 的基类。而该类继承自
System.Web.UI.Page. 类似的,我们有用户控件的基类 ScriptUserControl 和母版页的基类
ScriptMaster
新的模型支持哪些页面特性
原有的大部分特性基本都支持,只是其工作原理不同。
Application File
新的模型支持类似 Global.asax 的文件,但其名称是 Global.ext( ext 是语言特定的,
比如 IronPython 就是 Global.py)
另一个不同点是,该文件只能包含代码,不需要包含 <%@ %> 或者 runat="server"
这样的声明。
比如 Global.py 可以这样写:
def Application_BeginRequest(app):
app.Response.Write("Hello application!");
App_Script 目录
该目录类似于 asp.net 2.0 的 App_Code 目录。其中可以包含动态语言的文件。这里
的文件可以被整个应用程序所调用。
Generic HTTP Handlers
动态语言的 asp.net 程序中可以包含一个和传统 asp.net 中 .ashx 文件类似的 HTTP
handler, 但和 Global.asax 一样,它也稍有不同。
具体来说,handler 只能包含代码,而没有声明性的语句。
其命名方式是:
Web_name.ext
比如 IronPython 的一个 handler 可以这样命名:
Web_MyHelloHandler.py
注意 "Web_" 这个前缀是规定的,用以识别一个 HTTP handler.
动态语言的 handler 必须包含一个 ProcessRequest 方法,这类似于 .ashx 文件的
IHttpHandler.ProcessRequest 方法。示例如下:
def ProcessRequest(context):
context.Response.Write("Hello web handler!")
不支持:Web Services
主要原因是 Web Service 的架构只能用基础的 .net framework 类型,而动态语言来
创建他们是不容易的。并且 Web Services 的方法需要加上类似 [WebMethod()] 的标签,
在动态语言里也没有类似的语法。
在新的模型中,代码是如何被处理的?
每一个用户代码的片段会被当作一个独立的实体来处理。下面具体分析不同类型的
用户代码是怎么被处理的:
<script runat="server"> 元素中的代码:
在传统模型中,这里面的代码会被生成为页面类的一部分。而新的模型不会产生新类,
asp.net 直接实例化在 inherits 属性里指定的类(ScriptPage)。其实这里 "inherits" 的含义
已经不准确了。
<script> 元素中的代码会变为 ScriptPage 类的一些附加代码,也可以大致理解为类似于
partial class 的机制。
下面演示一个具体的例子:
<script runat="server">
def Page_Load():
Response.Write("<p>Page_Load!</p>")
</script>
这段代码并不会成为任何类的一部分。实际的情况是,页面类的成员会被 asp.net 注入
(injected) 到环境中,以便这里能调用他们。(比如使用 Respnose 对象)
从实践的角度讲,你可以当作这个方法是页面类的一部分,虽然实际上不是。
用 IronPython 的术语来说,代码块中的代码存在于模块(module)中。通常一个页面类,
或用户控件,或母版页都会对应于一个 module.(注意是每个页面一个对应的 module,
而不是每次 HTTP 请求产生一个)。
再看另一个例子:
<script runat="server">
someVar = 5
</script>
注意这个变量是模块级的,而上面提到模块是每个页面对应一个,所以这个变量也是。
这跟 asp.net 的静态变量语义类似。
后端代码文件(Code-Behind Files)
新的模型也支持后端代码的方式,但和传统模型的原理不同。
在新的模型中,后端代码中没有类定义,方法直接定义在文件中。这和刚才提到的
<script runat="server"> 块中的代码没有区别。
后端代码的文件名可以命名为类似 MyPage.aspx.py 这样的。
代码片段(Code Snippets)
代码片段表达式 (<%= ... %>), 和语句(<% ... %>) 同样在 page 伴随的 module 中执行。
因为这个原理,代码片段中就可以自由访问 module 中定义的任何方法。
例如:<%= Multiply(6,7) %>, 其中 Multiply 在后端代码中定义。代码片段中甚至可以
访问 Page 类的成员。如:
<%= Title %> 可以显示当前页面类的标题。
数据绑定表达式
数据绑定表达式虽然也是代码片段的一种,但有必要单独说明一下。因为在 IronPython
中,数据绑定特别有意思,他比普通的 asp.net 绑定更自然。
比如:在 GridView 的模版列中,我们可以用 <%# Eval("City") %> 的语法来绑定 City 字段
的内容。这很平常,但需要通过 Eval 语法来调用显得挺别扭的~
新的模型中,我们只需要这样:<%# City %>,是不是很兴奋?
这里的 City 其实是动态语言的一个真实代码片段,而不是原先 Eval 方法的一个字符串
类型的参数。所以,这里就有很大的发挥空间。比如,你可以这样:
<%# City.lower() %> 来显示为小写字母。
这种语法的支持是由动态语言的后期绑定估算(late-bound evaluation)特性所实现的。
虽然 City 的含义在 parse 阶段尚不可知,但在运行时动态语言引擎却能将他绑定到正确
的对象。
动态注入器机制(Dynamic Injector Mechanism)
动态语言相比于静态编译语言的另一个优势是注入器的机制。用例子来说明:
假设你需要从下列页面的查询字符串中获取一个值:
http://someserver/somepage.aspx?MyValue=17
在 C# 中,你需要这样:
string myValue = Request.QueryString["MyValue"];
而动态语言中这样就可以了:
myVar = Request.MyValue;
为什么能这样写呢?在新的模型中,我们注册了一个特殊的对象,叫做注入器(injector).
这个注入器的作用是,就好像它对动态语言引擎发出如下指令:"如果发现一个表达式 SomeObj.SomeName,
而 SomeObj 是一个 HttpRequest 对象,并且 SomeName 不是 HttpRequest 的一个属性,
则让我来处理他,而不是抛出失败"。
而这个注入器处理表达式的方式就是通过调用 SomeObj.QueryString["SomeName"].
同样的注入器机制还实现在其他场合。比如,
C#: SomeControl.FindControl("SomeChildControl")
IronPython: SomeControl.SomeChildControl.
注入器的机制是可扩展的。所以,如果你有个自定义的集合通过字符串做索引的话,
你可以实现自定义的注入器来简化语法。
虽然不是革命性的变化,但像上面提到的注入器的特性,和简化的绑定表达式会使得
开发 Web 程序变得更轻松。
动态代码的编译
以上提到,新的模型采用了不编译的页面模型,这也许会带来误解,让你觉得页面是解释
执行的,其实不是这样。
具体的解释是:术语 "不编译(no-compile)" 其实指的是 CodeDOM 方式的静态编译。
而新的模型中,动态语言的代码是由动态语言引擎就地编译的(being compiled on-the-fly)。
新模型的好处
更快的页面初始化处理
传统的页面在第一次访问某页面时,会有一个复杂的 CodeDOM 方式的编译过程,并且会
加载一个独立的进程(比如 csc.exe) 来进行编译,显得非常慢。
而对于不编译的页面而言,最昂贵的步骤是解析(parsing). 所以页面会执行的更快。
更好的性能
传统的模型会将页面编译为程序集,并加载到 application domain 中执行。但程序集一旦
被加载,则不能被卸载掉。所以,如果你有一个非常大的站点,有很多的页面,则越来越多
的程序集会被加载,甚至会导致内存不足。(out of memory)
asp.net 采用了一系列手段来减轻这个负担。首先,它可以将多个页面批量编译到一个
程序集中,以减少被加载的程序集数量。这的确有些帮助,但带来了非常高的复杂性。
这只是延迟了问题的解决。在某种情况下,asp.net 会卸载整个 application domain,并
重新启动一个来运行应用程序。但这是一个非常重量级操作,因为很多程序集要求在短
时间内被重新加载。
与之相反的是,新的模型完全不存在这个问题,因为它根本不产生程序集。当然,这样
处理页面会有一些额外的代价,但是垃圾回收器会回收这些占用的内存。
尽管动态代码通常会被编译为 IL, 这种编译和静态编译却是很不一样的。动态语言编译到
IL 采用的是公共语言运行时(common language runtime) 的一个特性,叫做轻量级代码生成
(lightweight code generation, LCG). 这个特性可以使得编译代码时占用的内存,会在不再
使用时被回收-- 不需要重启整个 application domain.
看看数字
新的模型究竟带来多大的性能收益?新模型和传统模型的差别是非常明显的。
在静态编译模型下,一个有 10,000 个 asp.net 页面的网站会对服务器造成非常大的负担。
(具体还取决于硬件配置)。相比而言,在新的模型上运行一个超过一百万个动态页面
的网站,服务器并未感受到很大的压力!
需要说明的是,asp.net 静态编译模型对大多数项目都工作的很好。只有对特别大的项目
才会感觉到这种性能的限制。
降低了的复杂度(Reduced Complexity)
前面提到,为了提高静态编译的性能,asp.net 对页面进行批量编译。但要达到这一点却
非常有技巧性,而且这种优化已经大幅度的提高了整个编译系统的复杂度。
简单的描述一下这种复杂度。在很常见的情形下,你有很多个页面和很多个用户控件。
为了能批量编译他们,我们首先要弄清楚他们的依赖树。(比如:页面A引用了用户控件B),
因为被依赖的文件必须在当前编译的页面之前被编译。并且我们需要处理使用不同语言
书写的页面。考虑一下,如果一个老的程序集已经无效了,但却无法卸载时怎么办呢?
如果两个页面使用了同一个类名怎么办?批量编译他们会导致编译错误。类似的问题还有
很多,但你大概能理解这种复杂度了吧。
上面说这么多并不是抱怨静态编译模型是多么难实现,而是为了和新的模型做对比。
因为总之只处理页面自身,并且不需要考虑老的程序集是否已经在当前 application domain
中加载,我么可以显著的降低复杂度。我们希望这能带来更加稳定的产品,和更少的
bugs. 总之,简单的总是更好!
运行时性能
总体来说,静态编译的语言,比如 C#,会比动态语言如 IronPython 更快。因为动态语言
通过晚绑定的方式执行。就是说在动态语言中 Object.Perperty 这个表达式,Property 的
含义在编译时是不会解析的,而是在运行时。解析表达式需要某种形式的查找,这本质上
就会比静态编译的方式要慢。静态编译是在编译期间就知道了这个值。
然而,我们的问题不在于动态语言是否能和静态语言跑得一样快,而是 asp.net 页面用
动态语言编写是否可以得到比较好的性能。经过测试,IronPython 书写的 asp.net 页面
运行的性能相当好。原因是,执行用户的代码(不管是静态还是动态的)只是 HTTP
请求处理工作中很小的一个部分。即使更多的时间花费在用户代码上,总体的请求
时间的差别也是小到可以忽略的。
当然你可以轻易的书写一个页面来执行复杂的运算,说明这个观点是错的。但其实
大部分网站页面所执行的代码都不是 CPU 要求很高的。实际的网页逻辑,大部分都
花费在处理输入,设定数值到控件的属性,查询数据库等。
总之,对于大部分你需要建立的 Web 应用程序而言,使用动态语言并不会带来多少
负面影响。