高性能EL——Fel探秘,兼談EL
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,整體一個運行的過程是這樣:
這個思路我在實現aviator之前就想過,但是後來考慮到API需要用的sun獨有的類,而且要求classpath必須有tools.jar這個依賴包,就放棄了這個思路,還是采用ASM生成字節碼的方式。題外,velocity的優化可以采用這個思路,我們有這麼一個項目是這麼做的,也準備開源了。
看看Fel生成的中間代碼,例如a+b這樣的一個簡單的表達式,假設我一開始不知道a和b的類型,編譯是這樣:
Expression exp = fel.compile("a+b", null);
我稍微改了下FEL的源碼,讓它打印中間生成的java代碼,a+b生成的中間結果為:
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都為數字,那麼結果就非常詭異了:
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)));
輸出:
1+3.14的結果,作為字符串拚接就是13.14,而不是我們想要的4.14。如果將表達式換成a*b,就完全運行不了
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加上類型,看看生成的中間代碼:
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)));
查看中間代碼:
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類型了,做數值相加,結果也正確了:
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出現,它們包含設計者自己的目標和口味,選擇很多,就看取舍。
文章轉自莊周夢蝶 ,原文發布時間2011-09-17
最後更新:2017-05-18 20:31:41