java fel_Fel实现探秘,兼谈EL

java fel_Fel实现探秘,兼谈ELFel 是最近 javaeye 比较火的关键词 这是由网友 lotusyu 开发的一个高性能的 EL 从作者给出的数据来看 性能非常优异 跟前段时间温少开源的 SimpleEL 有的一拼 首先要说 这是个好现象 国内的开源项目越来越多 可以看出开发者的水平是越来越高了 比如我最近还看到有人开源的类似 kestel 的轻量级 MQ fqueue 也非常不错 有兴趣可以看下我的分析 fqueue 初步分析 进入正文 本文

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.compile(“a+b”, null);

System.out.println(exp);

我稍微改了下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.0001

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出现,它们包含设计者自己的目标和口味,选择很多,就看取舍。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/210318.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 上午7:50
下一篇 2026年3月19日 上午7:50


相关推荐

  • 操作系统实验报告 lab1

    操作系统实验报告 lab1操作系统实验报告 lab1

    2026年3月18日
    1
  • 如何正确配置manus todo.md mcp文件结构?

    如何正确配置manus todo.md mcp文件结构?

    2026年3月15日
    3
  • 低代码技术与市场(Mendix与 OutSystems)

    低代码技术与市场(Mendix与 OutSystems)低代码技术与市场 Mendix 与 OutSystems 本文主要参考文章参考链接 https mp weixin com s OXCBORheAx99 ZfUdghttps blog csdn net article details 低代码分析低代码和无代码 称零代码 是什么关系 怎么判断一个低代码平台是否专业 国内是否有专业的低代码平台 低代码是不是新瓶装旧酒 低代码真的搞不定专业的企业应用吗 低代码不适合开发哪些应用 低代码并非银

    2026年3月17日
    2
  • Window 通过cmd查看端口占用、相应进程、杀死进程等的命令

    参考博文01:https://blog.csdn.net/qq_35923749/article/details/83786794博文02:https://blog.csdn.net/qq_24754061/article/details/826607561.查看所有进程的端口使用信息Windows键+R,在弹出的运行窗口中输入cmd,在cmd窗口中输入netstat-ano显示协…

    2022年4月3日
    75
  • 进程间的7种通信方式_linux 进程间通信

    进程间的7种通信方式_linux 进程间通信1无名管道通信无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。2高级管道通信高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。3有名管道通信有名管道(namedpipe):有名管道也是半双工的通信方式,但是它允许

    2022年10月11日
    4
  • 只响应ccTouchBegan的问题

    只响应ccTouchBegan的问题在 Touch 事件中 ccTouchBegan 有一个返回值 而这一个返回值则决定了是否会继续响应 ccTouchMoved ccTouchEnded 如果没有返回 true 的话 则直接会结束此 Touch 事件 只响应 ccTouchBegan boolJSprite ccTouchBegan CCTouch pTouch CCEvent pEvent returnt

    2026年3月26日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号