數字表達式解析工具jeval實現自定義函數詳解

因爲系統有許多需要加工數據的需求,爲了避免重複開發同樣的功能,決定引入jeval工具類,實現特殊處理可配置。可能這樣描述讀者還是一知半解,且聽我一一道來。

1. jeval簡述

大部分能看見這篇博客的讀者,肯定或多或少是提前瞭解過這個工具的,但是爲了整篇博客不那麼突兀,還是做個簡單的介紹

2.自定義函數實現

package cn.newhope.de.expr.function.array;

import cn.newhope.de.expr.EvaluatorUtil;
import cn.newhope.de.expr.util.JSONArrayUtil;
import cn.newhope.de.expr.util.PatternUtil;
import cn.newhope.de.util.Base64Util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import net.sourceforge.jeval.Evaluator;
import net.sourceforge.jeval.function.*;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 根據條件 判斷獲取數組最小值
 * arrayMin(數組,列名,條件)
 * eg:
 * arrayMin(#{datas},age,'equals(test,'@{name}')')
 * arrayMin(#{datas},age,'test==@{name}')
 *
 * arrayMin("#{data.BwjkGaofa.bizResponse.info}","filing_time","equals(""天津"",""@{province}"")")
 */
public class ArrayMinFunction implements Function {

    @Override
    public String getName() {
        return "arrayMin";
    }

    @Override
    public FunctionResult execute(Evaluator evaluator, String arguments) throws FunctionException {
         String result=null;
        try {
            List<String> list= PatternUtil.splitCSV(arguments);
            if (list.size()!=3){
                throw new FunctionException("參數異常");
            }
            String data = list.get(0);
            String prop = list.get(1);
            String condition = list.get(2);
            String filter=PatternUtil.replaceFilter(condition);
            //先根據條件查到符合條件的數組,然後求最小值
            JSONArray jsonArray=JSONArray.parseArray(data);
            String key=PatternUtil.getKeyByCondition(filter);
            List<JSONObject> filterList= Lists.newArrayList();
            for (Object o:jsonArray) {
                JSONObject jsonObject=JSONObject.parseObject(o.toString());
                EvaluatorUtil.putVariable(key,jsonObject.getString(key));
                if(!"0.0".equals(EvaluatorUtil.exEvaluate(filter))){
                    filterList.add(jsonObject);
                }
            }
            //這個是自己實現的工具類,用於求對象數組的某一列最小值,讀者可實現自己的,ps: 如果你的自定義函數能通過過濾條件走到這裏,基本這個函數的使用你就會用了
            result = JSONArrayUtil.arrayMin(jsonArray, prop);
            result.replace("\"","\"\"");
        } catch (Exception e) {  throw new FunctionException(this.getName() + ": evalute error!  " + arguments +"\n" +e.getMessage(), e); }

        return new FunctionResult(String.valueOf(result), FunctionConstants.FUNCTION_RESULT_TYPE_STRING);
    }
}

我先簡單說一下實現一個自定義函數要注意的:

  1. 所有創建的自定義函數都要實現jeval的function接口
  2. function 接口需要實現兩個功能,一個是自定義函數的名字,後面會通過這個名字配置你的表達式,另一個是具體實現邏輯,即數據和參數傳進來你打算如何處理
  3. 如果你想要創建的核心類evaluator使用這個函數,需要在使用前用evaluator的api,evaluator.putfunction(new yourfunction),先把你的自定義函數加載進去,否則會報錯這個函數找不到
  4. jeval使用 ‘,’作爲參數分隔符,所以要留意參數裏面不能包含‘,’,一定要包含的話需要用引號引起來,否則會報錯格式錯誤
  5. jeval默認使用‘’ 來標註字符串,但是我上面的例子在初始化的時候選擇了“”標註字符串,主要還是爲了和我們正常感官一致,運行我上面的demo需要初始化evaluator的時候留意。如下初始化
	private static final Evaluator evaluator = new Evaluator(EvaluationConstants.DOUBLE_QUOTE, true, true, true, true);

上面這個函數我大概說一下功能,到時候會傳進來三個參數,一個是源數據,一個是要求對象數組的最小值的屬性,最後一個是篩選條件,這個條件可以是簡單過濾條件 如name=‘liming’,可以是jeval內置函數,也可以是你的其他自定義函數

package cn.newhope.de.expr.util;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class PatternUtil {

    //表達式條件正則,需要嵌套函數的時候會用到
    private static Pattern pattern = Pattern.compile("@\\{([^}]+)\\}");
    //表達式正則
    private static Pattern pattCond = Pattern.compile("#\\{([^}]+)\\}");


    /**
     * 用於獲取表達式裏面的key
     *
     * @param expr
     * @return
     */
    public static String getKeyByCondition(String expr) {

        Matcher matcher = pattCond.matcher(expr);
        while (matcher.find()) {
            return matcher.group(1);
        }
        return "";
    }

    /**
     * 替換表達式條件裏面的特殊字符
     * 防止分割條件字符串存在分割符
     * 這裏主要是爲了防止篩選條件裏面出現特殊的符號,如@,這樣如果直			      接替換的話可能會把本來不應該替換的字符替換掉,所以想了個辦法是一點一點截取替換,每次匹配到需要的正則表達式,先把這部分截取下來替換裏面的字符,再拼接回去,匹配下一個
     * @param
     * @return
     */
    public static String replaceFilter(String filter) {

        Matcher matcher = pattern.matcher(filter);
        while (matcher.find()) {
            String originalStr = matcher.group();
            String newStr = originalStr.replace('@', '#');
            filter = filter.replace(originalStr, newStr);
        }
        return filter;
    }

    /**
    * @Description:  解析自定義函數參數
    * 這個方法主要是爲了切分配置的複雜表達式
    * 因爲jeval解析的時候會校驗格式,如果表達式不標準就會報錯,
    * 而jeval默認是用,分割,字符串用‘’,可以想見傳的數據很可能是帶逗號、#、{}、等特殊字符的,這樣解析會出現報錯
    * 目前常見方法有兩種
    * 1、對可能出現不規則數據的表達式參數先進行base64加密,傳入之後再對數據進行base64解密,另外因爲你很可能需要嵌套執行,所以對於自定義函數處理結果,需要進行再次加密,方便外面的嵌套函數執行,這樣可以正常實現,但是可能會對性能有影響
    * 2、實現一個正則表達式,他切割字符串不會考慮字符串裏面本來包含的特殊字符,因爲csv是一種比較常見並且通用的格式,我們如果能把參數按照csv 的格式拼接起來,並且能正確解析就能實現我們的目標
  	前面說了正因爲csv常見,有現成成熟的正則可以使用,我們先按照csv格式組裝參數,然後在函數內部再根據正則切分得到我們想要的參數
    */
    public static List<String> splitCSV(String txt) {

        String reg = "\\G(?:^|,)(?:\"([^\"]*+(?:\"\"[^\"]*+)*+)\"|([^\",]*+))";
        Matcher matcherMain = Pattern.compile(reg).matcher(txt);
        Matcher matcherQuoto = Pattern.compile("\"\"").matcher("");
        List strList = new ArrayList();
        while (matcherMain.find()) {
            String field;
            if (matcherMain.start(2) >= 0) {
                field = matcherMain.group(2);
            } else {
                field = matcherQuoto.reset(matcherMain.group(1)).replaceAll("\"");
            }
            strList.add(field);
        }
        return strList;
    }
}

這塊因爲我也是用不久,有些東西可能描述的不夠清楚,讀者有問題可以直接評價或者郵箱發我,大家互相學習,後續也會針對問題逐漸更新這邊博文
總結一下吧:

  1. 首先創建自己的自定義函數實現jeval的function接口
  2. 然後定義自己的函數名字,後面你配置的表達式要和這個名字一致
  3. 創建evaluator實體類,這邊可以做一些自定義的修改,比如字符串標誌的單引號改爲雙引號,是否加載自帶的數學函數,字符串函數等
  4. 最關鍵的一步,如何定義你的表達式,即你想實現的功能。要傳過去的數據,以及其他參數,比如加個條件,age=12, 比如你想求對象數組某一屬性的最大值,那你就需要傳過去源數據和屬性名等等
    5.關於組裝表達式,建議用筆者的這種方法,組裝的表達式易於分析和拼接,即字符串用“”標註,格式用csv格式

最後貼一個測試類說明參數是怎麼組裝的吧:

package array;
import cn.newhope.de.expr.EvaluatorUtil;
import cn.newhope.de.expr.function.array.ArrayMinFunction;
import cn.newhope.de.util.Base64Util;
import com.alibaba.fastjson.JSONObject;
import net.sourceforge.jeval.Evaluator;
import java.util.Arrays;
import java.util.List;
public class ArrayMinTest {
    public static void main(String[] args) throws Exception{
        Evaluator evaluator= EvaluatorUtil.getEvaluator();
        String expr="arrayMin(\"#{datas}\",\"age\",\"sfsf\")";
        Students students1=new Students(12,"zhangsan");
        Students students2=new Students(15,"lisi");
        Students students3=new Students(34,"wangwu");
        List list= Arrays.asList(students1,students2,students3);
        String listStr= JSONObject.toJSONString(list);
        evaluator.putFunction(new ArrayMinFunction());
        //evaluator.putVariable("datas", Base64Util.encode(listStr.getBytes()));
        //轉義字符串中的“”,讓csv正則能正確識別
        listStr=listStr.replace("\"","\"\"");
        evaluator.putVariable("datas", listStr);
        String result=evaluator.evaluate(expr);
       // System.out.println(Base64Util.decode(result));
    }
}

class Students {

    int age;

    String name;

    public Students(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

以上,感謝閱讀,如有錯誤,請不吝指教,謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章