Jexl动态代码执行逻辑引擎

一、前情提要

  • 现有物联网系统已经初步接入了一些智能设备并对相应的设备进行数据收集和控制。下一步是要实现设备的联动功能。


    温湿度传感器

    光照传感器

    红外遥控器

    空调

    智能插座

二、要做什么?

我们要实现的功能是,场景自动化功能。
  • 当温度达到30℃时且湿度小于45%时 ==> 打开空调,并将空调调整为制冷模式,温度调节为20摄氏度
  • 当济南的pm2.5浓度大于23时,==> 打开关闭插座,并发送钉钉消息提醒
  • 当济南的天气状态为“雨天”时或室内光照强度小于250LUX时 ==> 打开空调除湿
    这些任务,均可统一设置执行的时间段,并设置周几的执行状态。


    功能前端效果

三、怎么做?

1、难点
  • 如何定义任务的数据结构,保证能够实现上面的功能。
  • 如何通过代码动态去判断各种数据值的对比是否能够满足条件。
  • 如何动态的去执行各个动作,且每个动作所需的参数不同。
  • 如何优雅的实现,必然不能每个条件都写一大串 if - else
  • 如何触发每个任务的执行,定义统一的触发入口
  • 如何保证服务的稳定及可拓展,橙子便利现有140+门店,假设每家门店都有5个常规任务在执行,进行的任务数就要再700+。
2、表结构
CREATE TABLE `scene_automation` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `scene_name` varchar(200) DEFAULT NULL COMMENT '场景名称',
  `store_code` varchar(10) DEFAULT NULL COMMENT '门店编号',
  `match_type` varchar(10) DEFAULT NULL COMMENT '条件匹配模式(ANY 任意满足 ALL 全部满足)',
  `background` varchar(200) DEFAULT NULL COMMENT '背景',
  `scene_state` varchar(10) DEFAULT NULL COMMENT '场景状态(ON 开启 OFF 关闭)',
  `conditions` text COMMENT '条件',
  `actions` text COMMENT '条件',
  `preconditions` text COMMENT '前置条件',
  `create_no` varchar(50) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_no` varchar(50) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='场景自动化表';
3、数据结构
{
        "sceneName": "场景自动化测试",
        "storeCode": "0009",
        "matchType": "ALL",
        "background": "#ffffff",
        "sceneState": "ON",
        "conditionMessages": [
            {
                "entityId": "6cb6a428adede6b580jqqb",
                "entityType": "DEVICE",
                "orderNum": 1,
                "display": {
                    "code": "temperature",
                    "operator": "MORE",
                    "value": 25
                }
            },
            {
                "entityId": "6cb6a428adede6b580jqqb",
                "entityType": "DEVICE",
                "orderNum": 2,
                "display": {
                    "code": "humidity",
                    "operator": "MORE",
                    "value": 30
                }
            }
        ],
        "preconditionMessages": [
            {
                "display": {
                    "start": "00:00",
                    "end": "23:59",
                    "loops": "1111110"
                },
                "type": "TIME_CHECK"
            }
        ],
        "actionMessages": [
            {
                "executor": "socket",
                "entityId": "6c30fb6e4c962c96b2asgo",
                "property": {
                    "switchSocket": true
                }
            },
            {
                "executor": "sendDing",
                "property": {
                    "message": "钉钉消息发送测试"
                }
            }
        ]
    }
4、引入jexl逻辑引擎
    <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-jexl</artifactId>
            <version>2.1.1</version>
        </dependency>

Java表达式语言--Java Expression Language(JEXL),这是Apache开源的一个jar包,旨在促进在用Java编写的应用程序和框架中,实现动态和脚本功能,JEXL基于JSTL表达式语言的一些扩展实现了表达式语言,支持shell脚本或ECMAScript(js)中的大多数构造.

5、条件-核心逻辑封装(关系运算符)
    /**
     * 关系运算符
     *
     * @param conditionOperationMessage
     * @return boolean
     */
    private static boolean relationalCal(ConditionOperationMessage conditionOperationMessage) {
        JexlEngine engine = new JexlEngine();
        JexlContext context = new MapContext();
        // 构建表达式
        String command = " if ( a " +
                conditionOperationMessage.getOperation() +
                conditionOperationMessage.getValue() +
                " ) { return true } else { return false } ";
        // 设置变量值
        context.set("a", conditionOperationMessage.getParam());
        // 执行计算结果
        boolean result = (boolean) engine.createExpression(command).evaluate(context);
        log.info("关系运算结果 [ {} {} {} ] --> [{}] ", conditionOperationMessage.getParam(), conditionOperationMessage.getOperation(), conditionOperationMessage.getValue(), result);
        return result;
    }
6、条件-逻辑运算表达式
 /**
     * 逻辑或运算核心方法
     *
     * @param conditionOperationMessageList
     * @return boolean
     */
    private static boolean logicOrCoreCal(List<ConditionOperationMessage> conditionOperationMessageList) {
        log.info("========开始进入逻辑或运算=====");
        for (ConditionOperationMessage conditionOperationMessage : conditionOperationMessageList) {
            if (relationalCal(conditionOperationMessage)) {
                log.info("========满足逻辑或运算=====");
                return true;
            }
        }
        log.info("========不满足逻辑或运算=====");
        return false;
    }

    /**
     * 逻辑或运算核心方法
     *
     * @param conditionOperationMessageList
     * @return boolean
     */
    private static boolean logicAndCoreCal(List<ConditionOperationMessage> conditionOperationMessageList) {
        log.info("========开始进入逻辑且运算=====");
        for (ConditionOperationMessage conditionOperationMessage : conditionOperationMessageList) {
            if (!relationalCal(conditionOperationMessage)) {
                log.info("========不满足逻辑且运算=====");
                return false;
            }
        }
        log.info("========满足逻辑且运算=====");
        // 校验是否全部为true
        return true;
    }

7、动作-动态方法执行

    /**
     * 执行动作
     *
     * @param actionMessageList
     */
    public static void action(List<ActionMessage> actionMessageList) {
        JexlEngine engine = new JexlEngine();
        JexlContext context;
        if (!CollectionUtils.isEmpty(actionMessageList)) {
            for (ActionMessage actionMessage : actionMessageList) {
                context = new MapContext();
                String command = "ActionTool." + actionMessage.getExecutor() + "(property)";
                context.set("ActionTool", ActionTool.class);
                context.set("property", actionMessage.getProperty());
                boolean result = (boolean) engine.createExpression(command).evaluate(context);
                log.info("方法[{}]执行结果[{}]", actionMessage.getExecutor(), result);
            }
        }
    }
 /**
     * 插座控制
     *
     * @param property
     */
    public static boolean socket(ActionPropertyMessage property) {
        log.info("插座控制[{}]", property.getSwitchSocket());
        autoService.testService();
        return true;
    }

    /**
     * 插座控制
     *
     * @param property
     */
    public static boolean sendDing(ActionPropertyMessage property) {
        log.info("发送钉钉消息[{}]", property.getMessage());
        return true;
    }

    /**
     * 延迟
     *
     * @param property
     */
    public static boolean delay(ActionPropertyMessage property) {
        log.info("延时控制 时[{}] 分[{}] 秒[{}] ", property.getDelayHour(), property.getDelayMinutes(), property.getDelaySeconds());
        return true;
    }

    /**
     * 空调控制
     *
     * @param property
     */
    public static boolean airCondition(ActionPropertyMessage property) {
        log.info("空调控制 类型[{}] 值[{}]", property.getAirConditionType().getStr(), property.getAirConditionValue());
        return true;
    }
调用入口
 /**
     * 调用入口
     *
     * @param code  触发条件
     * @return SimpleMessage 
     */
    @Override
    public SimpleMessage imitate(String code) {
        // 获取正在开启的活动
        List<SceneAutomation> sceneAutomationList = sceneAutomationDao.getSuitableScene(code);
        // 校验是否存在该场景
        if (CollectionUtils.isEmpty(sceneAutomationList)) {
            return new SimpleMessage(ErrorCodeEnum.NO, "查询不到该场景");
        }
        // 遍历场景
        for (SceneAutomation sceneAutomation : sceneAutomationList) {
            // 判断前置条件
            List<PreconditionMessage> preconditionMessageList = JSON.parseArray(sceneAutomation.getPreconditions(), PreconditionMessage.class);
            if (!PreconditionTool.judge(preconditionMessageList)) {
                log.info("任务[{}] 不满足前置条件", sceneAutomation.getSceneName());
                continue;
            }
            // 解析条件
            List<ConditionMessage> conditionMessages = JSON.parseArray(sceneAutomation.getConditions(), ConditionMessage.class);
            // 条件执行封装列表
            List<ConditionOperationMessage> conditionOperationMessageList = new ArrayList<>();
            // 获取条件值
            conditionMessages.forEach(conditionMessage -> {
                // 设备类型
                if (ConditionEntityTypeEnum.DEVICE.equals(conditionMessage.getEntityType())) {
                    // 条件值
                    Object param = sceneAutomationDao.getDeviceParam(conditionMessage.getDisplay().getCode(), conditionMessage.getEntityId());
                    log.info("设备码[{}]  类型[{}] 当前值[{}] ", conditionMessage.getEntityId(), conditionMessage.getDisplay().getCode(), param);
                    conditionOperationMessageList.add(ConditionOperationMessage.builder()
                            .param(param)
                            .operation(conditionMessage.getDisplay().getOperator().getStr())
                            .value(conditionMessage.getDisplay().getValue())
                            .build());
                } else {
                    // TODO 待定
                }
            });
            // 判断条件是否执行完成
            if (CollectionUtils.isEmpty(conditionOperationMessageList)
                    || !ConditionTool.judge(conditionOperationMessageList, sceneAutomation.getMatchType())) {
                log.info("任务[{}] 不满足条件", sceneAutomation.getSceneName());
                continue;
            }
            // 执行动作
            List<ActionMessage> actionMessages = JSON.parseArray(sceneAutomation.getActions(), ActionMessage.class);
            // 执行
            ActionTool.action(actionMessages);
        }
        return new SimpleMessage(ErrorCodeEnum.OK);
    }

三、还有什么要补充的?

1、mysql 查询 json格式数据
    /**
     * 获取符合条件的场景
     *
     * @param code 条件值
     * @return List<SceneAutomation>
     */
    @Select("SELECT " +
            " id, " +
            " scene_name, " +
            " store_code, " +
            " match_type, " +
            " background, " +
            " scene_state, " +
            " conditions, " +
            " preconditions, " +
            " actions  " +
            "FROM " +
            " `scene_automation`  " +
            "WHERE " +
            " scene_state = 'ON'  " +
            " AND JSON_CONTAINS( conditions, JSON_OBJECT( 'display', JSON_OBJECT( 'code', #{code} ) ) )")
    List<SceneAutomation> getSuitableScene(@Param("code") String code);

    /**
     * 获取参数值
     *
     * @param key       key
     * @param deviceUid 设备码
     * @return Object
     */
    @Select(" SELECT " +
            "device_detail ->> '$.${key}' as 'param' " +
            "from store_devices WHERE " +
            "device_uid = #{deviceUid} ")
    Object getDeviceParam(@Param("key") String key, @Param("deviceUid") String deviceUid);
2、static 穿透 service 执行
  /**
     * static穿透service执行方法
     */
    final
    AutoService innerAutoService;
    static AutoService autoService;


    public ActionTool(AutoService innerAutoService) {
        this.innerAutoService = innerAutoService;
    }

    /**
     * static穿透service初始化
     */
    @PostConstruct
    public void init() {
        autoService = innerAutoService;
    }

四、这玩意儿的意义是什么?

1、我们在遇到特殊问题的时候,要打破固有的编程思想,根据自己的业务需求找到最优的解决方案。
2、不要妥协,要让自己的程序变得优雅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容