通過MVEL表達式和Apache Chain職責鏈模式解耦MQ消息處理節點的實踐應用

導讀

本文主要講解了MVEL表達式和責任鏈設計模式相結合一起的消息處理解決方案設計、解耦消息處理節點以及方便代碼維護擴展。通過“訂單拆單消息”的接入作爲具體實踐案例,簡要闡述了MVEL表達式和Apache Chain職責鏈設計模式應用場景。希望通過本文,讀者可以對MVEL表達式和責任鏈模式相關概念有一定的認識,並且能夠將它們應用到具體的業務場景之中,幫助大家在實際代碼研發的時候,降低代碼複雜度和提升代碼的複用率。

1、背景

互聯網的頭部公司,各個後臺系統應用交互主鏈路之中,會下發大量MQ消息給分支業務差異化應用。業務系統應用收到MQ消息後結合實際業務處理,但是往往大家在處理邏輯代碼的時候會進行不斷的疊加代碼,造成代碼臃腫、複雜和可讀性差等問題。例如:

public void handleMessage(String message) throws Exception {
    CallerInfo callerInfo = Profiler.registerInfo(UmpKey.KEY_BD_DLOK_FLAG_GHOST_HANDLER, "xxx", false, true);
    try {
        DeliveredMessage msg = parseMessage(message);
        if (null == msg) {
            return;
        }
        String id = msg.getOrderId();
        if (null == id) {
            //監聽到的訂單消息 id不應爲空
            return;
        }
        String sendPay = msg.getSendPay();
        //是否XXX
        boolean isShop = CAR_O2O.equals(String.valueOf(sendPay.charAt(XXX)));
        //是否是XXX
        boolean isCar = CAR_ADDED_SERVICE.equals(String.valueOf(sendPay.charAt(XXX)));
        String waybillSign = msg.getWaybillSign();
        //是否是XX
        boolean isSelf = SELF_ORDER.equals(String.valueOf(waybillSign.charAt(XXX)));

        long tid = System.nanoTime();
        Long orderId = Long.parseLong(id);

        //監聽到訂單後,更改訂單狀態表中的訂單狀態
        if (isCar && isSelf) {
            verOrderCarService.updateVerOrderCarStatusByOrderId(tid, orderIdLong, UPDATE_PIN);
        }
        if (isShop && isCar) {
            if (isSelf) {
                // 若在新表ver_order_sms_car中存在發送模板1短信,否則,發送原短信(模板3)
                List<VerOrderSmsCar> verOrderSmsCarList = verOrderSmsCarDao.getCarOrderListByOrderId(orderIdLong);
                if (CollectionUtils.isEmpty(verOrderSmsCarList)) {
                    dealTemplateThreeOrder(tid,orderId);
                } else {
                    dealTemplateOneOrder(tid, orderIdLong, verOrderSmsCarList);
                    this.sendShopSms(verOrderSmsCarList);
                }
            } else {
                // 滿足條件的訂單  即原訂單流程沒有走完,發送模板3
                List<VerOrderSmsCar> verOrderSmsCarList = verOrderSmsCarDao.getSmsCarOrderByOrderId(orderId);
                //返回數據字段id
                if (CollectionUtils.isNotEmpty(verOrderSmsCarList)) {
                    return;
                }
                dealTemplateThreeOrder(tid, orderId);
            }
        }
        // 發送狀態變更消息
        if(isCar){
            this.sendVerStore(orderId, isShop ? 1 : 0);
        }
    } catch (Exception e) {
        LOGGER.error("監聽MQ消息處理異常 : {}", e);
        Profiler.functionError(callerInfo);
    } finally {
        Profiler.registerInfoEnd(callerInfo);
    }
}

總結:代碼片段邏輯嵌套複雜、各個處理節點耦合(例如:dealTemplateThreeOrder方法、sendShopSms方法)、新增節點不方便(例如:dealTemplateOneOrder(tid, orderId, verOrderSmsCarList))以及代碼行數1000+等一系列問題。

2、MVEL表達式

MVEL爲 MVFLEX Expression Language(MVFLEX表達式語言)的縮寫,它是一種動態/靜態的可嵌入的表達式語言和爲Java平臺提供Runtime(運行時)的語言。它也可以用來解析簡單的JavaBean表達式。Runtime(運行時)允許MVEL表達式通過解釋執行或者預編譯生成字節碼後執行。簡單一句話,MVEL可以將字符串內容,轉化爲Java程序來運行,具體細節內容大家可以參考 https://blog.51cto.com/u_16091571/6271830。

3、責任鏈設計模式

定義:

責任鏈模式(Chain of Responsibility)又名 職責鏈模式,是一種行爲設計模式,它允許你構建一個由多個對象組成的鏈,每個對象都有機會處理請求,或者將請求傳遞給鏈中的下一個對象。這種模式常用於處理請求的對象之間存在 層次關係 的情況。責任鏈模式的主要目的是解耦發送者和接收者,使多個對象都有機會處理請求,而不是將請求發送者與接收者硬編碼在一起。

結構:

抽象處理者(Handler): 定義一個處理請求的接口,包含抽象處理方法並維護一個對下一個處理者的引用。

具體處理者(Concrete Handler): 實現處理請求的接口,判斷能否處理本次請求,如果能夠處理則處理,否則將請求傳遞給下一個處理者。

客戶端類(Client): 創建處理鏈,並向鏈頭的具體處理者對象提交請求,它不關心處理細節和請求的傳遞過程。

優缺點:

1)優點

a.鬆散耦合: 責任鏈模式使得請求發送者和接收者解耦,每個處理者僅需關心自己能否處理請求,而不需要知道整個處理流程的細節。

b.靈活性: 可以動態地改變處理者之間的關係和順序,新增或刪除處理者,以適應不同的需求和場景。

c.可擴展性: 容易添加新的處理者,無需修改現有的代碼,符合開閉原則。

d.單一職責原則: 每個具體處理者只負責處理特定類型的請求,符合單一職責原則,使得代碼更清晰和可維護。

2)缺點

a.性能問題: 在責任鏈比較長的情況下,請求可能需要遍歷整個鏈條才能找到合適的處理者,可能影響性能。

Apache Chain 職責鏈:

整個Apache Chain職責鏈,包括Context、Command和Filter三個核心組件以及ChainBase類。

1)Context 接口

Context 表示命令執行的上下文,在命令間實現共享信息的傳遞,父接口是 Map,它只是一個標記接口。

2)Command 接口

Commons Chain 中最重要的接口,表示在 Chain 中的具體某一步要執行的命令。它只有一個方法:boolean execute(Context context),如果返回 true,那麼表示 Chain 的處理結束,Chain 中的其他命令不會被調用;返回 false,則 Chain 會繼續調用下一個 Command,直到 Chain 的末尾或拋出異常。

3)Filter 接口

它是一種特殊的 Command,除了 Command 的 execute 方法之外,還包括了一個方法:boolean postProcess(Context context, Exception exception),Commons Chain 會在執行了 Filter 的 execute 方法之後,執行 postprocess(不論 Chain 以何種方式結束);Filter 執行 execute 的順序與 Filter 出現在 Chain 中出現的位置一致,但是執行 postprocess 順序與之相反。如:execute 的執行順序是:filter1 -> filter2;而 postprocess 的執行順序是:filter2 -> filter1。

4)ChainBase

ChainBase 實現 Chain 接口。Chain表示“命令鏈”,要在其中執行的命令,需要先添加到 Chain 中,Chain 的父接口是 Command。ChainBase類可以直接在Spring使用。它的具體執行方法:

public boolean execute(Context context) throws Exception {
    if (context == null) {
        throw new IllegalArgumentException();
    } else {
        this.frozen = true;
        boolean saveResult = false;
        Exception saveException = null;
        int i = false;
        int n = this.commands.length;
        int i;
        for(i = 0; i < n; ++i) {
            try {
                saveResult = this.commands[i].execute(context);
                if (saveResult) {
                    break;
                }
            } catch (Exception var11) {
                saveException = var11;
                break;
            }
        }
        if (i >= n) {
            --i;
        }
        boolean handled = false;
        boolean result = false;
        for(int j = i; j >= 0; --j) {
            if (this.commands[j] instanceof Filter) {
                try {
                    result = ((Filter)this.commands[j]).postprocess(context, saveException);
                    if (result) {
                        handled = true;
                    }
                } catch (Exception var10) {
                }
            }
        }
        if (saveException != null && !handled) {
            throw saveException;
        } else {
            return saveResult;
        }
    }
}

4、實踐案例(訂單MQ消息處理流程)

汽車線下安裝履約服務的業務場景之中,除開主站黃金流量流程以外,需要在接到中臺訂單拆單消息、訂單出庫消息之後給門店技師派單、發送覈銷碼短信等定製化業務流程。此過程中存在接入多個消息處理同一個事件的相同點,也有同一個消息處理不同事件差異點。具體處理層級結構圖如下:

 

 

相關類圖

 

 

實現代碼

基於SpringBoot框架實現,消息處理鏈路中,核心內容包含三部分。第一部分消息處理Handler,接收到消息後將消息內容轉化爲Java Bean,例如:訂單拆單消息(需要拆分訂單)OdcDivideOrderhandler。第二部分處理節點Handler,它是職責鏈的處理節點,按照業務需求進行具體業務代碼的實現,例如:技師派單消息發送節點(AddedTechDispatchHandler)。第三部分職責鏈配置文件,application-chain.xml,以下用訂單拆分消息(拆單)處理流程爲例。

第一部分(OdcDivideOrderHandler.java):

/**
 * 訂單拆分消息(拆單消息)
 
 * @author xxx
 * @date xxxx-xx-xx xx:xx
 */
@Service("odcDivideOrderHandler")
public class OdcDivideOrderHandler extends BaseOrderHandler implements MqMessageHandler<List<VerOrder>> {
    /**
     * 消息分派處理
     */
    @Resource(name="odcDivideOrderChain")
    private Chain odcDivideOrderChain;
    /**
     * 基於MVEL表達式過濾執行器的篩選規則
     */
    private final Map<String, String> expressionMap = new HashMap<String, String>() {
        {
            //派單過濾規則
            put("tech_dispatch_rule", "return sendPayMap.get(\"XXX\") == X && sendPayMap.get(\"XXX\") == X;");
        }
    };
    /**
     * @param tid        處理事件
     * @param messageDTO MQ消息
     * @return 處理結果
     * @throws Exception 處理異常
     */
    private boolean handleMessage(long tid, MqMessageDTO<List<VerOrder>> messageDTO) throws Exception {
         List<VerOrder> verOrderList = messageDTO.getObject();
        try {
            //上下文內容
            Context context = new ContextBase();
            //1.處理時間
            context.put(Constants.TID, tid);
            //2.派單列表
            context.put(Constants.VER_ORDER_LIST,carOrderList);
            //3.操作過濾規則
            context.put(Constants.EXPRESSION_RULE_MAP,expressionMap);
            odcDivideOrderChain.execute(context);
        } catch (Exception ex) {
            //此次代碼省略........
        }
        return true;
    }
}

解析:消息處理Handler主要是將接收到消息轉化Java Bean,再將具體的上下文內容下傳給後續事件處理Handler。參數expressionMap存儲的是MVEL表達式需要處理的內容,具體內容結合實際業務場景差異化設置,對於後續節點處理Handler擴展性有很大幫助。odcDivideOrderChain職責鏈的命令鏈類,後續各個節點的流轉全靠它。

第二部分(AddedTechDispatchHandler.java):

/**
 * 派單Handler
 *
 * @author xxx 
 * @date xxxx-xx-xx xx:xx
 */
@Service("addedTechDispatchHandler")
public class AddedTechDispatchHandler implements Command {
    /**
     * 派單消息topic
     */
    @Value("${xxx}")
    private String topic;
    /**
     * 消息生產
     */
    @Resource(name = "xxxxx")
    private MessageProducer messageProducer;
    @Override
    public boolean execute(Context context) throws Exception {
        Object tid = context.get(Constants.TID);
        Object object = context.get(Constants.VER_ORDER_LIST);
        if (!(object instanceof List)) {
            return false;
        }
        //訂單列表
        List<VerOrder> orders = (List<VerOrder>) object;
        //列表爲空
        if (CollectionUtils.isEmpty(orders)) {
            return false;
        }
        //過濾規則
        Object ruleObj = context.get(Constants.EXPRESSION_RULE_MAP);
        if (!(ruleObj instanceof Map)) {
            return false;
        }
        //派單規則
        Object obj = ((Map) ruleObj).get(Constants.TECH_DISPATCH_RULE);
        //沒有配置規則直接返回
        if (!(obj instanceof String)) {
            return false;
        }
        String expression = (String) obj;
        if (StringUtils.isBlank(expression)) {
            return false;
        }
        for (VerOrder verOrder : orders) {
            //發送派單消息
            this.sendTechDispatch(tid, verOrder, expression);
        }
        return false;
    }

    /**
     * 發送技師派單消息
     *
     * @param tid      處理時間
     * @param verOrder 訂單
     */
    public void sendTechDispatch(Object tid, VerOrder verOrder, String expression) {
        try {
            //派單規則判斷,false-不派單,true-需要派單
            if (!SendPayUtil.isExpression(expression, verOrder.getSendPayMap())) {
                return;
            }
            String cxt = JSON.toJSONString(verOrder);
            Message message = new Message(topic, cxt, verOrder.getOrderId().toString());
            messageProducer.send(message);
        } catch (JMQException e) {
           //此次代碼省略........
        } catch (Exception e) {
           //此次代碼省略........
        } finally {
           //此次代碼省略........
        }
    }
}

解析:事件節點Handler主要是解析上下內容,執行需要處理的事項內容。特別是SendPayUtil.isExpression(expression, verOrder.getSendPayMap())方法,識別了MVEL表達式,使得即使同一個事件處理節點(例如:派單節點)也可以根據不同MQ消息,設置不同的規則。

/**
 *  sendPayMap表達式解析
 * @param expression 表達式
 * @param sendPayMap 訂單SendPayMap值
 * @return 解析結果
 */
public static boolean isExpression(String expression,String sendPayMap){
    //sendPayMap爲空
    if (StringUtils.isBlank(sendPayMap)) {
        return false;
    }
    Map map = null;
    try {
        map = JSON.parseObject(sendPayMap, Map.class);
    } catch (Exception ex) {
        LOGGER.error("sendPayMap格式轉化錯誤", ex);
    }
    //map
    if (map == null || map.isEmpty()) {
        return false;
    }
    Map<String,Map> param = new HashMap<>(1);
    param.put(Constant.SEND_PAY_MAP,map);
    return (Boolean)MVEL.eval(expression,param);
}

第三部分(application-chain.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-lazy-init="false" default-autowire="byName">
    <bean id="odcOutStockFullOrderChain" class="org.apache.commons.chain.impl.ChainBase">
        <constructor-arg>
            <array>
                <ref bean="addedOrderWangShiFuHandler"/>
                <ref bean="addedTechDispatchHandler"/>
            </array>
        </constructor-arg>
    </bean>
    <bean id="odcDivideOrderChain" class="org.apache.commons.chain.impl.ChainBase">
        <constructor-arg>
            <array>
                <ref bean="addedTechDispatchHandler"/>
            </array>
        </constructor-arg>
    </bean>
    <bean id="odcUndividedOrderChain" class="org.apache.commons.chain.impl.ChainBase">
        <constructor-arg>
            <array>
                <ref bean="addedTechDispatchHandler"/>
            </array>
        </constructor-arg>
    </bean>
</beans>

解析:命令鏈配置文件,實現各個事件處理節點進行配置化,聚合各個分散的節點業務邏輯內,後續注入到對應的消息解析Handler。

5、總結

整個消息處理過程中採用Apache Chain職責鏈模式來降低代碼層面的耦合度以及可以動態地改變處理者之間的關係和順序,新增或刪除處理者,以適應不同的需求和場景。MVEL表達式增強了同一事件處理節點的複用性,最大限度的提升了代碼的簡潔性。希望此文對大家後續設計類似場景有一定的幫助和啓發。

作者:京東零售 張強

來源:京東雲開發者社區

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