BT

您是否属于早期采用者或者创新人士?InfoQ正在努力为您设计更多新功能。了解更多

半静态语言–原理和价值分析

| 作者 何坤 关注 0 他的粉丝 发布于 2010年12月10日. 估计阅读时间: 28 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

【摘要】动态类型语言在企业开发和互联网开发中应用广泛,而其弱类型的内在特点使其在这些业务复杂的应用开发中存在很多缺点:无法静态检查,程序不健壮,测试成本高;缺乏一些敏捷开发功能如IDE内实时验证、代码提示、代码重构等。为此,本文提出半静态语言,它的基本原理是两阶段模型,开发时运用变量类型声明进行类型检查,运行时采用解释执行的方式。并引入“基于注释的扩展声明指令”,与现有解释器保持完全兼容。 半静态语言它结合了动态语言和静态语言的优点,同时满足企业开发中的灵活性、健壮性与敏捷开发的需求。

【关键词】Semi-static Language, Dynamic Typing, Static Typing, Velocity

引言

动态类型语言在企业开发和互联网领域应用广泛,如Ruby ,Velocity, Python等。 动态类型语言在运行时进行类型推断,以解释方式执行,修改即生效,开发灵活性高;而静态类型语言(如:Java,C/C+/C++) 在执行前做类型检查,需要编译运行,对于互联网前端开发不够灵活。

因此,许多大型互联网站选择 Freemarker, Velocity这样的动态模板语言作为页面开发语言,在一定程度上满足了前端敏捷开发的需求。

然而,对于大型电子商务网站,不仅具有一般互联网需求频繁变更的特点,更显著特点则是业务繁多,业务模型和业务关系复杂。 因此,在此类应用开发中,Velocity 的开发也遇到了一些的问题。

前端模板开发问题

  1. 降低软件质量
  2. Velocity是弱类型动态语言,运行时才能检查出类型错误。由于动态类型等特点,有的错误在遇到特定参数时,才能激发执行路径,软件质量不能很好的保证。

  3. 测试成本高
  4. 由于无法像静态语言一样,在运行前进行类型检查,因此软件的测试周期长,测试成本高。

  5. 开发不敏捷
  6. 缺乏一些敏捷开发功能如IDE内实时验证、代码提示、代码重构等。虽然能修改即生效,但对于企业级开发,效率较低。

  7. 维护性差
  8. 对于一个大型系统,在重构业务模型(Java Model)或代码时, 无法知道哪些Velocity模板会受到影响;常常需要花费大量时间搜索相关模板,然后修改、测试。例如:笔者所在公司的一个基础产品升级,由于受影响模板众多,重构复杂,项目评估达上千人日。

这些动态语言天生的缺点在企业级和大型网站应用中非常突出,严重的影响了开发质量和开发效率。因此,在技术上亟待一种新的高质量、高效率的开发技术。

静态语言的优势

综合考虑后,我们发现动态类型语言(Dynamic Language)“解释执行方式和修改即生效”的最大优点仍是不能舍弃的。必须从问题出发,找到一条平滑的线路来解决问题。

遇到上述问题时,我们不由自主的会赞美Java的优点:

  1. 静态语法和静态类型实时检查。
  2. 如果赋值类型不匹配,方法不存在,参数类型错误等信息能马上在IDE中显示;

  3. 代码提示:
  4. 调用属性,方法时能代码提示,开发非常高效;

  5. 代码热链接:
  6. 通过变量和类名热链接到对应的Java类;

  7. 代码重构:
  8. 修改一个Java类时,受影响的Java代码会被实时重新验证,马上会显示红色的错误; 更强大的是重构,对Java类,方法敏性重命名,会自动修改所有相关代码中对它的引用。

Java等静态类型语言的这些优势就是解决问题的方向。那为什么动态语言不能做到这些呢? 原因在于动态语言的根本特点是变量无类型(即弱类型特点),类型在运行时推断,这使得它无法在开发阶段进行类型检查。

那如何将动态语言和静态语言的优点结合呢?答案就是半静态语言。

半静态语言(Semi-Static Language)

4.1 定义

半静态语言,严格说应该是静态化类型的动态语言(Statically Typed Dynamic Language)。它是这样一种语言:以静态方式开发,以解释方式执行;通过变量显式声明或隐式声明,运行前可对变量类型进行推断和验证。

静态语言,动态语言和半静态语言的特点对比分析如下:

语言类型

优点

缺点

举例

适用场景

Static Language

强类型,运行前类型检查,程序健壮

对Java等支持反射的语言,可实现代码提示,重构等敏捷开发特性

需编译运行,发布慢

无法快速响应需求变化

Java

C/C++

企业级后端开发

大型互联网后端开发

Dynamic Language

灵活性高,修改即生效

快速响应需求变化

弱类型,运行时类型检查,程序不健壮,测试成本高

PHP

Ruby

Velocity

业务简单的小型互联网前端开发

Semi-Static Laguange

开发时(Devtime)强类型,程序健壮

运行时(Runtime)弱类型,修改即生效,快速响应需求变化

   

业务复杂的企业级开发和大型互联网前端开发

半静态语言集合了静态语言和动态语言的优点,更适合企业级和大型互联网开发,例如:电子商务,ERP,金融,保险等。

4.2 技术原理

4.2.1 范例

为了实现目标,需要在动态类型语言基础上,引入变量声明技术。因此本质上,半静态语言也是一种声明式语言(Declarative Language), 这一点与静态类型语言一样。

以Velocity模板语言为例:

当前Velocity Template编程代码范例如下:

[Code 1] showBuyProducts.vm

<HTML>>
Hello $customer.Name
<table>>
#foreach( $product in $buyingProducts )
   Buy: $product.Name,  Price: $product.Price, 
#end
</table>>

该模板执行后,HTML页面上将用 $customer.Name 显示“客户名称”,循环显示该客户购买的每个产品的名称和价格。在Velocity中,运行时通过Velocity Context传递变量$customer和 $buyingProducts,而开发时这两个变量是未定型的(Untyped,或者说都是Object类型)。

为了实现静态化开发,引入变量声明,在模板顶部对变量$customer,$buyingProducts进行显式类型声明。变量声明指令为“##$”。

格式为:

 ##$ <Type> <var1[,var2[,[…]]]> 

带有变量声明的半静态模板代码为:

[Code 2] showBuyProducts_static.vm

##$ com.abc.crm.Customer customer
##$ com.abc.saling.Product product
##$ List<Product> buyingProducts

<HTML>
Hello $customer.Name
<table>
#foreach( $product in $buyingProducts )
   Buy: $product.Name,  Price: $product.Price, 
#end
</table>

上述代码中,指定了变量customer的类型为 com.abc.crm.Customer,变量buyingProducts 的类型为Product泛型集合。由于 "##"是Velocity的注释指令,因此 "##$" 在Velocity Engine解析(Parse)和渲染(Render)时不会与现有语法冲突,Velocity引擎能正常执行,从而保证了兼容性。

4.2.2 动态语言一阶段模型

在动态类型语言中,只有一个运行时(Run Time)阶段,运行阶段由解释器(Intepreter)来对源代码进行解析(Parsing)、执行(Evaluation)产生执行结果。过程如下:

由于动态语言无类型的特点,在解析步骤中产生的抽象语法树(Abstract Syntax Tree,AST)所有变量被存储为统一的类型,例如JavaScript,Velocity中变量都作为 Object 类型。在执行步骤,一般由类型推断系统(Type Inference System)负责根据变量的实际值动态判断变量的类型,并判断函数、方法或属性调用是否正确,由解释器进行执行或计算,从而产生结果。

4.2.3 半静态语言两阶段模型

而半静态语言,分开发时(Develop Time)和运行时(Run Time)两个阶段,两个阶段互不干扰。

  1. 开发时阶段。
  2. 开发时进行类型检查。一个“编译器”,更严格说是类型化解析器(Typing Parser)负责对源代码进行解析和类型检查,然后输出检查结果。“变量声明”是类型检查的必要条件。检查结果包含类型检查失败的错误信息和警告信息,类似于 Java编译时的错误信息。

    与静态类型语言不同,此编译器不输出机器代码或字节码,只输出类型检查错误信息。

  3. 运行时阶段。
  4. 此阶段中,源代码仍由解释器以解释方式执行,同动态语言的解释执行过程。

    半静态语言的两阶段模型如下图所示:

需要指出的是,运行时阶段仍采用无类型解析器(Untyping Parser), 是一个类型推断系统。而开发时采用的是一个新的类型化解析器(Typing Parser), 是一个类型检查系统(Type Checking System)。

4.2.4 开发流程

半静态语言的开发流程涉及5个步骤:

  1. 编码
  2. 编译(类型检查).
  3. 半静态语言的编译与静态类型语言很不相同,它的编译只进行类型检查,不产生机器码或字节码。因此,半静态语言的编译可以称为“检查”(Checking).

    在这个步骤中,如果代码存在类型错误(Error),编译失败,那么你必须退回到步骤1)修改代码bug,直到代码编译正确。

    编译过程还可以产生警告(Warning),程序员可以有选择的忽略。
  4. 测试
  5. QA 执行功能测试,集成测试和系统测试。

    如果测试失败,必须退回到步骤 1)。

  6. 发布
  7. 将代码发布到生产环境

  8. 执行
  9. 最终用户访问用半静态语言开发的应用功能。

从上面的开发流程可见, 开发时阶段覆盖了步骤 1)、2), 运行时阶段覆盖了步骤 3)、4)、5).

为了保证只有编译合法的半静态语言程序在生产环境运行,需要有以下两条约束规则来保证:

  1. 代码编译合法后,才能提交到测试阶段;
  2. 测试正确的代码才能发布上线。

由于半静态语言仍用解析器运行,理论上代码仍具有修改即生效的特点。但从软件质量保证角度,这个缺点应该规避。因此上线后的代码不允许未经编译、测试的随意修改。

4.2.5 类型检查系统和原理

半静态语言的类型检查系统中的核心组件编译器Compiler(或称为Checker),它本质上是一个类型化解析器。编译时,该系统采用类型检查算法(Type Checking Algorithm);而在运行时阶段,仍由解释器执行代码,采用类型推断算法(Type Inference Algorithm)。

半静态语言的类型检查基本原理是,根据变量声明对源码进行解析、类型检查和语义检查,输出检查结果。这个系统中类型检查系统的基本原理如下图所示:

我们使用一个命令行工具 vmcheck 来编译半静态语言代码。格式为:

Format: vmcheck templateFile

以前面的声明式Velocity源码为例,类型检查系统包含以下几个基本规则和检查点:

  1. 变量是否声明;
  2. 如果变量 $customer 未声明,编译错误如下:

    Error: line:2,column:7,variable $customer not declared !
  3. JavaBean的属性和方法是否存在
  4. 如果com.alibaba.saling.Customer类没有属性 'Name' , 编译错误如下:

    Error: line:2, column:7, property 'Name' not found for $customer.

    如果com.alibaba.utils.CurrencyUtil 类没有方法 'convert' , 编译错误如下:

    Error: line:6, column:22, method 'convert' not found for $currencyUtil.
  5. 方法调用的参数匹配;
  6. 3.1) 如果这样调用 'convert' 方法:

    $currencyUtil.convert()

    则产生如下编译错误信息:

    Error: line:6, column:22, insufficient parameters for  method call 'convert' .

    3.2) 如果这样调用 'convert' 方法

    $currencyUtil.convert( $customer , "##.##" )

    则产生编译错误信息:

    Error: line:6, column:22, parameter type mismatched of $customer for method call 'convert' , Double is required.
  7. 特定语句的类型匹配,如条件,循环语句:
  8. 如果有下面的复制语句调用

    #set( $customer.Name = $product.Price) 

    则产生编译错误信息:

    Error: line:11, column:5, type mismatched of assignment statement.

    'if', 'foreach' 等语句使用的类型匹配规则类似。这与Java等强类型语言一样。

    1. 集合泛型的类型匹配

    对于Java语言,JDK5+支持泛型特性。因此,类型检查也需支持泛型。对于以下代码

    ##$ List buyingProducts
    $buyingProducts.add( $customer)

    编译错误如下:

    Error: line:12, column:5, parameter type mismatched of $buyingProducts for method call 'add' , 'com.alibaba.saling.Product' is required. 

    As for the previous Velocity code snippet [Code 1], after executing 'vcheck' command on console,

4.3 变量声明

变量声明就是对变量的类型进行声明。变量声明根据放置的地点分为两种,显示声明(Explicit Declaration)和隐式声明(Implicit Declaration)。

  1. 显式声明
  2. 显式声明采用特殊指令(Directive)或语句(Statement),在源码中对变量进行类型声明。

    显式声明通常的格式为:

    <Declaration Directive>  <Type>  <varList>

    为了保持与运行时解释器的兼容性,我们引入一种“基于注释的扩展声明指令”技术。以Velocity模板语言(VTL)为例, 在Velocity注释指令“##”上扩展“##$”指令用于变量声明。如下例所示:

    [Code 3] showBuyProducts_static.vm

    ##$ com.abc.crm.Customer customer
    ##$ List buyingProducts
    ##$ String flag, sss, abc

    对于其他动态类型语言,同样使用“基于注释的扩展声明指令”来实现兼容性的半静态语言。

    Language

    Comment Instruction

    S2L Declaration Instruction

    Velocity

    ##

    ##$

    Javascript

    //

    //$

    Ruby

    #

    #$

    Python

    #

    #$

  3. 隐式声明
  4. 隐式声明不用在源码中编写声明语句,而从配置文件或其他地方分析变量声明。例如,使用Velocity进行Web App开发时,如果需要直接频繁操作request,response,session等Servlet容器对象,编译器可以将它们作为内置变量,使用隐式声明。如下表所示:

    Built-in variable

    Type

    request

    HttpServletRequest

    response

    HttpServletResponse

    session

    HttpSession

    application

    ServletContext

    以下代码使用隐式声明变量 request, session,

    <html>
    <body>
    Hello, $request.getParameter("username") ! <p/>
    Your logged in at $session.getAttribute("loginTime") last time.
    </body>
    </html>
    

    这段代码看起来,对现有Velocity语法没有任何扩展. 但实际上,在编译时,编译器使用内置变量对源码进行类型检查。

    如果编写了一段错误的调用,例如:

    $session.getParameter("loginTime") 

    则编译器输出一条“方法不存在的”错误信息:

    Error: line:12, column:5, method 'getParameter' not found for $session!.

4.4 语法约束

半静态语言基于某种动态类型语言进行实现,但它在语法语义上更接近与静态类型语言。在这两个端点,存在一些矛盾的地方,比如:变量动态定型,ducking type等。因此,半静态语言需要有语法约束:

  1. 变量先声明,后使用
  2. 变量在作用域scope内置能声明为一个类型;
  3. 禁止Ducking type 也就是说, 动态语言的无继承多态特性不允许使用,因为这与静态类型系统是冲突的。

如果违反这几个规则,编译器会产生相应的编译错误。以ducking type 为例(Ruby支持,而Velocity等Java系列脚本不支持), 如果尝试访问一个不存在的方法,则会产生下面的错误。

Error: line:12, column:5, method 'quack' not found for $dog. 

而在Ruby中,只要 dog 存在 quack 方法,代码运行是正确的。

4.5 半静态语言组成模型和实现方式

半静态语言本质上是动态语言思想和静态语言思想的结合的产物。一种基本的半静态语言实现,核心功能是在运行前进行类型检查和语义检查。其组件集合 SS包括:

  1. 一种静态类型语言S,S以编译方式运行;
  2. 一种以S语言为基础的动态类型语言 D。D以解释方式由P执行,解释器 P 由S 编写;
  3. 在语言D的语法集合上扩展变量声明语法,新语法集合名为 SD ;
  4. 用语言S对解释器P进行扩展,实现 SD 的类型编译器C;
  5. 开发时,遵循SD语法集合的代码由 C 进行类型检查;
  6. 运行时,遵循SD语法集合的代码由 P 进行解释执行。

因此,新的半静态语言SS是基本组成是:新语法集合SD和类型编译器C.

SS = SD + C

举例:

Java 是一种静态类型语言,运行前进行编译和类型检查;

Velocity是一种基于Java的动态模板语言,通过 Velocity Engine以解释方式运行;

基于Velocity实现半静态语言的方式为:为Velocity基本语法增加变量声明指令(语句),基于Velocity 解释器 实现类型编译器 ,负责在开发时对模板进行类型检查。

实践中,Java体系的动态类型语言一般与Java语言天生的结合使用,应用广泛。以它们为基础,很容易通过扩展方式实现类型编译器,进而实现半静态语言。 例如Freemarker,Groovy,JRuby,Bean Shell等。其他动态类型语言也可以基于此原理设计半静态语言,如:Python,Ruby。

IDE敏捷开发(Agile Development in IDE)

对于Velocity,Freemarker这类动态类型语言,它们基于Java等强类型语言,在模板内能直接操作传入的Java对象。由于Java等语言有反射(Reflection)机制。因而,除了静态类型检查的基本功能,可以在类型检查和反射技术基础上,实现一系列IDE敏捷开发功能。包括:

  1. 代码提示:编辑器内的Java对象的属性,方法代码提示;
  2. 参数提示:编辑器内的Java对象的方法参数提示;
  3. 全量构建和增量构建:Java类修改对相关Velocity模板的增量检查;
  4. 代码重构:修改Java属性或方法名称,自动批量修改相关模板中所有对应类型的JavaBean属性或方法名称。

其中3),4) 功能对于大型系统的维护和重构价值尤为明显。以上这些敏捷开发功能可独立实现或结合集成开发环境(IDE)如Eclipse插件来实现。

结论

通过以上分析可见,半静态化语言结合了静态语言和动态语言的优点,能很好的解决动态语言编程的开发质量和开发效率问题。半静态化语言保留了动态语言的灵活性优点,同时达到了静态语言在开发时强类型检查优势,能有效提升程序健壮性,减低测试复杂性和测试成本。通过与IDE结合,实现代码提示,代码重构等敏捷开发功能,有效提升动态语言的开发效率。在企业级应用和互联网应用开发中有着良好的应用价值。

参考资料

[1] Gordon Plotkin. A Semantics for Static Type Inference. (1992). http://homepages.inf.ed.ac.uk/gdp/publications/Stat_Type_Inf.pdf

[2] Velocity Template Language Reference. Apache Software Foundation. (2010)

[3] Michael Furr. Static Type Inference for Ruby. (2009 ) http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.144.5525&rep=rep1&type=pdf

[4] http://en.wikipedia.org/wiki/Duck_typing

关于作者

何坤,Raymond He,阿里巴巴技术部中文站架构师。多年JavaEE领域开发经验, 喜欢钻研崇尚创新。专注敏捷Web框架设计,敏捷Java开发技术和分布式Java系统。2009-2010作为Webx2.5项目架构师,推进中文站Web框架敏捷化升级、开发模式敏捷化等工作,通过一系列技术创新,将中文站开发效率每人每天平均节省30-90分钟。目前是阿里巴巴中文站开发效率领域负责人,正在研究半静态语言及其在大型网站的应用。


感谢张逸王瑜珩对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

不错 by x w

想法不错

很错 by Yu yum

楼主,velocity只是模板语言而已,就算再动态,再duck typing,能上升到动态语言的层面和ruby/python同台对比么?更不用说和c/cpp/java比较了,有啥意义呢?

正所谓骑白马的未必是王子,也可能是唐僧,会海豚音的未必是靓颖,也可能是健翔,请楼主不要搞错了啊。

而就文中所提到的问题而言,我认为"最好"的“半静态”语言当属jsp。

Re: 很错 by Yu yum

那么为什么人们不用jsp,而纷纷采用velocity呢? jsp太“语言”了,开发者常常一不小心就把逻辑和展现混淆到一起。。。

所以,让模板语言做该做的事情,还其本来面目吧。。。

阿里的半静态语言什么时候可以开源出来 by 曹 云飞

看起来阿里的半静态语言已经实际应用了。如果能够提炼成一门语言,开源出来,是件很好的事情啊

Re: 不错 by 杨 航

集中静态与动态的优势,语言发展的又一蹊径

为什么不试试scala by w NO777

如题

关于$customer如果错写成$custamer如何检查的问题 by zhang frank

在展示层还是倾向与原生态HTML by liu andy

比如开发一个树,对应的demo.jsp 如下,只需要通过id="demoDbTree"就可以完成数据的绑定,不需要学更多的语法:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<div style="padding: 10px;">
<div>这棵树直接和后台数据库的表绑定</div>
<div id="demoDbTree" style="padding: 8px; border: 5px solid #ddd;"></div>
</div>

对应的 demo.xml 描述文件:

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="/xsd/default/simple.xsd">
<components>
<dbTree name="demoDbTree" containerId="demoDbTree" tableName="simple_department"
parentIdName="parentId" idName="id" textName="text" width="300">
</dbTree>
</components>
</page>

实现原理:

Web应用中,无论服务器端采用(Java EE或.Net),客户端的请求(Request)经Web或应用服务器解析后,最终返回客户端的响应(Response)内容主体都是HTML(含Javascript脚本、CSS等)。由此,就提供了解决问题的契机,那就是在响应内容返回客户端(/浏览器)之前,“拦截”响应,解析响应HTML,并进行“再处理”,由此,可以保留HTML和HTTP的原生态。

</?xml></%@>

Re: 很错 by 何 坤

本文不是拿 Velocity 和 Ruby,Python 作对比,而是希望在这些动态类型语言基础上研究如何实现静态类型检查系统,解决它们在开发质量和开发效率两方面的问题。Velocity只是作为一个演示的 Demo,以此为例阐述基本原理,这种原理可以运用于其他语言。

JSP的化,如果用 <% bean.sayHello("jacky");%> 这样的 scriptlet,可以说是接近半静态语言的。我们都知道scriptlet 带来的问题是什么,因此才会有JSP Tag规范,Template语言等的诞生。</%>

Re: 在展示层还是倾向与原生态HTML by 何 坤

举的例子没错。 JSP Tag系的解决方案。
这与本文的介绍的内容不冲突,都是要解决不同的现实问题。 比如:Ruby的静态类型推断系统就是要解决Ruby 的弱类型问题(我的参考资料: Michael Furr. Static Type Inference for Ruby. (2009 ) citeseerx.ist.psu.edu/viewdoc)。

其实java就是你想要的 by sen firefly

作者文中把java列为静态语言是不合适的,其实C#和java都应该属于一种半静态的语言。他们很明显的分为两步
1. 开发时
2. 运行时
java开发时出来的结果是class文件,C#也是一种二进制的虚拟机指令码。
运行时,虚拟机执行这些指令码。在运行时你进行指令码的替换当然就是新指令码生效了。当然也可以直接产生代码,在运行时直接进行替换;但是之前基于一种性能的考虑才有一种中间码的产生(虚拟机解释的时候也是先翻译成中间代码,何不把这些代码直接作为一种输入呢?省去了每次装换的效率牺牲)

基于以上,无论是运行时直接解释java代码还是运行时解释jvm指令,java的表现都是一种半静态化的语言。

这篇文章更像是一个随想 by 刘 松

个人觉得这个方向是对的,从编程语言来看,类似scala的具备类型推断、模式匹配的现代化静态语言能贴近动态语言的开发效率(在支持动态化编程和简化基础语法集上多做一些工作就更好了),或者多关注象groovy++这种完全契合作者动静结合理念的东西。

关注 by D K

一直想找一种既有php,ruby这种便捷性又有java,C#等静态语言特性的语言

Re: 这篇文章更像是一个随想 by 何 坤

谢谢大家的意见。
其实初衷和思路来源于实际的问题和需求,并且已经有相应的实现了;以后可能有开源的计划,也会就具体实现进行深入介绍。

Re: 其实java就是你想要的 by Fang Yonka

java的“动态”太笨拙了...

Re: 其实java就是你想要的 by Fang Yonka

java的“动态”太笨拙了...

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

16 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT