IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

高性能EL――Fel探秘,兼谈EL

BlogJava-庄周梦蝶 2011-09-18 21:31:24 累计浏览 2,529 次
本机暂存
    Fel是最近javaeye比较火的关键词,这是由网友lotusyu开发的一个高性能的EL,从作者给出的数据来看,性能非常优异,跟前段时间温少开源的Simple EL有的一拼。首先要说,这是个好现象,国内的开源项目越来越多,可以看出开发者的水平是越来越高了,比如我最近还看到有人开源的类似kestel的轻量级MQ――fqueue也非常不错,有兴趣可以看下我的分析《fqueue初步分析》。

     进入正文,本文是尝试分析下Fel的实现原理,以及优缺点和aviator――我自己开源的EL之间的简单比较。

       Fel的实现原理跟Simple EL是类似,都是使用template生成中间代码――也就是普通的java代码,然后利用javac编译成class,最后运行,当然,这个过程都是动态的。JDK6已经引入了编译API,在此之前的版本可以调用sun的类来编译,因为javac其实就是用java实现的。回到Fel里面,FelCompiler15就是用 com.sun.tools.javac.Main来编译,而FelCompiler16用标准的javax.tools.JavaCompiler来编译的。

         文法和语法解释这块是使用antlr这个parse generator生成的,这块不多说,有兴趣可以看下antlr,整体一个运行的过程是这样:

    expression string -> antlr -> AST -> comiple -> java source template -> java class -> Expression 

     这个思路我在实现aviator之前就想过,但是后来考虑到API需要用的sun独有的类,而且要求classpath必须有tools.jar这个依赖包,就放弃了这个思路,还是采用ASM生成字节码的方式。题外,velocity的优化可以采用这个思路,我们有这么一个项目是这么做的,也准备开源了。

     看看Fel生成的中间代码,例如a+b这样的一个简单的表达式,假设我一开始不知道a和b的类型,编译是这样:

    FelEngine fel = new FelEngineImpl();  

     Expression exp = fel.comp

     我稍微改了下FEL的源码,让它打印中间生成的java代码,a+b生成的中间结果为:

    package com.greenpineyu.fel.compile;  

     import com.greenpineyu.fel.common.NumberUtil;  

     import com.greenpineyu.fel.Expression;  

     import com.greenpineyu.fel.context.FelContext;  

     import org.apache.commons.lang.ObjectUtils;  

     import org.apache.commons.lang.StringUtils;  

     public class Fel_0  implements Expression{  

    public Object eval(FelContext context) {  

    java.lang.Object var_1 = (java.lang.Object)context.get("b");   //b  

    java.lang.Object var_0 = (java.lang.Object)context.get("a");   //a  

    return (ObjectUtils.toString(var_0))+(ObjectUtils.toString(var_1));  

    }  

     } 

      可见,FEL对表达式解析和解释后,利用template生成这么一个普通的java类,而a和b都从context中获取并转化为Object类型,这里没有做任何判断就直接认为a和b是要做字符串相加,然后拼接字符串并返回。

      问题出来了,因为没有在编译的时候传入context(我们这里是null),FEL会将a和b的类型默认都为java.lang.Object,a+b解释为字符串拼接。但是运行的时候,我完全可以传入a和b都为数字,那么结果就非常诡异了:

     FelEngine fel = new FelEngineImpl();  

     Expression exp = fel.compile("a+b", null);  

     Map env=new HashMap();  

     env.put("a", 1);  

     env.put("b", 3.14);  

     System.out.println(exp.eval(new MapContext(env))); 

    输出:

    13.14 

     1+3.14的结果,作为字符串拼接就是13.14,而不是我们想要的4.14。如果将表达式换成a*b,就完全运行不了

    com.greenpineyu.fel.exception.CompileException: package com.greenpineyu.fel.compile;  

     import com.greenpineyu.fel.common.NumberUtil;  

     import com.greenpineyu.fel.Expression;  

     import com.greenpineyu.fel.context.FelContext;  

     import org.apache.commons.lang.ObjectUtils;  

     import org.apache.commons.lang.StringUtils;  

     public class Fel_0  implements Expression{  

    public Object eval(FelContext context) {  

    java.lang.Object var_1 = (java.lang.Object)context.get("b");   //b  

    java.lang.Object var_0 = (java.lang.Object)context.get("a");   //a  

    return (var_0)*(var_1);  

    }  

     }  

     [Fel_0.java:14: 运算符 * 不能应用于 java.lang.Object,java.lang.Object]  

    at com.greenpineyu.fel.compile.FelCompiler16.compileToClass(FelCompiler16.java:113)  

    at com.greenpineyu.fel.compile.FelCompiler16.compile(FelCompiler16.java:87)  

    at com.greenpineyu.fel.compile.CompileService.compile(CompileService.java:66)  

    at com.greenpineyu.fel.FelEngineImpl.compile(FelEngineImpl.java:62)  

    at TEst.main(TEst.java:14)  

     Exception in thread "main" java.lang.NullPointerException  

    at TEst.main(TEst.java:18) 

     这个问题对于Simple EL同样存在,如果没有在编译的时候能确定变量类型,这无法生成正确的中间代码,导致运行时出错,并且有可能造成非常诡异的bug。

     这个问题的本质是因为Fel和Simple EL没有自己的类型系统,他们都是直接使用java的类型的系统,并且必须在编译的时候确定变量类型,才能生成高效和正确的代码,我们可以将它们称为“强类型的EL“。

     现在让我们在编译的时候给a和b加上类型,看看生成的中间代码:

    FelEngine fel = new FelEngineImpl();  

     fel.getContext().set("a", 1);  

     fel.getContext().set("b", 3.14);  

     Expression exp = fel.compile("a+b", null);  

     Map env = new HashMap();  

     env.put("a", 1);  

     env.put("b", 3.14);  

     System.out.println(exp.eval(new MapContext(env))); 

     查看中间代码:

    package com.greenpineyu.fel.compile;  

     import com.greenpineyu.fel.common.NumberUtil;  

     import com.greenpineyu.fel.Expression;  

     import com.greenpineyu.fel.context.FelContext;  

     import org.apache.commons.lang.ObjectUtils;  

     import org.apache.commons.lang.StringUtils;  

     public class Fel_0  implements Expression{  

    public Object eval(FelContext context) {  

    double var_1 = ((java.lang.Number)context.get("b")).doubleValue();   //b  

    double var_0 = ((java.lang.Number)context.get("a")).doubleValue();   //a  

    return (var_0)+(var_1);  

    }  

     } 

    可以看到这次将a和b都强制转为double类型了,做数值相加,结果也正确了:

    4.140000000000001 

     Simple EL我没看过代码,这里猜测它的实现也应该是类似的,也应该有同样的问题。

     相比来说,aviator这是一个弱类型的EL在编译的时候不对变量类型做任何假设,而是在运行时做类型判断和自动转化。过去提过,我给aviator的定位是一个介于EL和script之间的东西,它有自己的类型系统。例如,3这个数字,在java里可能是long,int,short,byte,而aviator统一为AviatorLong这个类型。为了在这两个类型之间做适配,就需要做很多的判断和box,unbox操作。这些判断和转化都是运行时进行的,因此aviator没有办法做到Fel这样的高效,但是已经做到至少跟groovy这样的弱类型脚本语言一个级别,也超过了JXEL这样的纯解释EL,具体可以看这个性能测试

     强类型还是弱类型,这是一个选择问题,如果你能在运行前就确定变量的类型,那么使用Fel应该可以达到或者接近于原生java执行的效率,但是失去了灵活性;如果你无法确定变量类型,则只能采用弱类型的EL。

     EL涌现的越来越多,这个现象有点类似消息中间件领域,越来越多面向特定领域的轻量级MQ的出现,而不是原来那种大而笨重的通用MQ大行其道,一方面是互联网应用的发展,需求不是通用系统能够满足的,另一方面我认为也是开发者素质的提高,大家都能造适合自己的轮子。从EL这方面来说,我也认为会有越来越多特定于领域的,优点和缺点一样鲜明的EL出现,它们包含设计者自己的目标和口味,选择很多,就看取舍。

dennis 2011-09-17 12:52 发表评论

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,894)
  2. Java开发岗位面试题归类汇总 (累计阅读 22,156)
  3. android 开发入门 (累计阅读 19,527)
  4. 我的PHP,Python和Ruby之路 (累计阅读 13,147)
  5. HashMap解决hash冲突的方法 (累计阅读 12,654)
  6. 一个大二学生有关如何成为一名软件工程师的疑问及答复 (累计阅读 9,181)
  7. Java程序员应该知道的10个eclipse调试技巧 (累计阅读 8,012)
  8. 如何让员工忠于公司? (累计阅读 7,940)
  9. Java技术路线 (累计阅读 7,725)
  10. 聊聊ThoughtWorks面试 (累计阅读 7,614)