Java中動態規則的實現方式

阿凡盧發表於2020-08-25

背景

業務系統在應用過程中,有時候要處理“經常變化”的部分,這部分需求可能是“業務規則”,也可能是“不同的資料處理邏輯”,這部分動態規則的問題,往往需要可配置,並對效能和實時性有一定要求。

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 ,SpELJUELJava Expression Language (JEXL)JEvalJakarta JXPath 等。

這裡主要介紹在實踐中使用較多的MVEL、OGNL和SpEL。

OGNL(Object Graph Navigation Library)

在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):aviatoreasy-rulesdroolsesper

aviator

AviatorScript 是一門高效能、輕量級寄宿於 JVM 之上的指令碼語言。

使用場景包括:

  1. 規則判斷及規則引擎
  2. 公式計算
  3. 動態指令碼控制
  4. 集合資料 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

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);
    }
}

drools

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

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

 

相關文章