背景
業務系統在應用過程中,有時候要處理“經常變化”的部分,這部分需求可能是“業務規則”,也可能是“不同的數據處理邏輯”,這部分動態規則的問題,往往需要可配置,並對性能和實時性有一定要求。
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,siddhi
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(); } }
drools是比較重的規則引擎,有自己的狀態存儲,詳見其官方文檔。
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 static void main(String args[]) { 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 StatementAwareUpdateListener() { @Override public void update(EventBean[] newEventBeans, EventBean[] oldEventBeans, EPStatement epStatement, EPServiceProvider epServiceProvider) { if (newEventBeans != null && newEventBeans.length > 0) { Double avg = (Double) newEventBeans[0].get("avg(price)"); System.out.println("Avg price: " + avg); } } }); 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); }
Siddhi is a cloud native Streaming and Complex Event Processing engine that understands Streaming SQL queries in order to capture events from diverse data sources, process them, detect complex conditions, and publish output to various endpoints in real time.
For example:
package io.siddhi.sample; import io.siddhi.core.SiddhiAppRuntime; import io.siddhi.core.SiddhiManager; import io.siddhi.core.event.Event; import io.siddhi.core.stream.input.InputHandler; import io.siddhi.core.stream.output.StreamCallback; import io.siddhi.core.util.EventPrinter; /** * The sample demonstrate how to use Siddhi within another Java program. * This sample contains a simple filter query. */ public class SimpleFilterSample { public static void main(String[] args) throws InterruptedException { // Create Siddhi Manager SiddhiManager siddhiManager = new SiddhiManager(); //Siddhi Application String siddhiApp = "" + "define stream StockStream (symbol string, price float, volume long); " + "" + "@info(name = 'query1') " + "from StockStream[volume < 150] " + "select symbol, price " + "insert into OutputStream;"; //Generate runtime SiddhiAppRuntime siddhiAppRuntime = siddhiManager.createSiddhiAppRuntime(siddhiApp); //Adding callback to retrieve output events from stream siddhiAppRuntime.addCallback("OutputStream", new StreamCallback() { @Override public void receive(Event[] events) { EventPrinter.print(events); //To convert and print event as a map //EventPrinter.print(toMap(events)); } }); //Get InputHandler to push events into Siddhi InputHandler inputHandler = siddhiAppRuntime.getInputHandler("StockStream"); //Start processing siddhiAppRuntime.start(); //Sending events to Siddhi inputHandler.send(new Object[]{"IBM", 700f, 100L}); inputHandler.send(new Object[]{"WSO2", 60.5f, 200L}); inputHandler.send(new Object[]{"GOOG", 50f, 30L}); inputHandler.send(new Object[]{"IBM", 76.6f, 400L}); inputHandler.send(new Object[]{"WSO2", 45.6f, 50L}); Thread.sleep(500); //Shutdown runtime siddhiAppRuntime.shutdown(); //Shutdown Siddhi Manager siddhiManager.shutdown(); } }
esper和siddhi都是streaming process,支持CEP和SQL,詳見其官方文檔。
動態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語言集成。
1)ScriptEngineManager
按照JSR223,使用標准接口ScriptEngineManager調用。
ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine = factory.getEngineByName("groovy");// 每次生成一個engine實例 Bindings binding = engine.createBindings(); binding.put("date", new Date()); // 入參 engine.eval("def getTime(){return date.getTime();}", binding);// 如果script文本來自文件,請首先獲取文件內容 engine.eval("def sayHello(name,age){return 'Hello,I am ' + name + ',age' + age;}"); Long time = (Long) ((Invocable) engine).invokeFunction("getTime", null);// 反射到方法 System.out.println(time); String message = (String) ((Invocable) engine).invokeFunction("sayHello", "zhangsan", 12); System.out.println(message);
2)GroovyShell
Groovy官方提供GroovyShell,執行Groovy腳本片段,GroovyShell每一次執行時代碼時會動態將代碼編譯成Java Class,然后生成Java對象在Java虛擬機上執行,所以如果使用GroovyShell會造成Class太多,性能較差。
final String script = "Runtime.getRuntime().availableProcessors()"; Binding intBinding = new Binding(); GroovyShell shell = new GroovyShell(intBinding); final Object eval = shell.evaluate(script); System.out.println(eval);
3)GroovyClassLoader
Groovy官方提供GroovyClassLoader類,支持從文件、url或字符串中加載解析Groovy Class,實例化對象,反射調用指定方法。
GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); String helloScript = "package com.test.groovy.util" + // 可以是純Java代碼 "class Hello {" + "String say(String name) {" + "System.out.println(\"hello, \" + name)" + " return name;" "}" + "}"; Class helloClass = groovyClassLoader.parseClass(helloScript); GroovyObject object = (GroovyObject) helloClass.newInstance(); Object ret = object.invokeMethod("say", "test"); // 控制台輸出"hello, test" System.out.println(ret.toString()); // 打印test
Java每次調用Groovy代碼都會將Groovy編譯成Class文件,因此在調用過程中會出現JVM級別的問題。如使用GroovyShell的parse方法導致perm區爆滿的問題,使用GroovyClassLoader加載機制導致頻繁gc問題和CodeCache用滿,導致JIT禁用問題等,相關問題可以參考Groovy與Java集成常見的坑 。
解決方案
-
對於 parseClass 后生成的 Class 對象進行緩存,key 為 Groovy腳本的md5值,並且在配置端修改配置后可進行緩存刷新。這樣做的好處有兩點:(1)解決Metaspace爆滿的問題;(2)因為不需要在運行時編譯加載,所以可以加快腳本執行的速度。
-
GroovyClassLoader的使用用參考Tomcat的ClassLoader體系,有限個GroovyClassLoader實例常駐內存,增加處理的吞吐量。
-
腳本靜態化:Groovy腳本里面盡量都用Java靜態類型,可以減少Groovy動態類型檢查等,提高編譯和加載Groovy腳本的效率。
JavaScript
JavaSE6中自帶了JavaScript語言的腳本引擎,基於Mozilla的Rhino實現,通過腳本引擎,可以動態執行“規則”:
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); try { engine.eval("function add(a,b){" + "return a+b;" + "}"); if (engine instanceof Invocable) { Invocable in = (Invocable) engine; System.out.println(in.invokeFunction("add", 1, 1)); } } catch (Exception e) { e.printStackTrace(); }
參考:
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
Drools 規則引擎探究以及在 IOT 的應用:https://www.infoq.cn/article/jhSIS8qKp3WRHeUoq5uT