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);
我稍微改了下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<String, Object> env=new HashMap<String, Object>();
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<String, Object> env = new HashMap<String, Object>();
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出現,它們包含設計者自己的目標和口味,選擇很多,就看取舍。