当前位置: 首页 > 工具软件 > QLExpress > 使用案例 >

QLExpress规则引擎使用笔记

柳深
2023-12-01

目录

QLExpress支持哪些操作符?

手机号前缀规则 demo

地址规则 demo

地址+手机号前缀组合demo

自定义操作符

自定义的对象(例如Spring对象)方法转换为表达式

QLExpress设置成单例(关键)


QLExpress支持哪些操作符?

//参考com.ql.util.express.instruction.op.OperatorFactory	 
 public OperatorFactory(boolean aIsPrecise){
		  this.isPrecise = aIsPrecise;
		  addOperator("new",new OperatorNew("new"));
		  addOperator("anonymousNewArray",new OperatorAnonymousNewArray("anonymousNewArray"));
		  addOperator("NewList",new OperatorAnonymousNewList("NewList"));		  
		  addOperator(":",new OperatorKeyValue(":"));
		  addOperator("NewMap",new OperatorAnonymousNewMap("NewMap"));
		  addOperator("def",  new OperatorDef("def"));
		  addOperator("exportDef",  new OperatorExportDef("exportDef"));
		  addOperator("!",new OperatorNot("!"));
		  addOperator("*", new OperatorMultiDiv("*"));
		  addOperator("/", new OperatorMultiDiv("/"));
		  addOperator("%", new OperatorMultiDiv("%"));
		  addOperator("mod", new OperatorMultiDiv("mod"));
		  addOperator("+",new OperatorAdd("+"));
		  addOperator("-",new OperatorReduce("-"));
		  addOperator("<",new OperatorEqualsLessMore("<"));
		  addOperator(">",new OperatorEqualsLessMore(">"));
		  addOperator("<=",new OperatorEqualsLessMore("<="));
		  addOperator(">=",new OperatorEqualsLessMore(">="));
		  addOperator("==",new OperatorEqualsLessMore("=="));
		  addOperator("!=",new OperatorEqualsLessMore("!="));
		  addOperator("<>",new OperatorEqualsLessMore("<>"));
		  addOperator("&&",new OperatorAnd("&&"));
		  addOperator("||",new OperatorOr("||"));
		  addOperator("nor",new OperatorNor("nor"));
		  addOperator("=",new OperatorEvaluate("="));
		  addOperator("exportAlias",new OperatorExportAlias("exportAlias"));
		  addOperator("alias",new OperatorAlias("alias"));
		  addOperator("break",new OperatorBreak("break"));
		  addOperator("continue",new OperatorContinue("continue"));
		  addOperator("return",new OperatorReturn("return"));		  
		  addOperator("ARRAY_CALL",new OperatorArray("ARRAY_CALL"));
		  addOperator("++",new OperatorDoubleAddReduce("++"));
		  addOperator("--",new OperatorDoubleAddReduce("--"));
		  addOperator("cast", new OperatorCast("cast"));
		  addOperator("macro",new OperatorMacro("macro"));
		  addOperator("function",new OperatorFunction("function"));
		  addOperator("in", new OperatorIn("in"));	
		  addOperator("like", new OperatorLike("like"));
		  //bit operator
          addOperator("&",new OperatorBit("&"));
          addOperator("|",new OperatorBit("|"));
          addOperator("^",new OperatorBit("^"));
          addOperator("~",new OperatorBit("~"));
          addOperator("<<",new OperatorBit("<<"));
          addOperator(">>",new OperatorBit(">>"));
		}

 

  • 手机号前缀规则 demo

    private final ExpressRunner expressRunner = new ExpressRunner();

基础用法:  返回true or false

    @Test
    public void mobilePrefixCheck() throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("mobilePrefix", "162");
        String express = "mobilePrefix in (162,165,167,170,171)";
        Object execute = expressRunner.execute(express, context, null, false, false);
        System.out.println(execute);
    }

进阶用法:  当执行器返回false时,额外赋值自定义错误信息

    @Test
    public void mobilePrefixCheck() throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("mobilePrefix", "162");
        String express = "mobilePrefix in (162,165,167,170,171)";
        //没有合适的设置errorInfo方法 ,无奈用重命名操作符方法来设置errorInfo;errorInfo只针对替换的操作符起作用
        expressRunner.addOperatorWithAlias("等于", "==", "号码不满足期望");
        List<String> errorInfo = new ArrayList<>();
        Object execute = expressRunner.execute(express, context, errorInfo, false, false);
        System.out.println(execute);
        System.out.println(errorInfo.size() > 0 ? errorInfo.get(0) : null);
    }
  • 地址规则 demo

    @Test
    public void addressCheck() throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("city", "郑州");
        context.put("address", "二七");
        String express = "city in ('郑州','天庭') and address in ('二七','大上海','新郑')";
        Object execute = expressRunner.execute(express, context, null, false, false);
        System.out.println(execute);
    }
  • 地址+手机号前缀组合demo

    @Test
    public void addressCheck2() throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        // 地址
        context.put("country", "中国");
        context.put("province", "河南");
        context.put("city", "郑州");
        context.put("address", "二七");
        context.put("postcode", 1234);
        // 手机号
        context.put("mobile", "162");

        // 表达式
        String express = "" +
                "country in ('中国','美国') " +
                "and province in ('河南','浙江') " +
                "and city in ('郑州','杭州') " +
                "and address in ('二七','大上海','新郑') " +
                "and postcode in ('1234')" +
                "and (mobile in (162,165,167,170,171) 等于 false)";
        expressRunner.addOperatorWithAlias("等于", "==", "号码或地址不满足期望");
        List<String> errorInfo = new ArrayList<>();
        Object execute = expressRunner.execute(express, context, errorInfo, false, false);
        System.out.println(execute);
        System.out.println(errorInfo.size() > 0 ? errorInfo.get(0) : null);
    }

 

自定义操作符

 

官方文档是对于自定义操作符是这样介绍的:

//定义一个继承自com.ql.util.express.Operator的操作符
public class JoinOperator extends Operator{
	public Object executeInner(Object[] list) throws Exception {
		Object opdata1 = list[0];
		Object opdata2 = list[1];
		if(opdata1 instanceof java.util.List){
			((java.util.List)opdata1).add(opdata2);
			return opdata1;
		}else{
			java.util.List result = new java.util.ArrayList();
			result.add(opdata1);
			result.add(opdata2);
			return result;				
		}
	}
}
//(1)addOperator
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addOperator("join",new JoinOperator());
Object r = runner.execute("1 join 2 join 3", context, null, false, false);
System.out.println(r);
//返回结果  [1, 2, 3]

//(2)replaceOperator
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.replaceOperator("+",new JoinOperator());
Object r = runner.execute("1 + 2 + 3", context, null, false, false);
System.out.println(r);
//返回结果  [1, 2, 3]

//(3)addFunction
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addFunction("join",new JoinOperator());
Object r = runner.execute("join(1,2,3)", context, null, false, false);
System.out.println(r);
//返回结果  [1, 2, 3]

自己实现基于手机号截取后匹配in的规则:

这个规则是基于in操作符的二次改造,所有我们不用按官方的继承Operator,而是继承它的子类OperatorIn,构造方法可以设置截取字段的起始和结束(默认0,3)


import com.ql.util.express.instruction.op.OperatorIn;

/**
 * 自定义ELPress操作符->手机号前缀操作符 (该方法可用于所有字符截取后in操作的场景)
 * <p>
 * ExpressRunner expressRunner = new ExpressRunner();
 * <p>
 * expressRunner.addOperator("prefix", new MobilePrefixInOperator(0, 3));
 * <p>
 * String express = "mobile prefix [162,165,167,170,171]" // []代替{}
 * <p>
 * <a href='' style='color:red;font-weight:900:'>
 * 注意可以使用 mobile in (162,165,167) ,但无法使用data prefix (162,165,167),因为默认addOperator的是二元操作符o(╥﹏╥)o
 * <p>
 * {@link com.ql.util.express.instruction.InInstructionFactory}对语法树做了特殊处理
 * </a>
 *
 * @author yangning
 * @date 2021/4/16 2:10 下午
 **/
public class MobilePrefixInOperator extends OperatorIn {

    /**
     * OperatorIn默认传参
     * {@link com.ql.util.express.instruction.op.OperatorFactory}
     */
    public static final String IN = "in";
    /**
     * 手机号加密头
     */
    public static final String PREFIX_CRYPT = "crypt#";

    private Integer beginIndex = 0;

    private Integer endIndex = 3;

    public MobilePrefixInOperator() {
        super(IN);
    }

    public MobilePrefixInOperator(Integer beginIndex, Integer endIndex) {
        super(IN);
        this.beginIndex = beginIndex;
        this.endIndex = endIndex;
    }

    public MobilePrefixInOperator(String aName) {
        super(aName);
    }

    public MobilePrefixInOperator(String aAliasName, String aName, String aErrorInfo) {
        super(aAliasName, aName, aErrorInfo);
    }

    @Override
    public Object executeInner(Object[] list) throws Exception {
        // 手机号判断明文还是密文->截取前缀->执行内置in关键字Operator
        Object obj = list[0];
        if ((obj instanceof Number || obj instanceof String)) {
            String mobile = String.valueOf(obj);
            String mobilePrefix;
            if (mobile.startsWith(PREFIX_CRYPT)) {
                mobilePrefix = CryptUtil.decrypt(mobile).substring(beginIndex, endIndex);
            } else {
                mobilePrefix = mobile.substring(beginIndex, endIndex);
            }
            list[0] = mobilePrefix;
        }
        return super.executeInner(list);
    }
}

接下来我们试试自定义的操作符prefixIn,注意一个大坑,默认的in可以括号里加多个,但是自定义的操作符这样做就会报错,翻了下源码发现默认addOperator的是二元操作符o(╥﹏╥)o。

解决方法是用[]代替()

    @Test
    public void MobilePrefixOperatorCheck() throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("mobile", "17115847153");
        // 注意可以使用 mobile in (162,165,167) ,但无法使用data prefix (162,165,167),因为默认addOperator的是二元操作符o(╥﹏╥)o
        expressRunner.addOperator("prefix", new MobilePrefixInOperator(0, 3));
        String express = "mobile prefix [162,165,167,170,171] 等于 false";
        // 用等于替换==的唯一作用是false时往errorInfo添加自定义错误信息(官方api不支持直接设置错误信息)
        expressRunner.addOperatorWithAlias("等于", "==", "号码不满足期望");
        List<String> errorInfo = new ArrayList<>();
        Object execute = expressRunner.execute(express, context, errorInfo, false, false);
        System.out.println(execute);
        System.out.println(errorInfo.size() > 0 ? errorInfo.get(0) : null);
    }

 

 

自定义的对象(例如Spring对象)方法转换为表达式

首先,我们写一个自定义对象,实现我们想要的函数效果

import org.apache.commons.lang3.StringUtils;

/**
 * QLExpress->自定义Contains函数
 *
 * @author yangning
 * @date 2021/4/19 12:49 下午
 **/
public class ContainsFunction {

    public boolean contains(String str, String searchStr) {
        return str.contains(searchStr);
    }

    public boolean contains(String str, String... searchStr) {
        for (String s : searchStr) {
            if (str.contains(s)) {
                return true;
            }
        }
        return false;
    }

    public boolean containsIgnoreCase(String str, String searchStr) {
        return StringUtils.containsIgnoreCase(str, searchStr);
    }

    public boolean containsIgnoreCase(String str, String... searchStr) {
        for (String s : searchStr) {
            if (StringUtils.containsIgnoreCase(str, s)) {
                return true;
            }
        }
        return false;
    }
}

然后,我们应用到QLExpress中

    @Test
    public void addressCheck() throws Exception {
        ExpressRunner expressRunner = new ExpressRunner();

        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("city", "郑州");
        context.put("address", "w我二七123订单");
        expressRunner.addFunctionOfServiceMethod("contain", new ContainsFunction(), "contains",
                new Class[]{String.class, String.class}, null);
        expressRunner.addFunctionOfServiceMethod("contains", new ContainsFunction(), "contains",
                new Class[]{String.class, String[].class}, null);
        expressRunner.addFunctionOfServiceMethod("containsIgnoreCase", new ContainsFunction(), "containsIgnoreCase",
                new Class[]{String.class, String.class}, null);
        expressRunner.addFunctionOfServiceMethod("containsIgnoreCases", new ContainsFunction(), "containsIgnoreCase",
                new Class[]{String.class, String[].class}, null);
        String express = "city == '郑州' and contains (address,['二七','大上海'])";
        String s = "w我二七123订单";
        Object execute = expressRunner.execute(express, context, null, false, false);
        System.out.println(execute);
    }

 

QLExpress设置成单例(关键)

规则引擎QLExpress是一个重量级的对象,我们实际中使用它应该设置成一个单例。

我们先模拟一次多线程执行导致的异常:

com.yn.risk.management.system.express.operator.MobilePrefixInOperator;
import com.ql.util.express.DefaultContext;
import com.ql.util.express.ExpressRunner;
import com.ql.util.express.OperateData;
import lombok.SneakyThrows;

/**
 * @author yangning
 * @date 2021/4/22 6:35 下午
 **/
public class SynchronizedTest implements Runnable {
    public int a;

    public SynchronizedTest() {
    }

    public SynchronizedTest(int a) {
        this.a = a;
    }

    @SneakyThrows
    @Override
    public void run() {
        for (; ; ) {
            ExpressRunner runner = ExpressRunnerFactory.getInstance();
            Object execute;
            String express = "score == '100" + a + "'";
            DefaultContext<String, Object> context = new DefaultContext<>();
            MobilePrefixInOperator prefixInOperator = new MobilePrefixInOperator(1, a);
            // addCustomizeOperator(prefixInOperator, runner);
            runner.replaceOperator("prefix", prefixInOperator);
            context.put("score", "100" + a);
            execute = runner.execute(express, context, null, true, false);
            OperateData data = runner.getOperateDataCache().fetchOperateData(prefixInOperator, MobilePrefixInOperator.class);
            if (!data.toString().equals("prefix")) {
                System.out.println(data.toString());
                System.out.println(execute);
                break;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest1 = new SynchronizedTest(1);
        SynchronizedTest synchronizedTest2 = new SynchronizedTest(2);
        SynchronizedTest synchronizedTest3 = new SynchronizedTest(3);
        Thread t1;
        Thread t2;
        Thread t3;
        t1 = new Thread(synchronizedTest1, "线程1");
        t2 = new Thread(synchronizedTest2, "线程2");
        t3 = new Thread(synchronizedTest3, "线程3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
    }

    public static synchronized void addCustomizeOperator(MobilePrefixInOperator prefixInOperator, ExpressRunner runner) {
        // 手机号前缀规则校验
        runner.replaceOperator("prefix", prefixInOperator);
    }
}

输出

Exception in thread "线程2" Exception in thread "线程3" java.lang.RuntimeException: 重复定义操作符:prefix定义1:class com.yn.risk.management.system.express.operator.MobilePrefixInOperator 定义2:class com.yn.risk.management.system.express.operator.MobilePrefixInOperator
	at com.ql.util.express.instruction.op.OperatorFactory.addOperator(OperatorFactory.java:71)
	at com.ql.util.express.instruction.op.OperatorFactory.replaceOperator(OperatorFactory.java:79)
	at com.ql.util.express.ExpressRunner.replaceOperator(ExpressRunner.java:480)
	at com.yn.SynchronizedTest.run(SynchronizedTest.java:35)
	at java.lang.Thread.run(Thread.java:748)
java.lang.RuntimeException: 重复定义操作符:prefix定义1:class com.yn.risk.management.system.express.operator.MobilePrefixInOperator 定义2:class com.yn.risk.management.system.express.operator.MobilePrefixInOperator
	at com.ql.util.express.instruction.op.OperatorFactory.addOperator(OperatorFactory.java:71)
	at com.ql.util.express.instruction.op.OperatorFactory.replaceOperator(OperatorFactory.java:79)
	at com.ql.util.express.ExpressRunner.replaceOperator(ExpressRunner.java:480)
	at com.yn.SynchronizedTest.run(SynchronizedTest.java:35)
	at java.lang.Thread.run(Thread.java:748)

 

单例示范:

import com.ql.util.express.ExpressRunner;
import lombok.extern.slf4j.Slf4j;

/**
 * 规则引擎ExpressRunner工厂,创建ExpressRunner单例
 *
 * @author yangning
 * @date 2021/4/22 11:38 上午
 **/
@Slf4j
public class ExpressRunnerFactory {
    private volatile static ExpressRunner instance;

    public static ExpressRunner getInstance() {
        if (instance == null) {
            synchronized (ExpressRunnerFactory.class) {
                if (instance == null) {
                    ExpressRunner runner = new ExpressRunner();
                    initFunctionOfServiceMethods(runner);
                    instance = runner;
                }
            }
        }
        return instance;
    }

    /**
     * 加载自定义的对象(例如Spring对象)方法转换为表达式
     *
     * @param runner 初始化语法分析和计算的入口类
     */
    private static void initFunctionOfServiceMethods(ExpressRunner runner) {
        try {
            runner.addFunctionOfServiceMethod("contain", new ContainsFunction(), "contains",
                    new Class[]{String.class, String.class}, null);
            runner.addFunctionOfServiceMethod("contains", new ContainsFunction(), "contains",
                    new Class[]{String.class, String[].class}, null);
            runner.addFunctionOfServiceMethod("containsIgnoreCase", new ContainsFunction(), "containsIgnoreCase",
                    new Class[]{String.class, String.class}, null);
            runner.addFunctionOfServiceMethod("containsIgnoreCases", new ContainsFunction(), "containsIgnoreCase",
                    new Class[]{String.class, String[].class}, null);
        } catch (Exception e) {
            log.error("规则引擎initFunctionOfServiceMethods()异常:", e);
            throw ExFactory.throwSystem("规则引擎初始化异常!");
        }
    }
}

在注入自定义操作符和content的时候,需要注意线程安全问题:

    public synchronized void injectCustomizeOperator(ExpressRunner runner, BaseContext baseContext) throws Exception {
        // 手机号前缀规则校验
        if (baseContext.getMobileContext() != null) {
            Integer beginIndex = baseContext.getMobileContext().getBeginIndex();
            Integer endIndex = baseContext.getMobileContext().getEndIndex();

            OperatorBase prefix = runner.getOperatorFactory().getOperator("prefix");
            if (prefix == null) {
                runner.addOperator("prefix", new MobilePrefixInOperator(beginIndex, endIndex));
            } else {
                runner.replaceOperator("prefix", new MobilePrefixInOperator(beginIndex, endIndex));
            }
        }
    }

 

 类似资料: