聊聊我們那些年用過的表示式引擎元件

linyb极客之路發表於2024-11-12

前言

我們在設計一些表單或者流程引擎時,可能我們會設計各種各樣的表示式或者規則,我們透過各種表示式或者規則來實現我們的業務流轉。今天就來盤點一下我們經常會使用到的表示式引擎

常用表示式引擎

1、spring el

官方文件

https://docs.spring.io/spring-framework/reference/core/expressions.html

官方示例
https://github.com/spring-projects/spring-framework/tree/master/spring-expression

Spring Expression Language (SpEL) 是Spring框架中的一個強大的表示式語言,用於在執行時查詢和操作物件圖。以下是關於Spring EL的幾個關鍵點:

動態查詢和操作: SpEL允許你在執行時執行復雜的查詢和運算元據,比如讀取bean的屬性值、呼叫方法、進行算術運算、邏輯判斷等。
整合於Spring框架: SpEL廣泛應用於Spring的各種模組中,如Spring Security的訪問控制表示式、Spring Data的查詢條件定義、Spring Integration的訊息路由等。
基本語法: SpEL表示式通常被包含在#{...}中,例如#{property}用來獲取一個bean的屬性值。它支援字串、布林、算術、關係、邏輯運算子,以及方法呼叫、陣列和列表索引訪問等。
上下文感知: SpEL能夠訪問Spring應用上下文中的Bean,這意味著你可以直接在表示式中引用配置的bean,實現高度靈活的配置和執行時行為調整。
型別轉換: SpEL提供了內建的型別轉換服務,可以自動或顯式地將一種型別的值轉換為另一種型別。
安全考量: 使用SpEL時需要注意安全性,避免注入攻擊。Spring提供了ExpressionParser的配置來限制表示式的執行能力,如禁用方法呼叫或屬性訪問等。
例子:

  • 訪問Bean屬性: #{myBean.propertyName}
  • 方法呼叫: #{myBean.myMethod(args)}
  • 三元運算子: #{condition ? trueValue : falseValue}
  • 列表和陣列訪問: #{myList[0]}

    • 算術運算: #{2+3}

spel工具類

public class SpringExpressionUtil {

    private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();

    private SpringExpressionUtil(){}

    /**
     * Evaluates the given Spring EL expression against the provided root object.
     * 
     * @param rootObject The object to use as the root of the expression evaluation.
     * @param expressionString The Spring EL expression to evaluate.
     * @param returnType The expected return type.
     * @return The result of the expression evaluation.
     */
    public static <T> T evaluateExpression(Map<String, Object> rootObject, String expressionString, Class<T> returnType) {
        StandardEvaluationContext context = new StandardEvaluationContext(rootObject);
        rootObject.forEach(context::setVariable);
        return EXPRESSION_PARSER.parseExpression(expressionString).getValue(context,returnType);
    }



    public static void main(String[] args) {
        Map<String,Object> map = new HashMap<>();
        map.put("name","lybgeek");
        map.put("hello","world");
        System.out.println(evaluateExpression(map,"#root.get('name')",String.class));

    }
}
2、ognl

官方文件
https://ognl.orphan.software/language-guide

官方示例
https://github.com/orphan-oss/ognl

OGNL (Object-Graph Navigation Language) 是一個強大的表示式語言,用於獲取和設定Java物件的屬性。它在許多Java框架中被用作資料繫結和操作物件圖的工具,最著名的應用是在Apache Struts2框架中。以下是關於OGNL的一些關鍵特性:
簡單表示式: OGNL允許你以簡單的字串形式編寫表示式來訪問物件屬性,如person.name就可以獲取person物件的name屬性。
鏈式導航: 支援鏈式呼叫來深入物件圖,例如customer.address.street會依次導航到customer的address屬性,再從address獲取street。
集合操作: OGNL可以直接在表示式中處理集合和陣列,包括遍歷、篩選、投影等操作,如customers.{name}可以獲取所有customers集合中每個元素的name屬性。
上下文敏感: OGNL表示式解析時會考慮一個上下文環境,這個環境包含了變數、物件和其他表示式可能需要的資訊。
方法呼叫與構造器: 除了屬性訪問,OGNL還支援呼叫物件的方法和構造新物件,如@myUtil.trim(name)呼叫工具類方法,或new java.util.Date()建立新物件。
條件與邏輯運算: 支援if、else邏輯,以及&&、||等邏輯運算子,使得表示式可以處理更復雜的邏輯判斷。
變數賦值: OGNL不僅能夠讀取資料,還能設定物件屬性的值,如person.name = "Alice"。
安全問題: 和SpEL一樣,使用OGNL時也需注意表示式注入的安全風險,確保使用者輸入不會被直接用於構造表示式,以防止惡意操作。
OGNL以其簡潔的語法和強大的功能,在處理物件關係和資料繫結方面非常實用,尤其是在需要動態操作物件和集合的場景下。

ognl工具類

public class OgnlExpressionUtil {


    private OgnlExpressionUtil(){}

    /**
     * Evaluates the given Ognl EL expression against the provided root object.
     * 
     * @param rootObject The object to use as the root of the expression evaluation.
     * @param expressionString The OGNL EL expression to evaluate.
     * @param returnType The expected return type.
     * @return The result of the expression evaluation.
     */
    public static <T> T evaluateExpression(Map<String, Object> rootObject, String expressionString, Class<T> returnType) {
        Object value = OgnlCache.getValue(expressionString, rootObject);
        if(value != null && value.getClass().isAssignableFrom(returnType)){
            return (T)value;
        }

        return null;
    }

    public static void main(String[] args) {
        Map<String,Object> map = new HashMap<>();
        map.put("name","lybgeek");
        map.put("hello","world");
        System.out.println(OgnlExpressionUtil.evaluateExpression(map,"#root.name",String.class));

        System.out.println(SpringExpressionUtil.evaluateExpression(map,"#root.get('hello')",String.class));
    }
}
3、Aviator

官方文件
http://fnil.net/aviator/

官方示例
https://github.com/killme2008/aviatorscript

Aviator是一個輕量級的Java表示式執行引擎,它設計用於高效能的動態計算場景,特別是那些需要在執行時解析和執行復雜表示式的應用場景。以下是Aviator的一些核心特點和功能:
高效能: Aviator最佳化了表示式的編譯和執行過程,特別適合於對效能有嚴格要求的系統,如金融風控、實時計算等領域。
易於整合: 提供簡單的API介面,使得在Java專案中嵌入Aviator變得非常容易,只需引入依賴,即可開始編寫和執行表示式。
豐富的表示式支援: 支援數學運算、邏輯運算、比較運算、位運算、字串操作、三元運算、變數定義與引用、函式呼叫等,幾乎覆蓋了所有常見的運算需求。
安全沙箱模式: Aviator提供了沙箱機制,可以限制表示式的執行許可權,比如禁止訪問某些方法或欄位,從而提高應用的安全性。
動態指令碼執行: 允許在執行時動態載入和執行指令碼,非常適合用於規則引擎、配置驅動的系統邏輯等場景。
JIT編譯: Aviator採用即時編譯技術,將表示式編譯成Java位元組碼執行,進一步提升執行效率。
資料繫結: 可以方便地將Java物件、Map、List等資料結構繫結到表示式上下文中,實現表示式與Java資料的無縫對接。
擴充套件性: 支援自定義函式,使用者可以根據需要擴充套件Aviator的功能,增加特定業務邏輯的處理能力。
Aviator因其高效能和靈活性,在需要動態指令碼處理的場景中,特別是在那些對效能敏感且需要頻繁執行復雜計算邏輯的應用中,是一個非常有吸引力的選擇。

Aviator工具類

public final class AviatorExpressionUtil {


    private AviatorExpressionUtil() {
    }

    /**
     * 執行Aviator表示式並返回結果
     *
     * @param expression Aviator表示式字串
     * @param env        上下文環境,可以包含變數和函式
     * @return 表示式計算後的結果
     */
    public static <T> T evaluateExpression(Map<String, Object> env,String expression, Class<T> returnType) {
        Object value = AviatorEvaluator.execute(expression, env);
        if(value != null && value.getClass().isAssignableFrom(returnType)){
            return (T)value;
        }

        return null;

    }

    public static void main(String[] args) {
        Map<String,Object> map = new HashMap<>();
        map.put("name","lybgeek");
        map.put("hello","world");

        Map<String,Object> env = new HashMap<>();
        env.put("root",map);
        System.out.println(evaluateExpression(env,"#root.name",String.class));
    }

}
4、Mvel2

官方文件
mvel.documentnode.com/

官方示例
https://github.com/mvel/mvel

MVEL2(MVFLEX Expression Language 2)是一種強大且靈活的Java庫,用於解析和執行表示式語言。它是MVEL專案的第二代版本,旨在提供高效、簡潔的方式來操作物件和執行邏輯。下面是關於MVEL2的一些關鍵特性和使用指南:
動態型別與靜態型別混合: MVEL支援動態型別,同時也允許靜態型別檢查,這意味著你可以選擇是否在編譯時檢查型別錯誤,增加了靈活性和安全性。
簡潔的語法: MVEL語法基於Java但更加簡潔,便於編寫和閱讀,適用於快速構建表示式和小型指令碼。
屬性訪問與方法呼叫: 類似於其他表示式語言,MVEL允許直接訪問物件屬性和呼叫其方法,如person.name或list.size()。
控制流語句: 支援if、else、switch、迴圈(for、while)等控制流結構,使得在表示式中實現複雜邏輯成為可能。
模板引擎: MVEL2提供了一個強大的模板引擎,可以用來生成文字輸出,類似於Velocity或Freemarker,但與MVEL表示式無縫整合。
變數賦值與函式定義: 直接在表示式中定義變數和函式,支援區域性變數和閉包(匿名函式)。
資料繫結與轉換: 自動或手動進行型別轉換,簡化了不同資料型別間的操作。
整合與擴充套件: MVEL設計為易於整合到現有Java專案中,同時提供了擴充套件點,允許使用者定義自定義函式和運算子。
效能最佳化: MVEL關注執行效率,透過最佳化的編譯器和執行引擎來減少執行時開銷。

5、Hutool表示式引擎門面

官方文件
https://doc.hutool.cn/pages/ExpressionUtil/#介紹

hutool工具包在5.5.0版本之後,提供了表示式計算引擎封裝為門面模式,提供統一的API,去除差異。目前支援如下表示式引擎

  • Aviator
  • Apache Jexl3
  • MVEL
  • JfireEL
  • Rhino
  • Spring Expression Language
    (SpEL)

如上所述的表示式引擎不能滿足要求,hutool還支援透過SPI進行自定義擴充套件

基於hutool封裝的工具類

public class HutoolExpressionUtil {


    private HutoolExpressionUtil(){}


    /**
     * 執行表示式並返回結果。
     *
     * @param expression 表示式字串
     * @param variables  變數對映,鍵為變數名,值為變數值
     * @return 表示式計算後的結果
     */
    public static <T> T evaluateExpression(Map<String, Object> variables,String expression, Class<T> returnType) {
        try {
            Object value = ExpressionUtil.eval(expression, variables);
            if(value != null && value.getClass().isAssignableFrom(returnType)){
                return (T)value;
            }
        } catch (Exception e) {
            throw new RuntimeException("Error executing  expression: " + expression, e);
        }

        return null;
    }


    public static void main(String[] args) {
        Map<String,Object> map = new HashMap<>();
        map.put("name","lybgeek");
        map.put("hello","world");

        Map<String,Object> variables = new HashMap<>();
        variables.put("root",map);
        System.out.println(evaluateExpression(variables,"root.name",String.class));
    }
}

總結

本文介紹了市面比較常用的表示式引擎元件,而這些引擎基本上都可以用hutool提供的表示式門面實現,hutool確實在工具類這方面做得很好,基本上我們日常會用到的工具,它大部分都涵蓋到。最後文末demo連結,也提供了跟spring整合的表達引擎聚合實現,大家感興趣也可以看看。

demo連結

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-el

相關文章