閱讀797 返回首頁    go 阿裏雲 go 技術社區[雲棲]


高性能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,整體一個運行的過程是這樣:

    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出現,它們包含設計者自己的目標和口味,選擇很多,就看取舍。

文章轉自莊周夢蝶  ,原文發布時間2011-09-17

最後更新:2017-05-18 20:31:41

  上一篇:go  緊急發布xmemcached 1.3.5
  下一篇:go  fqueue初步分析