O2O網際網路的運營開發最大的特點就是每次運營活動規則千奇百怪,需要有許多個性化的配置,如何例A活動需要針對新使用者做發紅包的活動,B活動針對全部使用者做發紅包活動,而在B活動中針對新使用者發x面額的紅包,而針對老使用者發y面值的紅包。兩個活動規則差別較大,如果每次都個性化開發,會非常浪費時間,因此如何支援規則的動態配置是個很大的挑戰。
Java不是解決動態層問題的理想語言,這些動態層問題包括原型設計、指令碼處理等。
公司的專案主要基於Java平臺,在實踐中發現主要有兩種方式可以實現:
- 統一表示式語言
- 動態語言,如Groovy
JUEL(Java 統一表示式語言)
Java*統一表示式語言(英語:Unified Expression Language,簡稱JUEL*)是一種特殊用途的程式語言,主要在Java Web應用程式用於將表示式嵌入到web頁面。Java規範制定者和Java Web領域技術專家小組制定了統一的表示式語言。JUEL最初包含在JSP 2.1規範JSR-245中,後來成為Java EE 7的一部分,改在JSR-341中定義。
主要的開源實現有:OGNL ,MVEL ,SpEL ,JUEL ,Java Expression Language (JEXL) ,JEval ,Jakarta JXPath 等。這裡主要介紹在實踐中使用較多的MVEL、OGNL和SpEL。
OGNL(Object Graph Navigation Library)
在Struts 2 的標籤庫中都是使用OGNL表示式訪問ApplicationContext中的物件資料,OGNL主要有三個重要因素:
- Expression
Expression是整個OGNL的核心內容,所有的OGNL操作都是針對表示式解析後進行的。通過Expression來告知OGNL操作到底要幹些什麼。因此,Expression其實是一個帶有語法含義的字串,整個字串將規定操作的型別和內容。OGNL表示式支援大量Expression,如“鏈式訪問物件”、表示式計算、甚至還支援Lambda表示式。
- Root物件:
OGNL的Root物件可以理解為OGNL的操作物件。當我們指定了一個表示式的時候,我們需要指定這個表示式針對的是哪個具體的物件。而這個具體的物件就是Root物件,這就意味著,如果有一個OGNL表示式,那麼我們需要針對Root物件來進行OGNL表示式的計算並且返回結果。
- ApplicationContext
有個Root物件和Expression,我們就可以使用OGNL進行簡單的操作了,如對Root物件的賦值與取值操作。但是,實際上在OGNL的內部,所有的操作都會在一個特定的資料環境中執行。這個資料環境就是ApplicationContext(上下文環境)。OGNL的上下文環境是一個Map結構,稱之為OgnlContext。Root物件也會被新增到上下文環境當中去。
1 2 3 4 5 6 7 8 9 10 11 |
Foo foo = new Foo(); foo.setName("test"); Map<String, Object> context = new HashMap<String, Object>(); context.put("foo",foo); String expression = "foo.name == 'test'"; try { Boolean result = (Boolean) Ognl.getValue(expression,context); System.out.println(result); } catch (OgnlException e) { e.printStackTrace(); } |
這段程式碼就是判斷物件foo的name屬性是否為test。
OGNL的具體語法參見OGNL language guide 。
MVEL
MVEL最初作為Mike Brock建立的 Valhalla專案的表示式計算器(expression evaluator)。Valhalla本身是一個早期的類似 Seam 的“開箱即用”的Web 應用框架,而 Valhalla 專案現在處於休眠狀態, MVEL則成為一個繼續積極發展的專案。相比最初的OGNL、JEXL和JUEL等專案,而它具有遠超它們的效能、功能和易用性 – 特別是整合方面。它不會嘗試另一種JVM語言,而是著重解決嵌入式指令碼的問題。
MVEL特別適用於受限環境 – 諸如由於記憶體或沙箱(sand-boxing)問題不能使用位元組碼生成。它不是試圖重新發明Java,而是旨在提供一種Java程式設計師熟悉的語法,同時還加入了簡短的表示式語法。
MVEL主要使用在Drools,是Drools規則引擎不可分割的一部分。
MVEL語法較為豐富,不僅包含了基本的屬性表示式,布林表示式,變數複製和方法呼叫,還支援函式定義,詳情參見MVEL Language Guide 。
MVEL在執行語言時主要有解釋模式(Interpreted Mode)和編譯模式(Compiled Mode )兩種:
- 解釋模式(Interpreted Mode)是一個無狀態的,動態解釋執行,不需要負載表示式就可以執行相應的指令碼。
- 編譯模式(Compiled Mode)需要在快取中產生一個完全規範化表示式之後再執行。
解釋模式
1 2 3 4 5 6 7 8 9 |
//解釋模式 Foo foo = new Foo(); foo.setName("test"); Map context = new HashMap(); String expression = "foo.name == 'test'"; VariableResolverFactory functionFactory = new MapVariableResolverFactory(context); context.put("foo",foo); Boolean result = (Boolean) MVEL.eval(expression,functionFactory); System.out.println(result); |
編譯模式
1 2 3 4 |
//編譯模式 Foo foo = new Foo();foo.setName("test");Map context = new HashMap();String expression = "foo.name == 'test'";VariableResolverFactory functionFactory = new MapVariableResolverFactory(context);context.put("foo",foo); Serializable compileExpression = MVEL.compileExpression(expression); Boolean result = (Boolean) MVEL.executeExpression(compileExpression, context, functionFactory); |
SpEL
SpEl(Spring表示式語言)是一個支援查詢和操作執行時物件導航圖功能的強大的表示式語言。 它的語法類似於傳統EL,但提供額外的功能,最出色的就是函式呼叫和簡單字串的模板函式。SpEL類似於Struts2x中使用的OGNL表示式語言,能在執行時構建複雜表示式、存取物件圖屬性、物件方法呼叫等等,並且能與Spring功能完美整合,如能用來配置Bean定義。
SpEL主要提供基本表示式、類相關表示式及集合相關表示式等,詳細參見Spring 表示式語言 (SpEL) 。
類似與OGNL,SpEL具有expression(表示式),Parser(解析器),EvaluationContext(上下文)等基本概念;類似與MVEL,SpEl也提供瞭解釋模式和編譯模式兩種執行模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//直譯器模式 Foo foo = new Foo(); foo.setName("test"); // Turn on: // - auto null reference initialization // - auto collection growing SpelParserConfiguration config = new SpelParserConfiguration(true,true); ExpressionParser parser = new SpelExpressionParser(config); String expressionStr = "#foo.name == 'test'"; StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("foo",foo); Expression expression = parser.parseExpression(expressionStr); Boolean result = expression.getValue(context,Boolean.class); //編譯模式 config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, RunSpel.class.getClassLoader()); parser = new SpelExpressionParser(config); context = new StandardEvaluationContext(); context.setVariable("foo",foo); expression = parser.parseExpression(expressionStr); result = expression.getValue(context,Boolean.class); |
Groovy
Groovy除了Gradle 上的廣泛應用之外,另一個大範圍的使用應該就是結合Java使用動態程式碼了。Groovy的語法與Java非常相似,以至於多數的Java程式碼也是正確的Groovy程式碼。Groovy程式碼動態的被編譯器轉換成Java位元組碼。由於其執行在JVM上的特性,Groovy可以使用其他Java語言編寫的庫。
Groovy可以看作給Java靜態世界補充動態能力的語言,同時Groovy已經實現了java不具備的語言特性:
- 函式字面值;
- 對集合的一等支援;
- 對正規表示式的一等支援;
- 對xml的一等支援;
Groovy作為基於JVM的語言,與表示式語言存在語言級的不同,因此在語法上比表達還是語言更靈活。Java在呼叫Groovy時,都需要將Groovy程式碼編譯成Class檔案。
Groovy 可以採用GroovyClassLoader、GroovyShell、GroovyScriptEngine和JSR223 等方式與Java語言整合。
GroovyClassLoader
GroovyClassLoader是一個定製的類裝載器,負責解釋載入Java類中用到的Groovy類,也可以編譯,Java程式碼可通過其動態載入Groovy指令碼並執行。
1 2 3 4 5 6 7 |
class FooCompare{ boolean compare(String toCompare){ Foo foo = new Foo(); foo.name = "test"; return foo.name == toCompare; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
GroovyClassLoader loader = new GroovyClassLoader(); Class groovyClass = null; try { String path = "FooCompare.groovy"; groovyClass = loader.parseClass(new File(path)); } catch (IOException e) { e.printStackTrace(); } GroovyObject groovyObject = null; try { groovyObject = (GroovyObject) groovyClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } result = groovyObject.invokeMethod("compare", "test"); assert result.equals(Boolean.TRUE); System.out.print(result); |
GroovyShell
GroovyShell允許在Java類中(甚至Groovy類)求任意Groovy表示式的值。可以使用Binding物件輸入引數給表示式,並最終通過GroovyShell返回Groovy表示式的計算結果。
1 2 3 4 5 6 7 8 |
Foo foo = new Foo(); foo.setName("test"); Binding binding = new Binding(); binding.setVariable("foo",foo); GroovyShell shell = new GroovyShell(binding); String expression = "foo.name=='test'"; Object result = shell.evaluate(expression); assert result.equals(Boolean.TRUE); |
GroovyScriptEngine
GroovyShell多用於推求對立的指令碼或表示式,如果換成相互關聯的多個指令碼,使用GroovyScriptEngine會更好些。GroovyScriptEngine從您指定的位置(檔案系統,URL,資料庫,等等)載入Groovy指令碼,並且隨著指令碼變化而重新載入它們。如同GroovyShell一樣,GroovyScriptEngine也允許您傳入引數值,並能返回指令碼的值。
FooScript.groovy
1 2 3 |
package blog.brucefeng.info.groovy foo.name=="test"; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Foo foo = new Foo(); foo.setName("test"); Binding binding = new Binding(); binding.setVariable("foo",foo); String[] paths = {"/demopath/"} GroovyScriptEngine gse = new GroovyScriptEngine(paths); try { result = gse.run("FooScript.groovy", binding); } catch (ResourceException e) { e.printStackTrace(); } catch (ScriptException e) { e.printStackTrace(); } assert result.equals(Boolean.TRUE); |
JSR223
JSR223 是Java 6提供的一種從Java內部執行指令碼編寫語言的方便、標準的方式,並提供從指令碼內部訪問Java 資源和類的功能,可以使用其執行多種指令碼語言如JavaScript和Groovy等。
1 2 3 4 5 6 7 8 9 10 11 |
Foo foo = new Foo(); foo.setName("test"); ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine1 = factory.getEngineByName("groovy"); engine1.put("foo",foo); try { result = engine1.eval(expression); } catch (javax.script.ScriptException e) { e.printStackTrace(); } assert result.equals(Boolean.TRUE); |
使用中經常出現的問題
因此Java每次呼叫Groovy程式碼都會將Groovy編譯成Class檔案,因此在呼叫過程中會出現JVM級別的問題。如使用GroovyShell的parse方法導致perm區爆滿的問題,使用GroovyClassLoader載入機制導致頻繁gc問題和CodeCache用滿,導致JIT禁用問題等,相關問題可以參考Groovy與Java整合常見的坑 。
效能對比
在這裡簡單對上面介紹到的OGNL、MVEL、SpEL和Groovy2.4 的效能進行大致的效能測試(簡單測試):
實現方式 | 耗時(ms) |
---|---|
Java | 13 |
OGNL | 2958 |
MVEL | 225 |
SpEL | 1023 |
Groovy | 99 |
通過這個簡單測試發現,Groovy 2.4的效能已經足夠的好,而MVEL的效能依然保持強勁,不過已經遠遠落後與Groovy,在對效能有一定要求的場景下已經不建議使用OGNL和SpEL。
不過動態程式碼的執行效率還是遠低於Java,因此在高效能的場景下慎用。
以下是測試程式碼:
1 2 3 4 5 6 7 |
package blog.brucefeng.info.performance class GroovyCal{ Integer cal(int x,int y,int z){ return x + y*2 - z; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
package blog.brucefeng.info.performance; public class RunPerform { public static void main(String[] args) { try { int xmax = 100,ymax = 100,zmax= 10; runJava(xmax, ymax, zmax); runOgnl(xmax, ymax, zmax); runMvel(xmax, ymax, zmax); runSpel(xmax, ymax, zmax); runGroovyClass(xmax, ymax, zmax); } catch (Exception e) { e.printStackTrace(); } } public static void runJava(int xmax, int ymax, int zmax) { Date start = new Date(); Integer result = 0; for (int xval = 0; xval < xmax; xval++) { for (int yval = 0; yval < ymax; yval++) { for (int zval = 0; zval <= zmax; zval++) { result += xval + yval * 2 - zval; } } } Date end = new Date(); System.out.println("time is : " + (end.getTime() - start.getTime()) + ",result is " + result); } public static void runOgnl(int xmax, int ymax, int zmax) throws OgnlException { String expression = "x + y*2 - z"; Map<String, Object> context = new HashMap<String, Object>(); Integer result = 0; Date start = new Date(); for (int xval = 0; xval < xmax; xval++) { for (int yval = 0; yval < ymax; yval++) { for (int zval = 0; zval <= zmax; zval++) { context.put("x", xval); context.put("y", yval); context.put("z", zval); Integer cal = (Integer) Ognl.getValue(expression, context); result += cal; } } } Date end = new Date(); System.out.println("Ognl:time is : " + (end.getTime() - start.getTime()) + ",result is " + result); } public static void runMvel(int xmax, int ymax, int zmax) { Map context = new HashMap(); String expression = "x + y*2 - z"; Serializable compileExpression = MVEL.compileExpression(expression); Integer result = 0; Date start = new Date(); for (int xval = 0; xval < xmax; xval++) { for (int yval = 0; yval < ymax; yval++) { for (int zval = 0; zval <= zmax; zval++) { context.put("x", xval); context.put("y", yval); context.put("z", zval); VariableResolverFactory functionFactory = new MapVariableResolverFactory(context); Integer cal = (Integer) MVEL.executeExpression(compileExpression, context, functionFactory); result += cal; } } } Date end = new Date(); System.out.println("MVEL:time is : " + (end.getTime() - start.getTime()) + ",result is " + result); } public static void runSpel(int xmax, int ymax, int zmax) { SpelParserConfiguration config; ExpressionParser parser; config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, RunSpel.class.getClassLoader()); parser = new SpelExpressionParser(config); StandardEvaluationContext context = new StandardEvaluationContext(); Integer result = 0; String expressionStr = "#x + #y*2 - #z"; Date start = new Date(); for (Integer xval = 0; xval < xmax; xval++) { for (Integer yval = 0; yval < ymax; yval++) { for (Integer zval = 0; zval <= zmax; zval++) { context.setVariable("x", xval); context.setVariable("y", yval); context.setVariable("z", zval); Expression expression = parser.parseExpression(expressionStr); Integer cal = expression.getValue(context, Integer.class); result += cal; } } } Date end = new Date(); System.out.println("SpEL:time is : " + (end.getTime() - start.getTime()) + ",result is " + result); } public static void runGroovyClass(int xmax, int ymax, int zmax) { GroovyClassLoader loader = new GroovyClassLoader(); Class groovyClass = null; try { groovyClass = loader.parseClass(new File( "GroovyCal.groovy")); } catch (IOException e) { e.printStackTrace(); } GroovyObject groovyObject = null; try { groovyObject = (GroovyObject) groovyClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } Integer result = 0; Date start = new Date(); for (int xval = 0; xval < xmax; xval++) { for (int yval = 0; yval < ymax; yval++) { for (int zval = 0; zval <= zmax; zval++) { Object[] args = {xval,yval,zval}; Integer cal = (Integer) groovyObject.invokeMethod("cal", args); result += cal; } } } Date end = new Date(); System.out.println("Groovy Class:time is : " + (end.getTime() - start.getTime()) + ",result is " + result); } } |
本文的程式碼可以參見eldemo github 。
References
- Groovy vs Java Performance
- Groovy與Java整合常見的坑
- OGNL language guide
- Groovy引發的PermGen區爆滿問題定位與解決
- groovy指令碼導致的FullGC問題
- Groovy效能問題
- Groovy的classloader載入機制喚起的頻繁GC
- Groovy深入探索——Groovy的ClassLoader體系
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式