Java中使用動態代碼
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對象也會被添加到上下文環境當中去。
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最初作為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)需要在緩存中產生一個完全規范化表達式之后再執行。
解釋模式
//解釋模式
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);
編譯模式
//編譯模式
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(Spring表達式語言)是一個支持查詢和操作運行時對象導航圖功能的強大的表達式語言。 它的語法類似于傳統EL,但提供額外的功能,最出色的就是函數調用和簡單字符串的模板函數。SpEL類似于Struts2x中使用的OGNL表達式語言,能在運行時構建復雜表達式、存取對象圖屬性、對象方法調用等等,并且能與Spring功能完美整合,如能用來配置Bean定義。
SpEL主要提供基本表達式、類相關表達式及集合相關表達式等,詳細參見 Spring 表達式語言 (SpEL) 。
類似與OGNL,SpEL具有expression(表達式),Parser(解析器),EvaluationContext(上下文)等基本概念;類似與MVEL,SpEl也提供了解釋模式和編譯模式兩種運行模式。
//解釋器模式
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腳本并執行。
class FooCompare{
boolean compare(String toCompare){
Foo foo = new Foo();
foo.name = "test";
return foo.name == toCompare;
}
}
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表達式的計算結果。
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
package blog.brucefeng.info.groovy
foo.name=="test";
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等。
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。
以下是測試代碼:
package blog.brucefeng.info.performance
class GroovyCal{
Integer cal(int x,int y,int z){
return x + y*2 - z;
}
}
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深入探索——Groovy的ClassLoader體系
來自: http://blog.brucefeng.info/post/dynamic-code-in-java