Jexl動態(tài)代碼執(zhí)行邏輯引擎

一、前情提要

  • 現(xiàn)有物聯(lián)網(wǎng)系統(tǒng)已經(jīng)初步接入了一些智能設備并對相應的設備進行數(shù)據(jù)收集和控制。下一步是要實現(xiàn)設備的聯(lián)動功能。


    溫濕度傳感器

    光照傳感器

    紅外遙控器

    空調(diào)

    智能插座

二、要做什么?

我們要實現(xiàn)的功能是,場景自動化功能。
  • 當溫度達到30℃時且濕度小于45%時 ==> 打開空調(diào),并將空調(diào)調(diào)整為制冷模式,溫度調(diào)節(jié)為20攝氏度
  • 當濟南的pm2.5濃度大于23時,==> 打開關閉插座,并發(fā)送釘釘消息提醒
  • 當濟南的天氣狀態(tài)為“雨天”時或室內(nèi)光照強度小于250LUX時 ==> 打開空調(diào)除濕
    這些任務,均可統(tǒng)一設置執(zhí)行的時間段,并設置周幾的執(zhí)行狀態(tài)。


    功能前端效果

三、怎么做?

1、難點
  • 如何定義任務的數(shù)據(jù)結(jié)構(gòu),保證能夠?qū)崿F(xiàn)上面的功能。
  • 如何通過代碼動態(tài)去判斷各種數(shù)據(jù)值的對比是否能夠滿足條件。
  • 如何動態(tài)的去執(zhí)行各個動作,且每個動作所需的參數(shù)不同。
  • 如何優(yōu)雅的實現(xiàn),必然不能每個條件都寫一大串 if - else
  • 如何觸發(fā)每個任務的執(zhí)行,定義統(tǒng)一的觸發(fā)入口
  • 如何保證服務的穩(wěn)定及可拓展,橙子便利現(xiàn)有140+門店,假設每家門店都有5個常規(guī)任務在執(zhí)行,進行的任務數(shù)就要再700+。
2、表結(jié)構(gòu)
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 '場景狀態(tài)(ON 開啟 OFF 關閉)',
  `conditions` text COMMENT '條件',
  `actions` text COMMENT '條件',
  `preconditions` text COMMENT '前置條件',
  `create_no` varchar(50) DEFAULT NULL COMMENT '創(chuàng)建人',
  `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
  `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、數(shù)據(jù)結(jié)構(gòu)
{
        "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": "釘釘消息發(fā)送測試"
                }
            }
        ]
    }
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編寫的應用程序和框架中,實現(xiàn)動態(tài)和腳本功能,JEXL基于JSTL表達式語言的一些擴展實現(xiàn)了表達式語言,支持shell腳本或ECMAScript(js)中的大多數(shù)構(gòu)造.

5、條件-核心邏輯封裝(關系運算符)
    /**
     * 關系運算符
     *
     * @param conditionOperationMessage
     * @return boolean
     */
    private static boolean relationalCal(ConditionOperationMessage conditionOperationMessage) {
        JexlEngine engine = new JexlEngine();
        JexlContext context = new MapContext();
        // 構(gòu)建表達式
        String command = " if ( a " +
                conditionOperationMessage.getOperation() +
                conditionOperationMessage.getValue() +
                " ) { return true } else { return false } ";
        // 設置變量值
        context.set("a", conditionOperationMessage.getParam());
        // 執(zhí)行計算結(jié)果
        boolean result = (boolean) engine.createExpression(command).evaluate(context);
        log.info("關系運算結(jié)果 [ {} {} {} ] --> [{}] ", 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、動作-動態(tài)方法執(zhí)行

    /**
     * 執(zhí)行動作
     *
     * @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("方法[{}]執(zhí)行結(jié)果[{}]", 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("發(fā)送釘釘消息[{}]", property.getMessage());
        return true;
    }

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

    /**
     * 空調(diào)控制
     *
     * @param property
     */
    public static boolean airCondition(ActionPropertyMessage property) {
        log.info("空調(diào)控制 類型[{}] 值[{}]", property.getAirConditionType().getStr(), property.getAirConditionValue());
        return true;
    }
調(diào)用入口
 /**
     * 調(diào)用入口
     *
     * @param code  觸發(fā)條件
     * @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);
            // 條件執(zhí)行封裝列表
            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 待定
                }
            });
            // 判斷條件是否執(zhí)行完成
            if (CollectionUtils.isEmpty(conditionOperationMessageList)
                    || !ConditionTool.judge(conditionOperationMessageList, sceneAutomation.getMatchType())) {
                log.info("任務[{}] 不滿足條件", sceneAutomation.getSceneName());
                continue;
            }
            // 執(zhí)行動作
            List<ActionMessage> actionMessages = JSON.parseArray(sceneAutomation.getActions(), ActionMessage.class);
            // 執(zhí)行
            ActionTool.action(actionMessages);
        }
        return new SimpleMessage(ErrorCodeEnum.OK);
    }

三、還有什么要補充的?

1、mysql 查詢 json格式數(shù)據(jù)
    /**
     * 獲取符合條件的場景
     *
     * @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);

    /**
     * 獲取參數(shù)值
     *
     * @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 執(zhí)行
  /**
     * static穿透service執(zhí)行方法
     */
    final
    AutoService innerAutoService;
    static AutoService autoService;


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

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

四、這玩意兒的意義是什么?

1、我們在遇到特殊問題的時候,要打破固有的編程思想,根據(jù)自己的業(yè)務需求找到最優(yōu)的解決方案。
2、不要妥協(xié),要讓自己的程序變得優(yōu)雅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容