🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

注解規則編排元件

image

大家好,我是奕叔😈,一個工作八年快退休的程式設計師,微信:lyfinal,交個朋友,一起交流Java後端技術,相互成長,成為更優秀的🐒~

使用場景

當面對複雜的業務進行開發時,程式本身邏輯碼和業務碼互相嵌套、錯綜複雜,碼一旦寫死很難更改,而業務規則經常改變。這時候就需要使用規則編排來管理,這種業務模型的特性有著固定的輸入參數和輸出格式,中間規則部分要求能夠配置化,盡可能靈活,不需要重新寫碼。

學會了這個元件包括思想,以後能在工作中大量使用,你會看到很多業務部門當業務複雜時做執行編排,規則管理除了使用很重的開源框架之外,基本都會自實現一個輕量的元件來完成,因為這樣零依賴無侵入


為什麼需要規則抽象

我相信大家應該首先會有一個問題,什麼是規則? 用最通俗的話來說規則就是在開發中一個業務PD提出來的玩法或者邏輯。例如在預售商品在商品詳情頁售賣,預售的開始時間是多少,結束時間是多少,這就是一個規則。規則決定著表現,然後開發按照此規則去設計、去編碼。

然而,規則肯定不止一個,是多個。就拿預售商品在商品詳情頁售賣例子來說,除了有預售時間段規則,還有預售不是針對所有商品類型生效的,往往是部分類目下的商品才可以預售,那麼這也將是一個規則。

這裡引申下,如果是你們來設計,你是會把這個預售作為一種類型字段掛在商品主表上區分呢,還是會對商品進行打標處理呢?答案是打標,用小白的話解釋就是打標就是對一類商品主體貼上標籤,如果存儲在db的話擴展非常之差,往往採用hbase寬表進行存儲商品標。

那麼問題來了,這麼多規則,比如ABCD規則,是依次執行嗎,如果存在某種商品類型只需要執行BCD呢,那麼執行A不就無效調用了嗎?另外是順序又是怎能的呢,不同的場景執行的順序可能不一樣又是如何設計呢?規則執行存在依賴關係如果管理呢?等等 會碰到各種各樣的問題。


倘若我們硬著頭皮編碼開發,前期的話你會感受到美滋滋,三下五除二就開發編碼上線完成,等到迭代到一定的程度,產品規則越來越多,業務邏輯越來越複雜,這時候你就難受了,你改的碼你無法很有信心保證不會影響舊的邏輯。這時候就可能為了安全上線,搞個灰度,if...else...上線,如果命中灰度則走你的邏輯,if else在手,天下我有!bug 退!退!退!

image

但是作為一個有點情操的程式猿,往往不會停止於這樣。需要從業務上能看出這個整個鏈路上是怎樣的,服務內上下層是如何調用組裝的。

ok,那我們一步步來思考怎麼來演練這種碼模型架構,如上面例子所說,規則這麼多,我們肯定在開始設計的時候無法窮舉所有的情況,需要拋開具體的業務規則來抽象出來頂層設計。因為不管是前端還是服務端對外提供的接口無外乎都是一系列方法或函數的串接組合結果。基本可以是以下的過程。

  • 編寫每一個方法函數
  • 對所有方法和函數進行串接
  • 構建返回結果

真正需要開發的是第一步,而這些方法函數都是可以高度抽象之後復用的,每一個方法或函數只關注做具體的事,它不關注為什麼業務而做,為哪個接口而做。每一個方法函數無非兩種情況,一種是通過此方法得到某結果,可以使從某個源或者三方接口得到的數據,也可以是執行特定業務邏輯計算之後的結果;另一種就是對某個數據發生改變,可以是數據源持久化,也可以是三方接口做的某些通知。將每個小業務規則抽取成獨立元件,每個功能元件確定是否正常執行,將執行結果放到處理上下文中,規則元件之間通過抽象編排配置流程化

元件設計

首先怎麼識別是一個規則,可以採用Java特性注解。比方說識別一個方法有沒有重寫,看有沒有@override注解是同樣的道理,注解會讓碼更清潔清晰。

注解的定義

首先先定義一個Rule注解,這個注解需要怎樣的屬性呢,毋庸置疑id肯定需要,識別規則唯一,name規則名稱,如果一個規則比較複雜,實在很難用name來進行描述這時候就可以設計一個detail字段來備註規則詳情。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Rule {
    /**
     * 規則id
     *
     * @return
     */
    String id();

    /**
     * 規則name
     *
     * @return
     */
    String name();

    /**
     * 規則描述
     *
     * @return
     */
    String detail();
}

處理器介面定義

規則注解定義好了,那麼規則如何處理呢,有些規則可能是內存計算,有些可能需要請求三方接口等,形式不統一,那麼這裡就要用到介面!提供統一處理方法進行抽象。這時候你會考慮介面的入參怎麼定義?因為不知道具體的規則執行參數有哪些,都有什麼類型。那麼這裡就需要用泛型。泛型能解決的是無需感知具體的數據類型。OK,介面雛形可能要出來了,一個介面提供一個方法,方法的參數是泛型,那不就是泛型介面嗎,沒錯。

public interface IHandler<T> {
    /**
     * 處理事件Event
     *
     * @param t
     */
    void onEvent(T t);
}

規則抽象實體

一般來說,有泛型的地方肯定就有繼承,為什麼呢?我們想下既然這裡提供了泛型T,可能有這個T是A,也可能是B,是C。那么根據Java的特性,這裡ABC有沒有一些公共屬性可以復用,答案是隨著規則越來越多肯定是有的,所以在一開始這裡就需要這樣設計防止後期還需要重構。

@Data
public class RuleBaseEntity implements Serializable {
    /**
     * 實體id
     */
    private Long entityId;
}

規則實體上下文

為了處理實體在規則執行過程中內存數據的流轉,何為內存數據,可以理解在某一步的值,需要在下一步需要使用到。最簡單的就是傳遞過去,這是最low的方法也是最普通的方法,但是作為通用元件需考慮碼抽象,所以需要上下文來做這一層。先將上一步的數據放到上下文中,在下一步使用的時候從上下文取。整個過程都是基於內存操作,沒有進行存儲。所以稱之內存數據。

那麼這也會有另外一個問題,當內存數據在多線程的情況下發生竄亂的時候,會導致內存數據不安全。可能A線程先存了進去,但是取出去在B線程取了出來,這種就需要使用線程副本ThreadLocal來包裝上下文,這也是最常見的套路搭配組合。

public class EntityContextHolder {
    private static ThreadLocal<RuleBaseEntity> contextThreadLocal = new ThreadLocal<>();

    /**
     * 存入線程副本
     *
     * @param entity
     */
    public static void setContext(RuleBaseEntity entity) {
        contextThreadLocal.set(entity);
    }

    /**
     * 從線程副本取出
     *
     * @return
     */
    public static RuleBaseEntity getContext() {
        return contextThreadLocal.get();
    }
}

規則處理器和規則關係

既然介面泛型設計好了,那麼這裡會碰到下一個問題就是規則處理器的實現問題,我們定義的 IHandler 介面,在一個規則下可能有多個處理器,理論上我們要求是一個原子的處理器,怎麼理解,比方說前文說的預售商品詳情頁售賣規則,預售的開始時間和結束時間需要依賴一個處理器來完成。預售針對什麼商品類型生效需要依賴另一個處理器來完成,倘若你說這裡放一個處理器來完成行不行,這裡會有隱患。假設這裡放在一個處理器中,無法保證多個無相關的操作是否會相互影響,比方說前者異常了就直接影響後者了,所以這裡隔離出兩個處理器合適。

處理好了規則和處理器之間的關係,能看出這裡的關係是1:N的,也就是一個規則可以映射出多個規則處理器。

規則組的演變

寫到這裡,思考這裡還會有什麼新問題產生呢?不妨思考這個例子,我們假設:

A規則映射著編號為1,編號為2,編號為3的規則處理器;
B規則映射著編號為2,編號為4的規則處理器;
C規則映射著編號為3,編號為5的規則處理器;

現在有某一個M業務產品鏈路,這條鏈路上需要走A規則和B規則貫穿,另一個N業務產品鏈路,這條鏈路上需要走B規則核C規則貫穿。

你可能會這樣設計,我將A規則和B規則進行合併,衍生出一個合併(AB)規則,裡面包含編號為1,編號為2,編號為3,編號為4的規則處理器。也就是規則池中有4個規則(A、B、C、(AB))。

那麼依法炮製,針對N業務產品鏈路相同的方法操作,規則池中就有了5個規則。相信也看到了,這回導致什麼?

規則池的泛濫。這是個致命的缺點,因為你後面無法一眼看出哪些規則可以復用。

為了解決此問題,需要用到數據結構中的樹結構。這是一個經典的思想,利用樹層級關係特性來梳理關係。所以針對上面的情況,我們可以不用增加任何新的衍生規則。在這些規則中劃出很多條上層線條牽扯合併成上一層節點,注意這裡理論上可以無限的往上,需要根據業務複雜度合理抽象。我們叫之為規則組

規則Node抽象定義

前者我們定義了規則注解作用打上類頭上,為了更好地定義規則節點,這裡我們使用抽象泛型類定義(原理和上面說的規則處理器是一樣的),一個規則具備多個規則處理器,我們不能開放這種處理的能力對外,根據Java的封裝特徵,規則和處理器之間就相差一個發出指令,可以理解由規則向規則處理器發起。所以我們需要在抽象規則中定義執行規則處理器的方法。performActions。既然規則和規則處理器是一對多的關係,這裡根據封裝需要在抽象規則定義中可以對規則處理器進行添加addHandler,這也是委託的設計模式。

public abstract class AbstractRule<T> {
    /**
     * 規則Id
     */
    @Getter
    @Setter
    private String id;
    /**
     * 規則名稱
     */
    @Getter
    @Setter
    private String name;
    /**
     * 規則處理器
     */
    @Getter
    @Setter
    private List<IHandler<RuleBaseEntity>> handlers;

    /**
     * 滿足的觸發條件
     *
     * @param t
     * @return
     */
    public abstract boolean evaluateConditions(T t);

    public AbstractRule<T> assembly(AbstractRule<T> aRule) {
        Rule annotation = getRuleAnnotation();
        aRule.setId(annotation.id());
        aRule.setName(annotation.name());
        return aRule;
    }

    public AbstractRule<T> addHandler(IHandler<RuleBaseEntity> iHandler) {
        if (objects.isNull(handlers)) {
            handlers = new ArrayList<>();
            handlers.add(iHandler);
        } else {
            handlers.add(iHandler);
        }
        return this;
    }

    /**
     * 獲取規則注解
     *
     * @return
     */
    public abstract Rule getRuleAnnotation();

    public abstract AbstractRule<T> builder();

    /**
     * 規則處理器handler處理事件
     */
    public void performActions() {
        for (IHandler<RuleBaseEntity> handler : handlers) {
            handler.onEvent(EntityContextHolder.getContext());
        }
    }
}

規則組的定義

規則組是對規則的組合衍生的節點。一個規則組首先需要定義規則組id,這裡和規則的定義道理是一樣的,包括name的設計,規則組下的規則執行其實是一套策略需要提供給使用方,暴露選擇權給外部,比方說這些規則是串行執行,還是並行執行,還是部分串行,部分並行。亦或是規則之間需要順序,哪個規則必須在哪個規則執行結束完成後才能執行,等等,所以我們需要提供策略包給到使用方。

這裡你可以使用枚舉先窮舉常用寫在你的元件裡,另外再提供可擴展口子給到使用方。這裡為了演示,我們元件中策略包只有是否並行還是串行。

再結合封裝特性,規則組下包含規則這是一對多的模型,這時候就需要使用委派,提供對規則的添加方法。另外為了實例化方便,我們暴露關鍵參數給外部實例化方便。像是否並發這種字段對於元件來說是內部的語意,無須給使用者暴露,我們需要給使用者說明的是你就調這個方法,就可以實現並發執行規則,你調這個是串行執行規則,在方法名中做清晰隔離就行。另外就是執行的方法,如果滿足規則條件則執行其對應的規則處理器。

@Data
public class RuleGroupDriver {
    private RuleGroupDriver() {
    }

    /**
     * 規則組ID
     */
    private String ruleGroupId;

    /**
     * 規則組name
     */
    private String ruleGroupName;

    /**
     * 是否並發處理
     */
    private Boolean parallelInvoke;
    /**
     * 規則集
     */
    private Set<AbstractRule> rules;

    public RuleGroupDriver bindingRule(AbstractRule rule) {
        if (objects.isNull(rules)) {
            this.rules = new HashSet<>();
            rules.add(rule);
        } else {
            rules.add(rule);
        }
        return this;
    }

    /**
     * 實例化單線程處理規則組
     *
     * @param ruleGroupId
     * @param ruleGroupName
     */
    public static RuleGroupDriver instanceRuleDriver(String ruleGroupId, String ruleGroupName) {
        RuleGroupDriver ruleGroupDriver = new RuleGroupDriver();
        ruleGroupDriver.setRuleGroupId(ruleGroupId);
        ruleGroupDriver.setRuleGroupName(ruleGroupName);
        ruleGroupDriver.setParallelInvoke(Boolean.FALSE);
        return ruleGroupDriver;
    }

    /**
     * 實例化多線程處理規則組
     *
     * @param ruleGroupId
     * @param ruleGroupName
     */
    public static RuleGroupDriver instanceRuleDriverWithParallel(String ruleGroupId, String ruleGroupName) {
        RuleGroupDriver ruleGroupDriver = new RuleGroupDriver();
        ruleGroupDriver.setRuleGroupId(ruleGroupId);
        ruleGroupDriver.setRuleGroupName(ruleGroupName);
        ruleGroupDriver.setParallelInvoke(Boolean.TRUE);
        return ruleGroupDriver;
    }

    /**
     * 規則組處理
     */
    public void process() {
        if (parallelInvoke) {
            System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱:" + ruleGroupName + "---> 接受到處理任務]");
            CountDownLatch begin = new CountDownLatch(1);
            CountDownLatch end = new CountDownLatch(rules.size());
            ExecutorService exec = Executors.newFixedThreadPool(rules.size());
            AbstractRule[] abstractRules = rules.toArray(new AbstractRule[0]);
            System.out.println("該規則組下共有" + abstractRules.length + "子規則");
            for (int index = 0; index < abstractRules.length; index++) {
                AbstractRule rule = abstractRules[index];
                System.out.println("該規則下共有" + rule.getHandlers().size() + "子處理器");
                Runnable run = () -> {
                    try {
                        begin.await();
                        if (rule.evaluateConditions(EntityContextHolder.getContext())) {
                            System.out.println("[規則Id:" + rule.getId() + "---> 規則名稱:" + rule.getName() + "---> 被觸發]");
                            rule.performActions();
                            System.out.println("[規則Id:" + rule.getId() + "---> 規則名稱:" + rule.getName() + "---> 被成功執行]");
                        }
                    } catch (InterruptedException e) {
                        System.out.println("[規則Id:" + rule.getId() + "---> 規則名稱:" + rule.getName() + "---> 執行異常]");
                    } finally {
                        end.countDown();
                    }
                };
                exec.submit(run);
            }
            System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱:" + ruleGroupName + "---> 多線程開始處理]");
            begin.countDown();
            try {
                end.await();
            } catch (InterruptedException e) {
                System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱:" + ruleGroupName + "---> 規則組執行過程中異常中斷]");
            }
            System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱" + ruleGroupName + "---> 多線程處理完成]");
            exec.shutdown();
        } else {
            System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱:" + ruleGroupName + "---> 接受到處理任務]");
            System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱:" + ruleGroupName + "---> 單線程開始處理]");
            System.out.println("該規則組下共有" + rules.size() + "子規則");
            for (AbstractRule rule : rules) {
                try {
                    System.out.println("該規則下共有" + rule.getHandlers().size() + "子處理器");
                    if (rule.evaluateConditions(EntityContextHolder.getContext())) {
                        System.out.println("[規則Id:" + rule.getId() + "---> 規則名稱:" + rule.getName() + "---> 被觸發]");
                        rule.performActions();
                        System.out.println("[規則Id:" + rule.getId() + "---> 規則名稱:" + rule.getName() + "---> 被成功執行]");
                    }
                } catch (Exception ex) {
                    System.out.println("[規則Id:" + rule.getId() + "---> 規則名稱:" + rule.getName() + "---> 執行異常]");
                }
            }
            System.out.println("[規則組Id:" + ruleGroupId + "---> 規則組名稱:" + ruleGroupName + "---> 單線程處理完成]");
        }
    }
}

案例演示

我們來設計一個場景,驗證下這個元件。假設現在商品發佈流程有三個規則,分別是商品標題敏感詞校驗規則、商品行銷互斥校驗規則、商品庫存限購校驗規則。我們先簡單定義一個商品類:

@Data
public class LbItem implements Serializable {
    private Long itemId;
    private String itemTitle;
    private Long itemBasicPrice;
    private Long itemMarketingPrice;
    private Integer itemStockNum;
}

具體的規則如下:

@Rule(id = "itemTitleSensitiveCheckRule", name = "商品發佈規則", detail = "商品標題敏感詞校驗規則")
public class ItemTitleSensitiveCheckRule extends AbstractRule<LbItem> {
    @Override
    public boolean evaluateConditions(LbItem lbItem) {
        return true;
    }

    @Override
    public Rule getRuleAnnotation() {
        return ItemTitleSensitiveCheckRule.class.getAnnotation(Rule.class);
    }

    @Override
    public ItemTitleSensitiveCheckRule builder() {
        return (ItemTitleSensitiveCheckRule) assembly(new ItemTitleSensitiveCheckRule());
    }

    public static ItemTitleSensitiveCheckRule build() {
        ItemTitleSensitiveCheckRule checkRule = new ItemTitleSensitiveCheckRule();
        return checkRule.builder();
    }
}
@Rule(id = "itemStockNumQuotaCheckRule", name = "商品發佈規則", detail = "商品庫存限購校驗規則")
public class ItemStockNumQuotaCheckRule extends AbstractRule<LbItem> {
    @Override
    public boolean evaluateConditions(LbItem lbItem) {
        return true;
    }

    @Override
    public Rule getRuleAnnotation() {
        return ItemStockNumQuotaCheckRule.class.getAnnotation(Rule.class);
    }

    @Override
    public ItemStockNumQuotaCheckRule builder() {
        return (ItemStockNumQuotaCheckRule) assembly(new ItemStockNumQuotaCheckRule());
    }

    public static ItemStockNumQuotaCheckRule build() {
        ItemStockNumQuotaCheckRule checkRule = new ItemStockNumQuotaCheckRule();
        return checkRule.builder();
    }
}
@Rule(id = "itemMarketingLimitCheckRule", name = "商品發佈規則", detail = "商品行銷互斥校驗規則")
public class ItemMarketingLimitCheckRule extends AbstractRule<LbItem> {
    @Override
    public boolean evaluateConditions(LbItem lbItem) {
        return false;
    }

    @Override
    public Rule getRuleAnnotation() {
        return ItemMarketingLimitCheckRule.class.getAnnotation(Rule.class);
    }

    @Override
    public ItemMarketingLimitCheckRule builder() {
        return (ItemMarketingLimitCheckRule) assembly(new ItemMarketingLimitCheckRule());
    }

    public static ItemMarketingLimitCheckRule build() {
        ItemMarketingLimitCheckRule checkRule = new ItemMarketingLimitCheckRule();
        return checkRule.builder();
    }
}

規則處理器定義如下:

public class ItemTitleSensitiveRiskHandler implements IHandler<RuleBaseEntity> {
    @Override
    public void onEvent(RuleBaseEntity t) {
        //do your biz logic 做你的業務邏輯
        System.out.println("風控檢查商品標題是否含有敏感詞");
    }
}
public class ItemCategoryRecommendHandler implements IHandler<RuleBaseEntity> {
    @Override
    public void onEvent(RuleBaseEntity order) {
        //do your biz logic 做你的業務邏輯
        System.out.println("檢查商品類目推薦匹配");
    }
}
public class ItemOtherMoreInfoHandler implements IHandler<RuleBaseEntity> {
    @Override
    public void onEvent(RuleBaseEntity t) {
        //do your biz logic 做你的業務邏輯
        System.out.println("結合業務,更多的檢查處理器");
    }
}

ok,準備工作一切就緒,讓我們來測試一下吧。

public class Client {
    public static void main(String[] args) {
        // 單個規則組測試
        singleRuleGroupTest();

        // 給大家留下的作業
        multiRuleGroupTest();
    }

    private static void multiRuleGroupTest() {
        // 快來try try coding
    }

    private static void singleRuleGroupTest() {
        String ruleGroupId = "1";
        String ruleGroupName = "商品發佈規則組";
        RuleGroupDriver ruleGroupDriver = RuleGroupDriver.instanceRuleDriver(ruleGroupId, ruleGroupName);
        ItemTitleSensitiveCheckRule rule1 = ItemTitleSensitiveCheckRule.build();
        rule1.addHandler(new ItemTitleSensitiveRiskHandler())
                .addHandler(new ItemCategoryRecommendHandler())
                .addHandler(new ItemOtherMoreInfoHandler());

        //如法炮製等..
        //ItemStockNumQuotaCheckRule rule2 = ItemStockNumQuotaCheckRule.build();
        ruleGroupDriver.bindingRule(rule1);
        ruleGroupDriver.process();
    }
}

image

這套元件就分享到這裡了,有疑問的同學可以結合源碼實現反復看幾遍。元件設計過程是如何一步步思考的,建議把這些問題都搞懂,優秀的設計和理解能力對於開發是比較重要的~


原文出處:https://juejin.cn/post/7555704883988824116


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝19   💬9   ❤️5
726
🥈
我愛JS
📝4   💬14   ❤️7
249
🥉
御魂
💬1  
3
#5
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付