企业级规则引擎落地实战:动态脚本引擎 QLExpress ,真香!

引言

最近点外卖发现起手接单越来越慢了,主要原因是因为这个时间点骑手少了。

用户增长部门反馈接单时间变长,用户体验变差,用户会流失。需要解决,需要缩短接单时长。

解决方式就是给骑手补贴。

  • 降低抽佣比例,但是这种公司的营收大盘会降低,这种一般只有国家出政策才会使用。
  • 建活动,完成10单奖励10元,吸引骑手参与活动。

所以场景就是外卖订单和活动匹配。

规则引擎

  • 动态表达式执行:支持类似 Java 语法的表达式,可以在运行时解析和执行。
  • 轻量级:不依赖复杂框架,适合嵌入到各类 Java 应用中。
  • 高性能:通过缓存、编译优化等机制提升执行效率。
  • 安全性控制:可以限制可调用的方法、类、操作符等,防止恶意脚本执行。
  • 扩展性强:支持自定义函数、操作符、宏等。
  • 中文友好:支持中文变量名、中文操作符(如"如果"、"那么"),便于非程序员编写规则。

企业级项目落地

从开发系统的角度出发,我们要建设底层的能力,能业务是对能力的编排。规则引擎也是。

规则引擎首先定义脚本函数,然后定义表达式(能力),最后定义表达式组(业务)

小试牛刀:城市活动

  • 脚本函数(能力)
bash 复制代码
public class FunctionConfig {
    public static String[][] customFunctions = {
            {
                    "xInList", //判断x是否在list中
                    "function xInList(Object x,Object list){\n" +
                            "    if (null == list || list.size() == 0) {\n" +
                            "        return true;\n" +
                            "    } else {\n" +
                            "        return (x in list);\n" +
                            "    }\n" +
                            "}"
            }
    };
}
  • 表达式
bash 复制代码
xInList(city,cityList)
  • 表达式组(组)
bash 复制代码
    //城市活动-订单和活动匹配
    public static List<ProcessNode> CITY_ACTIVITY_ORDER_MATCH = Arrays.asList(
            new ProcessNode("城市匹配" ,"city", "cityList", "xInList(city,cityList)")
    );

跑一个案例

bash 复制代码
public class App {
    public static void main(String[] args) throws Exception {

        //创建一个城市活动
        Activity activity = new Activity();
        activity.setName("情人节晚上冲单奖");
        activity.setCityList(Arrays.asList("上海", "北京"));
        JSONObject activityJSON = JSONUtil.parseObj(activity);

        //创建一个天津订单
        Order order = new Order();
        order.setCity("天津");
        JSONObject orderJSON = JSONUtil.parseObj(order);

        //初始化运行器
        ExpressRunner runner = new ExpressRunner(true, true);
        String[][] customFunctions = FunctionConfig.customFunctions;
        for (String[] customFunction : customFunctions) {
            runner.loadMutilExpress(null, customFunction[1]);
        }

        //执行匹配逻辑
        Boolean matchStatus = true;
        String matchFailReason = "";
        //订单和活动匹配
        List<ProcessNode> orderMatchActivity = ProcessConfig.CITY_ACTIVITY_ORDER_MATCH;
        for (ProcessNode processNode : orderMatchActivity) {
            String orderKey = processNode.getOrderKey();
            String activityKey = processNode.getActivityKey();
            String expression = processNode.getExpression();
            //创建上下文,参数赋值
            DefaultContext<String, Object> ctx = new DefaultContext<>();
            ctx.put(activityKey, activityJSON.get(activityKey));
            ctx.put(orderKey, orderJSON.get(orderKey));
            //执行业务逻辑
            matchStatus = matchStatus && (Boolean) runner.execute(expression, ctx, null, true, false);
            if (!matchStatus){
                matchFailReason = processNode.getName();
            }
        }

        System.out.println("订单和活动匹配结果:" + matchStatus + ",匹配失败原因:" + matchFailReason);
        
    }
}

输出结果

bash 复制代码
订单和活动匹配结果:false,匹配失败原因:城市匹配

大刀阔斧:时间段活动

业务新增规则,要求只在晚上8点到9点才算

  • 脚本函数(能力):逻辑比较复杂,通过Java好实现,
bash 复制代码
public class TimeSegmentMatch {

    public static boolean match(Date orderDate, JSONArray timeSegmentArray) {
        if (timeSegmentArray == null || timeSegmentArray.size() == 0){
            return true;
        }
        List<TimeSegment> timeSegmentList = JSONUtil.toList(JSONUtil.toJsonStr(timeSegmentArray), TimeSegment.class);
        String orderDateFormat = new SimpleDateFormat("HH:mm:ss").format(orderDate);
        for (TimeSegment timeSegment : timeSegmentList) {
            if (orderDateFormat.compareTo(timeSegment.getSegmentStart()) >= 0 && orderDateFormat.compareTo(timeSegment.getSegmentEnd()) <= 0) {
                return true;
            }
        }
        return false;

    }
}
  • 表达式
bash 复制代码
timeSegmentMatch(orderDate,timeSegmentList)
  • 表达式组(组)
bash 复制代码
     //城市活动-订单和活动匹配
    public static List<ProcessNode> TIME_SEGMENT_ACTIVITY_ORDER_MATCH = Arrays.asList(
            new ProcessNode("时间段匹配" ,"orderDate", "timeSegmentList", "timeSegmentMatch(orderDate,timeSegmentList)")
    );

跑一个案例:限制活动参与时间是20~21点,而订单时间是7点,不匹配

bash 复制代码
public class App {
    public static void main(String[] args) throws Exception {

        //创建一个城市活动
        Activity activity = new Activity();
        activity.setName("情人节晚上冲单奖");
        //限制参与活动的时间点
        TimeSegment timeSegment8 = new TimeSegment();
        timeSegment8.setSegmentStart("20:00:00");
        timeSegment8.setSegmentEnd("21:00:00");
        TimeSegment timeSegment9 = new TimeSegment();
        timeSegment9.setSegmentStart("21:00:00");
        timeSegment9.setSegmentEnd("22:00:00");
        activity.setTimeSegmentList(Arrays.asList(timeSegment8, timeSegment9));
        JSONObject activityJSON = JSONUtil.parseObj(activity);

        //创建一个订单
        Order order = new Order();
        order.setOrderDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2026-02-14 07:00:00"));
        JSONObject orderJSON = JSONUtil.parseObj(order);

        //初始化运行器
        ExpressRunner runner = new ExpressRunner(true, true);
        //加载自定义函数
        runner.addFunctionOfClassMethod("timeSegmentMatch", TimeSegmentMatch.class.getName(), "match", new String[]{"java.util.Date", "cn.hutool.json.JSONArray"}, null);

        //执行匹配逻辑
        Boolean matchStatus = true;
        String matchFailReason = "";
        //订单和活动匹配
        List<ProcessNode> orderMatchActivity = ProcessConfig.TIME_SEGMENT_ACTIVITY_ORDER_MATCH;
        for (ProcessNode processNode : orderMatchActivity) {
            String orderKey = processNode.getOrderKey();
            String activityKey = processNode.getActivityKey();
            String expression = processNode.getExpression();
            //创建上下文,参数赋值
            DefaultContext<String, Object> ctx = new DefaultContext<>();
            System.out.println(activityJSON.get(activityKey).getClass());
            System.out.println(orderJSON.get(orderKey).getClass());
            ctx.put(activityKey, activityJSON.get(activityKey));
            ctx.put(orderKey, orderJSON.get(orderKey));
            //执行业务逻辑
            matchStatus = matchStatus && (Boolean) runner.execute(expression, ctx, null, true, false);
            if (!matchStatus){
                matchFailReason = processNode.getName();
            }
        }

        System.out.println("订单和活动匹配结果:" + matchStatus + ",匹配失败原因:" + matchFailReason);

    }
}

输出结果

bash 复制代码
订单和活动匹配结果:false,匹配失败原因:时间段匹配

性能优化

优化性能的方式无非就是加缓存,少执行,用单例。本次也是一样。

  • 函数只注册一次
  • execute() 是线程安全的(QLExpress 官方保证)
  • 高性能、低内存
bash 复制代码
public class QLExpressEngine {
    private static final ExpressRunner RUNNER = new ExpressRunner(true, true);
    ;

    static {
        // 应用启动时注册所有函数(只执行一次!)
        registerBuiltInFunctions();
    }

    private static void registerBuiltInFunctions() {
        try {
            // 注册 Java 静态方法
            RUNNER.addFunctionOfClassMethod("timeSegmentMatch", TimeSegmentMatch.class.getName(), "match", new String[]{"java.util.Date", "cn.hutool.json.JSONArray"}, null);
            // 注册脚本函数 todo
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static Boolean execute(String script, DefaultContext<String, Object> context)
            throws Exception {
        // ExpressRunner.execute() 是线程安全的!
        return (Boolean) RUNNER.execute(script, context, null, true, false);
    }
}

复制代码
- 😃作者简介:大厂高级 Java 开发工程师
- 💻称 号:CSDN 博客专家✨、阿里云博客专家🌟
- 🔍公众号:云服务小管家。免费💡的阿里云服务器☁ 和云环境直接使用
- 💪生活:专注于后端技术分享🎓迷茫时可来瞅瞅码农轨迹🚶‍♂️
- 📋服务:提供模拟面试和简历辅导,提供生产项目。📩内推可私信✉
- 💬卷卷群:可以和大家一起学习,一起进步👀
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
相关推荐
callJJ12 小时前
Spring AI 文本聊天模型完全指南:ChatModel 与 ChatClient
java·大数据·人工智能·spring·spring ai·聊天模型
懈尘12 小时前
从 Java 1.7 到 Java 21:逐版本深入解析新特性与平台演进
java·开发语言
亓才孓12 小时前
[Maven]Maven基础
java·maven
hello 早上好12 小时前
05_Java 类加载过程
java·开发语言
Thexhy12 小时前
Ollama 指南
ai·大模型
冻感糕人~12 小时前
收藏备用|小白&程序员必看!AI Agent入门详解(附工业落地实操关联)
大数据·人工智能·架构·大模型·agent·ai大模型·大模型学习
echoVic12 小时前
多模型支持的架构设计:如何集成 10+ AI 模型
java·javascript
橙露12 小时前
Java并发编程进阶:线程池原理、参数配置与死锁避免实战
java·开发语言
echoVic12 小时前
AI Agent 安全权限设计:blade-code 的 5 种权限模式与三级控制
java·javascript