占位,先顶后看。
从编译的角度看为什么 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条回答
为你推荐
- Velocity 读取模板内容 改变模板内容
- 0个回答
- velocity 静态化 同步问题
- 0个回答
- Velocity 输出field的疑问
- 0个回答
- 使用velocity的路径问题
- 0个回答
- 求WebLogic10.3下运行velocity出错的解决办法
- 0个回答
- Spring velocity 取session的问题
- spring
- 0个回答
- velocity中可不可以使用Struts标签?
- struts
- 0个回答
- 关于velocity 显示list值的问题
- 0个回答
- velocity的VM文件是否支持iframe???-熟悉velocity的朋友请进!
- struts
- 0个回答
- velocity
- 0个回答
- velocity中如何进行数字与字符串的转换?
- 0个回答
- eclipse4.8怎么安装velocity插件
- eclipse
- 2个回答
- velocity前端导入echarts.js
- 4个回答
- 前台用velocity怎么取出后台传的JSON值?求帮助
- json
- 1个回答
- freemarker和jsp的具体使用上的区别是什么 freemarker和EL表达式关系呢
- java
- 1个回答
- springboot 集成 velocity
- spring
- 1个回答
- 对于什么是Velocity介绍的不太清楚,谁能仔细的介绍一下
- 2个回答
- 请问有没有人知道 velocity的#cacheFragment是干什么的?哪位大佬知道
- 1个回答
- java spring配置视图解析(velocity)报错
- 2个回答
- velocity问题。求解,求教
- java
- 3个回答