背景
業務系統在應用過程中,有時候要處理“經常變化”的部分,這部分需求可能是“業務規則”,也可能是“不同的資料處理邏輯”,這部分動態規則的問題,往往需要可配置,並對效能和實時性有一定要求。
Java不是解決動態層問題的理想語言,在實踐中發現主要有以下幾種方式可以實現:
- 表示式語言(expression language)
- 動態語言(dynamic/script language language),如Groovy
- 規則引擎(rule engine)
表示式語言
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。
在Struts 2 的標籤庫中都是使用OGNL表示式訪問ApplicationContext中的物件資料,簡單示例:
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();
}
MVEL
MVEL最初作為Mike Brock建立的 Valhalla專案的表示式計算器(expression evaluator),相比最初的OGNL、JEXL和JUEL等專案,而它具有遠超它們的效能、功能和易用性 - 特別是整合方面。它不會嘗試另一種JVM語言,而是著重解決嵌入式指令碼的問題。
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
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);
規則引擎
一些規則引擎(rule engine):aviator,easy-rules,drools,esper
AviatorScript
是一門高效能、輕量級寄宿於 JVM 之上的指令碼語言。
使用場景包括:
- 規則判斷及規則引擎
- 公式計算
- 動態指令碼控制
- 集合資料 ELT 等
public class Test { public static void main(String[] args) { String expression = "a+(b-c)>100"; // 編譯表示式 Expression compiledExp = AviatorEvaluator.compile(expression); Map<String, Object> env = new HashMap<>(); env.put("a", 100.3); env.put("b", 45); env.put("c", -199.100); // 執行表示式 Boolean result = (Boolean) compiledExp.execute(env); System.out.println(result); } }
Easy Rules is a Java rules engine。
使用POJO定義規則:
@Rule(name = "weather rule", description = "if it rains then take an umbrella") public class WeatherRule { @Condition public boolean itRains(@Fact("rain") boolean rain) { return rain; } @Action public void takeAnUmbrella() { System.out.println("It rains, take an umbrella!"); } } Rule weatherRule = new RuleBuilder() .name("weather rule") .description("if it rains then take an umbrella") .when(facts -> facts.get("rain").equals(true)) .then(facts -> System.out.println("It rains, take an umbrella!")) .build();
支援使用表示式語言(MVEL/SpEL)來定義規則:
weather-rule.yml
example:
name: "weather rule" description: "if it rains then take an umbrella" condition: "rain == true" actions: - "System.out.println(\"It rains, take an umbrella!\");"
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader()); Rule weatherRule = ruleFactory.createRule(new FileReader("weather-rule.yml"));
觸發規則:
public class Test { public static void main(String[] args) { // define facts Facts facts = new Facts(); facts.put("rain", true); // define rules Rule weatherRule = ... Rules rules = new Rules(); rules.register(weatherRule); // fire rules on known facts RulesEngine rulesEngine = new DefaultRulesEngine(); rulesEngine.fire(rules, facts); } }
An open source rule engine, DMN engine and complex event processing (CEP) engine for Java and the JVM Platform.
定義規則:
import com.lrq.wechatDemo.domain.User // 匯入類 dialect "mvel" rule "age" // 規則名,唯一 when $user : User(age<15 || age>60) //規則的條件部分 then System.out.println("年齡不符合要求!"); end
參考例子:
public class TestUser { private static KieContainer container = null; private KieSession statefulKieSession = null; @Test public void test(){ KieServices kieServices = KieServices.Factory.get(); container = kieServices.getKieClasspathContainer(); statefulKieSession = container.newKieSession("myAgeSession"); User user = new User("duval yang",12); statefulKieSession.insert(user); statefulKieSession.fireAllRules(); statefulKieSession.dispose(); } }
esper
Esper is a component for complex event processing (CEP), streaming SQL and event series analysis, available for Java as Esper, and for .NET as NEsper.
一個例子:
public class Test { public static void main(String[] args) throws InterruptedException { EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider(); EPAdministrator admin = epService.getEPAdministrator(); String product = Apple.class.getName(); String epl = "select avg(price) from " + product + ".win:length_batch(3)"; EPStatement state = admin.createEPL(epl); state.addListener(new AppleListener()); EPRuntime runtime = epService.getEPRuntime(); Apple apple1 = new Apple(); apple1.setId(1); apple1.setPrice(5); runtime.sendEvent(apple1); Apple apple2 = new Apple(); apple2.setId(2); apple2.setPrice(2); runtime.sendEvent(apple2); Apple apple3 = new Apple(); apple3.setId(3); apple3.setPrice(5); runtime.sendEvent(apple3); } }
drools和esper都是比較重的規則引擎,詳見其官方文件。
動態JVM語言
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動態對json物件進行filter的例子:
public class GroovyFilter implements Filter { private static String template = "" + "package com.alarm.eagle.filter;" + "import com.fasterxml.jackson.databind.node.ObjectNode;" + "def match(ObjectNode o){[exp]}"; private static String method = "match"; private String filterExp; private transient GroovyObject filterObj; public GroovyFilter(String filterExp) throws Exception { ClassLoader parent = Thread.currentThread().getContextClassLoader(); GroovyClassLoader classLoader = new GroovyClassLoader(parent); Class clazz = classLoader.parseClass(template.replace("[exp]", filterExp)); filterObj = (GroovyObject)clazz.newInstance(); } public boolean filter(ObjectNode objectNode) { return (boolean)filterObj.invokeMethod(method, objectNode); } }
Java每次呼叫Groovy程式碼都會將Groovy編譯成Class檔案,因此在呼叫過程中會出現JVM級別的問題。如使用GroovyShell的parse方法導致perm區爆滿的問題,使用GroovyClassLoader載入機制導致頻繁gc問題和CodeCache用滿,導致JIT禁用問題等,相關問題可以參考Groovy與Java整合常見的坑 。
參考:
Java各種規則引擎:https://www.jianshu.com/p/41ea7a43093c
Java中使用動態程式碼:http://brucefengnju.github.io/post/dynamic-code-in-java/
量身定製規則引擎,適應多變業務場景:https://my.oschina.net/yygh/blog/616808?p=1