数字表达式解析工具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;
    }
}

以上,感谢阅读,如有错误,请不吝指教,谢谢

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