weixin_39717110
weixin_39717110
2020-12-30 18:08

从编译的角度看为什么 Velocity 丑到哭

从编译的角度看为什么 Velocity 丑到哭

阅读本文需要你对编译器的前端有所了解,能读懂词法规则和语法规则。

由于 Velocity 不同版本之间有些规则差异较大,文中未单独指明之处均使用 v1.7。

Velocity 为什么丑?恐怕大部分同学都会说 “用着就觉得够丑了”(这和它仍然是 Java 界最主流的模板语言不矛盾)。从模板编译的角度来看,也是丑得可以。让我们通过一些例子来一窥究竟: 1. 难以预测的语言行为 2. 为什么 $a.b($c + $d) 不合法 3. 恼人的 “双引号字符串” 4. 奇葩的 macro 参数列表

1、难以预测的语言行为

你能准确说出下列输入串被如何解析么? 1. \$!a$\!a 2. $a()${a()} 3. $a.b(c)$a.b(c.d) 4. Macro 调用 #a--b(...)#a__b(...#a-_b(...)

答案是: 1. 文本 $!a,引用 $!a 2. 引用 $a 后跟文本 (), 语法错 3. 以引用 c 为参数的方法调用, 非法参数 4. 非法, 合法, 合法

即使你是 Velocity 资深用户,也认真阅读过文档,恐怕都很难全部答对。

你可能会觉得正常人怎么写的出上面那些代码?好吧,其实我也这么想。这些语言特性,看起来能让用户在很少情况下减少一丁点输入,但却很让人困惑,还增加了解析的难度。

拿第 2 个例子来说,看起来像是同一个引用的简写格式和规范(format)格式,但两者行为却不一致。将 () 解析成方法调用还是普通文本,是在词法分析阶段进行的,那么词法分析时就必须知道所处的开始条件(Start Condition),例如:


$a()         // 普通文本
$a[0]()      // 普通文本
$a.b()       // 方法调用
$a.b()()     // 方法调用和普通文本
${a()}       // 方法调用,但有语法错
${a[0]()}    // 方法调用,但有语法错
${a.b()}     // 方法调用
${a.b()()}   // 方法调用和普通文本,但有语法错

由以上代码可知,需要知道以下因素才能确定如何解析 (): 1. 引用格式 - 简写格式?
- 还是规范格式? 2. 所处位置 - 跟在 ID 后面 ($a/())? - 还是跟在索引后面($a[0]/())? - 还是跟在属性后面($a.b/())? - 还是跟在方法调用后面 ($a.b()/())?

也就是说要正确解析 (),需要使用 2 * 4 = 8 个开始条件。如果将 $a()${a()} 一致对待,仅需使用 1 个开始条件(在引用中?)。

2、为什么 $a.b($c + $d) 不合法

要理解这个问题,先看另一个问题:- 4 是数值还是一元表达式?

观察一个简化的表达式模型,该模型仅支持引用和数值的加减法。一般来说产生式如下:


expr
  : reference  /* 对应 Velocity 的引用 */
  | number
  | '-' expr
  | '+' expr
  | '(' expr ')'
  | expr '+' expr
  | expr '-' expr
  ;

根据产生式, - 4 的最左推导过程是 expr -> - expr -> - number -> - 4。这种情况下 - 4 被看成是一元表达式。

该产生式亦可解析+ 4- - 4- ( 4 )- a 等输入串。

在 Velocity 中,情况就不一样了:除了 - 4,上述提到的其他输入串都是非法的。所以,在 Velocity 中,产生式就会变为:


expr
  : number
  | reference
  | '(' expr ')'
  | expr '+' expr
  | expr '-' expr
  ;

显然,在 Velocity 中,- 4 被看成是数值。Velocity 之所以这么处理恐怕不是为了简化表达式,而是因为以下原因:

在 Velocity 中,方法调用的参数仅允许 reference 和字面量(包含正负数),我们把这类语法节点称为 exprItem ,产生式为:


expr
  : exprItem
  | '(' expr ')'
  | ...
  ;

exprItem
  : reference
  | number
  | list
  | range
  | map
  | dstring
  | string
  | ...
  ;

number
  : integer
  | float
  ;

试想,如果在 expr 中允许一元表达式,在 exprItem 中又要允许 number 为负数,是件多奇怪的事情。所以呢,你就没法写 $a.b($c + $d) 之类的代码啦,只能写成 #set($e = $c + $d) $a.b($e),没错,就是这么麻烦。

3、恼人的 “双引号字符串”

Velocity 中,双引号字符串中的引用和指令都会被解析,例如输入串 #set($a = "${name}, welcome!") 会使得 $a 被赋值为 "fool2fish, welcome!" 之类的。

这也就意味着编译器在遇到双引号字符串时,必须做二次解析。

别想着在最初解析的同时完成双引号字符串的解析,例如 #set($a = "today is: $util.format(\"20140318\")") 这样的输入串,对双引号的转义会导致无法正确解析。

设计该特性的初衷应该是用于字符串拼接的,与其如此还不如支持 + 来得简单明了。我会告诉你我厂还提供了 $Util.String.concat($a, $b) 这种方法方法来拼接字符串?

4、奇葩的 macro 参数列表

几乎所有语言都是以逗号作为参数分隔符,而 macro 的声明和调用支持逗号和空格两种参数分隔符:


#macro( macroName, $a, $b )
#macroName( $c [0] )

通常,在词法分析阶段会忽略非字符串中的所有空格,上面两行代码会解析成如下词法单元序列:


<macro>  <id macroname>   <id a>   <id b> 
<macro_call>   <id c>  <integer>  
</integer></id></macro_call></id></id></id></macro>

以 macro 调用为例,其产生式为:


macroCall 
  : MACRO_CALL '(' ')'
  | MACRO_CALL '(' macroParams ')'
  ;

macroArguments
  : exprItem                      // 单参数
  | exprItem macroArguments       // 空格分隔
  | exprItem ',' macroArguments   // 逗号分隔
  ;

那么解析输入串 #macroName( $c [0] ) 时,就会产生移入/规约冲突。当 $c 规约为 id 时,是移入 [0], 将 $c [0] 规约为 reference 呢还是直接将其规约为 exprItem 呢?

由此可见,进行词法分析时,macro 声明和调用的括号中的空格不可忽略,而为了使产生式不会过于复杂,紧挨着括号的空格又要忽略(妹的,词法分析要不要搞得这么复杂啊=。=),据此得到的词法单元序列:


不期望的方式:
<macro_call>  <ws>  <id c> <ws>  <integer> <ws> 
                          ^                                      ^
                  没有忽略紧挨括号的空格                    没有忽略紧挨括号的空格

期望的方式:
<macro_call>   <id c> <ws>  <integer>  
</integer></ws></id></macro_call></ws></integer></ws></id></ws></macro_call>

进而得到正确的产生式为:


macroCall 
  : MACRO_CALL '(' ')'
  | MACRO_CALL '(' macroParams ')'
  ;

macroArguments
  : exprItem
  | exprItem delimiter macroArguments
  ;

delimieter
  : WS
  | ','
  ;

小结

一句话总结:少既是好,简洁的设计往往能让开发者和使用者都受益。

该提问来源于开源项目:fool2fish/blog

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

14条回答

  • weixin_39840606 weixin_39840606 3月前

    占位,先顶后看。

    点赞 评论 复制链接分享
  • weixin_39519072 weixin_39519072 3月前

    沙发~

    点赞 评论 复制链接分享
  • weixin_39944944 weixin_39944944 3月前

    文章在哪?^_^

    点赞 评论 复制链接分享
  • weixin_39717110 weixin_39717110 3月前

    还打着未完成的标签啊……

    点赞 评论 复制链接分享
  • weixin_39759155 weixin_39759155 3月前

    围观!!终于看到有人跟 velocity 较劲了

    点赞 评论 复制链接分享
  • weixin_39739404 weixin_39739404 3月前

    三年多了,Velocity 再也没有更新过

    点赞 评论 复制链接分享
  • weixin_39690625 weixin_39690625 3月前

    强帖留念

    po主是个JAVA黑

    点赞 评论 复制链接分享
  • weixin_39717110 weixin_39717110 3月前

    你才黑 Java 啊,说的问题跟 Java 又没啥关系……

    点赞 评论 复制链接分享
  • weixin_39713833 weixin_39713833 3月前

    “双引号字符串” <-- 这条很多模板语言都有啊. 不算奇怪

    点赞 评论 复制链接分享
  • weixin_39678304 weixin_39678304 3月前

    厄, 难道之后还有机会 Velocity .... T_T

    点赞 评论 复制链接分享
  • weixin_39575758 weixin_39575758 3月前

    当时实在学不会那个,只好放弃了。

    点赞 评论 复制链接分享
  • weixin_39805924 weixin_39805924 3月前

    它的市场占有率就是胜利;语法前端的东西,其实毕竟只是小头,velocity先入为主在freemarker之流还未出生就已占领主流;后来者再好也难翻盘;其实velocity后端也很烂,遍历ast解释执行,不过还好也有专门帮它编译成字节码的实现;所以还是将就用着吧

    点赞 评论 复制链接分享
  • weixin_39805924 weixin_39805924 3月前

    有个问题值得深思:jsp是比velocity更“老牌”,也更velocity的模板方案(执行效率比velocity高一大截);为何那么多人还是放弃了jsp而转向velocity呢?

    点赞 评论 复制链接分享
  • weixin_39621379 weixin_39621379 3月前

    velocity 语法是有点太灵活了,相比较起来freemarker比较适中

    点赞 评论 复制链接分享