Java中动态规则的实现方式
背景
业务系统在应用过程中,有时候要处理“经常变化”的部分,这部分需求可能是“业务规则”,也可能是“不同的数据处理逻辑”,这部分动态规则的问题,往往需要可配置,并对性能和实时性有一定要求。
Java不是解决动态层问题的理想语言,在实践中发现主要有以下几种方式可以实现:
- 表达式语言(expression language)
- 动态语言(dynamic/script 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