天天看點

責任鍊模式在複雜資料處理場景中的實戰

作者:黃維傑

相信大家在日常的開發中都遇到過複雜資料處理和複雜資料校驗的場景,本文從一線開發者的角度,分享了責任鍊模式在這種複雜資料處理場景下的實戰案例,此外,作者在普通責任鍊模式的基礎上進行了更新改造,可以适配更加複雜的應用場景,希望可以讓讀者對于設計模式-責任鍊模式有更深刻的印象。

一、什麼是責任鍊模式

1.1 概念

責任鍊模式讓多個對象都有機會處理同一個請求。它将請求的發送者和處理者之間進行解耦,同時将這些處理者對象連成一條鍊,并沿着這條鍊傳遞該請求,滿足條件的處理者會執行相應的邏輯直至走完整個鍊條。

1.2 應用場景

如果在一次請求中,需要多個處理者處理多種複雜的邏輯,且希望能夠解耦多個處理者,實作高擴充性,可以考慮使用責任鍊模式。

二、Servlet中的過濾器(Filter)和過濾器鍊(FilterChain)

2.1 概念

Filter和FilterChain是【責任鍊模式】的一種熱門應用場景,過濾器Filter相信大家都很熟悉了,我們在Servlet中經常能發現它的身影。

【Filter】一般用于Servlet處理之前做一些前置的校驗,每個Filter都有自己的職責和邏輯。調用filter時,需要傳入目前filterChain的引用,來告訴filter目前執行的是哪一個filterChain。

public interface Filter {
    //初始化方法
    public void init(FilterConfig filterConfig) throws ServletException;
    //處理邏輯,
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;
    //生命周期銷毀
    public void destroy();
}           

【FilterChain】是由多個Filter組成的鍊條,如果在鍊上的filter校驗通過或處理完成,那麼調用"chain.doFilter(request, response)"就可以讓下一個filter繼續執行邏輯直到filterChain結束

public interface FilterChain {
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException;
}           

2.2 在Servlet中的攔截過程

請求資源時,過濾器鍊中的過濾器依次對請求進行處理,并将請求傳遞給下一個過濾器,直到最後将請求傳遞給目标資源。發送響應資訊時,則按照相反的順序對響應進行處理,直到将響應傳回給用戶端。

責任鍊模式在複雜資料處理場景中的實戰

2.3 啟發

Servlet中的責任鍊模式給我們展示的是利用Filter和FilterChain來過濾和攔截請求;我們在複雜的業務場景中是否也能模仿來實作一個業務處理的責任鍊呢?

我們可以發現,Filter與Filter之間是互相解耦的,我們可以很輕量級的加入一個新的Filter到FilterChain當中;

此外,我們還可以實作多個FilterChain,其中裝載不同的Filter來适配多種業務場景。

三、業務場景應用案例

3.1 業務場景

在電商平台的很多業務場景下,涉及到對于資料的多重校驗或多重過濾等操作。而随着業務的增長,校驗邏輯或者資料處理邏輯會變得越來越複雜,這個時候責任鍊模式就能夠展現出很好的優勢了。

拿建立優惠券活動來舉例;使用者可以自由選擇某些類目、某些商品或者某些門店來參與券活動;并且可以按需導入或者選擇自己需要參與活動的資料;

【系統需要校驗使用者上傳的資料是否滿足業務條件、并将校驗失敗的資料傳回給客戶】

3.2 複雜點

根據上述業務場景,在一次請求中,我們需要做多種校驗邏輯:

  • 資料鑒權校驗、過濾使用者無權限的資料。
  • 若使用者選擇商品,需對商品類型進行校驗;(電商的商品模型有很多種,每一種商品模型都對應一種校驗規則)。
  • 若使用者選擇門店,需對門店類型進行校驗;(電商的門店類型也有很多,比如線上門店、旗艦店、線下門店等等,需要判斷門店是否能夠參與優惠活動)。
  • 對于不同投放管道也有不同管道的校驗規則。
  • 我們需要完整的走完所有校驗邏輯,而不能因為中途的一個邏輯校驗不通過而阻斷校驗,因為我們需要傳回給使用者一個完整的資料校驗結果;舉個例子,如果使用者上傳的商品當中,既存在無權限的商品,又存在不符合商品類型的資料,那麼我們需要走完所有校驗邏輯,一并給使用者傳回所有的報錯,而不是隻傳回無權限的商品。
  • 其他校驗規則……

如果不使用設計模式

如果使用流水式代碼,将會顯得很臃腫,且有很多if else……嵌套,讓人很難看懂和維護。如下:

//校驗資料
if (使用者選擇商品) {
        if (商品模型一) {
        //校驗邏輯1
      } else if (商品模型二) {
        //校驗邏輯2
      } else if (商品模型三) {
        //校驗邏輯3
      } else {
        //校驗邏輯4
      }
} else if (使用者選擇門店) {
        if (門店模型一) {
            //校驗邏輯1
          } else if (門店模型二) {
            //校驗邏輯2
          }
  //校驗邏輯……
} else if (使用者選擇類目) {
  //校驗邏輯……
}
//校驗管道
if (管道是A管道){

} else if (管道是B管道){

}           

上述僞代碼僅僅隻覆寫了幾種簡單的校驗場景;試想就算開發完成之後,如果下次再有一個業務邏輯校驗需要加入進來,則對代碼需要進行很大的改動,需要重新梳理if else的邏輯,缺乏代碼的可讀性和可拓展性。

如果使用責任鍊模式

我們可以遵守單一職責原則,定義多個Filter對象,每個對象實作自己的業務校驗邏輯;同時主幹代碼上僅需要初始化一個FilterChain,并調用doFilter方法執行鍊上每一個filter即可。

3.3 使用責任鍊模式+改進

參照Servlet的filter與filterChain接口【見2.1】,實作了多種不同的過濾器,也在其基礎上結合業務需求進行了相應的改進:

定義AbstractOrderFilter抽象類

讓Filter對象具備順序屬性,初始化FilterChain的時候,可以按順序排列filter;同時定義accept方法,讓filter自行控制是否處理請求。

@Data
public abstract class AbstractOrderFilter implements Filter, Comparable<AbstractOrderFilter> {
    protected Integer order;
    @Override
    public int compareTo(AbstractOrderFilter o) {
        return getOrder().compareTo(o.getOrder());
    }
    //根據Filter自己使用的業務場景,自行定義
    public boolean accept(FilterRequestDTO filterRequestDTO) {
        return true;
    }
    @Override
    public void doFilter(FilterRequestDTO filterRequestDTO, FilterChain filterChain) {
    }
}           

繼承AbstractOrderFilter,遵守單一職責原則,實作多種Filter

舉例:定義一個ItemPermissionFilter,專門做商品權限校驗。

@Slf4j
public class ItemPermissionFilter extends AbstractOrderFilter {
    //目前filter對應的業務邏輯manager(自行根據業務場景定義)
    ItemCheckManager itemCheckManager;
    //構造器私有
    private ItemPermissionFilter(Integer order, ItemCheckManager itemCheckManager) {
        super.order = order;
        this.itemCheckManager = itemCheckManager;
    }
    @Override
    public void doFilter(FilterRequestDTO filterRequestDTO, FilterChain filterChain) {
        if (accept(filterRequestDTO)) {
          //業務邏輯對應的manager進行校驗處理(不做展開)
            itemCheckManager.checkItemPermission(filterRequestDTO, elementCheckResults);
        }
        //繼續走責任鍊的下一個filter
        filterChain.doFilter(filterRequestDTO);
    }
    @Override
    public boolean accept(FilterRequestDTO filterRequestDTO) {
        //自行根據業務場景定義處理何種請求
        return true;
    }
    //對外暴露的create方法
    public static ItemPermissionFilter create(Integer order, ItemCheckManager itemCheckManager) {
        return new ItemPermissionFilter(order, itemCheckManager);
    }
}           

定義CouponFilterChain實作filterChain接口,定義對于内部filter的處理邏輯

注意其中幾個屬性:

  • filters:是filterChain當中的filter集合。
  • posLocal:是一個ThreadLocal變量,記錄着目前filterChain執行到了第幾個filter的index。
  • checkResult:也是一個ThreadLocal變量,它記錄着全局所有Filter的校驗結果,每執行一個filter,filter就會把目前的執行結果記錄在該變量中,之後會統一傳回給使用者,大大減少了參數的傳遞複雜度。
public class CouponFilterChain implements FilterChain {
    /**
     * 責任鍊中的所有的處理元件 非變量
     */
    private final List<? extends AbstractOrderFilter> filters;
    /**
     * 目前執行到的位置 這是個共享變量
     */
    private static ThreadLocal<Integer> posLocal = ThreadLocal.withInitial(() -> 0);

    /**
     * 責任鍊的校驗結果--即需要給使用者回報的校驗結果,共享變量,threadLocal,會作為全局參數
     */
    public static final ThreadLocal<List<CheckResult>> checkResult = new ThreadLocal<>();
​
    /**
     * 包含filter數量 非變量
     */
    private final int size;
​
    @Override
    public void doFilter(FilterRequestDTO filterRequestDTO) {
      //共享變量記住目前filterChain執行的filter的index,直至結束
        Integer pos = posLocal.get();
        if (pos < size) {
            pos++;
            posLocal.set(pos);
            Filter filter = this.filters.get(pos - 1);
            filter.doFilter(filterRequestDTO, this);
        }
    }
​
    //供外部業務代碼調用的主要方法
   public BaseResult<CheckResult> process(FilterRequestDTO filterRequestDTO) {
        this.doFilter(filterRequestDTO);
        //将共享變量裡面的結果取出來,傳回給使用者
        return BaseResult.makeSuccess(checkResult.get(););
    }
​
    @Override
  //注意避免ThreadLocal記憶體洩漏,要remove
    public void reset() {
        posLocal.remove();
        posLocal.set(0);
        checkResult.remove();
    }
​
    public CouponFilterChain(List<? extends AbstractOrderFilter> filters) {
        filters.sort(AbstractOrderFilter::compareTo);
        this.filters = filters;
        this.size = filters.size();
    }
}           

責任鍊初始化 — 根據業務場景自行拼接filter

在實作好了Filter已經FilterChain之後,我們需要對他們進行初始化,這個時候就可以根據所需要的業務場景自行組裝filter到filterChain當中; 有多種初始化方法,下面隻簡單介紹一種(将商品filter和門店filter初始化)。

@Component
@Slf4j
public class FilterChainManager {
    @Resource
    StoreManager storeManager;
    @Resource
    ItemManager itemManager;
​
    private CouponFilterChain couponFilterChain;
​
    //初始化責任鍊
    @PostConstruct
    private void init() {
        //總鍊
        List<AbstractOrderFilter> filters = new ArrayList<>();
        //按需添加鍊上的filter……
        //商品校驗filter
        filters.add(ItemFilter.create(100, ItemManager));
        //門店校驗filter
        filters.add(StoreFilter.create(200, StoreManager));
        this.couponFilterChain = new CouponFilterChain(filters);
    }
​
   //供外部調用的方法
   public BaseResult<CheckResult> process(FilterRequestDTO filterRequestDTO) {
        BaseResult<CheckResult> result = null;
        try {
            //責任鍊模式,校驗每一個參數的合法性并輸出錯誤原因
            result = couponFilterChain.process(filterRequestDTO);
            return result
        } catch (Exception e) {
            return TMPResult.failOf("system error", e.getMessage());
        } finally {
            //這裡非常重要 必須重置
            if (couponFilterChain != null) {
                couponFilterChain.reset();
            }
        }
    }
}           

到此我們已經實作了責任鍊模式,可以畫個圖了解一下:

責任鍊模式在複雜資料處理場景中的實戰

拓展 — 組合過濾器CompositeFilter + FilterChain

有時候我們的filter當中可能需要加上一些子處理,為了遵守單一職責原則,不适合将這些業務邏輯放在同一個filter中,于是考慮将多個filter合并組合成一個大的Filter; SpringMVC還有一種過濾器叫做組合過濾器CompositeFilter,過濾器裡面嵌套過濾器,使得整個處理過程更加有層次;參照org.springframework.web.filter.CompositeFilter,自己定義了一個CompositeFilter。

/**
 * 合成的過濾器,改過濾器内部由多個過濾器組合而成
 */
public class CompositeFilter extends AbstractOrderFilter {
    /**
     * 所有的責任事件
     */
    private List<? extends AbstractOrderFilter> filters = new ArrayList();
​
    public CompositeFilter(Integer order, List<? extends AbstractOrderFilter> filters) {
        super.order = order;
        this.filters = filters;
    }
​
    @Override
    public void doFilter(FilterRequestDTO filterRequestDTO, FilterChain filterChain) {
        (new InnerFilterChain(filterChain, this.filters)).doFilter(filterRequestDTO);
    }
​
    /**
     * 内部鍊處理邏輯,優先将合成過濾器的内部過濾器進行處理,然後再傳給下一個過濾器
     */
    private static class InnerFilterChain implements FilterChain {
        private final FilterChain originalChain;
        private final List<? extends AbstractOrderFilter> additionalFilters;
        private int currentPosition = 0;
​
        public InnerFilterChain(FilterChain chain, List<? extends AbstractOrderFilter> additionalFilters) {
            this.originalChain = chain;
            this.additionalFilters = additionalFilters;
        }
​
        @Override
        public void doFilter(FilterRequestDTO filterRequestDTO) {
            if (this.currentPosition >= this.additionalFilters.size()) {
                //如果已經執行完了内部過濾器,則跳到外部繼續執行外部下一個節點的過濾器
                this.originalChain.doFilter(filterRequestDTO);
            } else {
                //繼續執行内部過濾器
                this.currentPosition++;
                AbstractOrderFilter currentFilter = this.additionalFilters.get(this.currentPosition - 1);
                currentFilter.doFilter(filterRequestDTO, this);
            }
​
        }
​
        @Override
        public void reset() {
​
        }
    }
​
    public static CompositeFilter create(Integer order, List<? extends AbstractOrderFilter> filters) {
        filters.sort(AbstractOrderFilter::compareTo);
        return new CompositeFilter(order, filters);
    }
}           

實作了組合過濾器之後,可以将其與FilterChain結合; 示意圖如下圖所示,在一條責任鍊上可以有普通的Filter和CompositeFilter,當執行到B時,按照B内部的順序,從内部子filterB1執行開始一直到B4,直到執行完整個組合責任鍊然後再執行C,依次類推。可以看出CompositeFilter讓整個責任鍊子產品化,子產品與子產品之間能夠各司其職,子產品内部也能按照自定義的順序執行。

責任鍊模式在複雜資料處理場景中的實戰

四、思考

本文實作的責任鍊僅供參考,大家可以結合自己的業務場景定義合适的FilterChain和Filter;與Servlet中的Filter不同的是,本文中定義的Filter隻有一個入參FilterRequestDTO,而是将response作為了ThreadLocal共享變量,大家使用時也一定要注意記憶體洩漏的風險。

其實不需要FilterChain,我們隻需要使用一個List<Filter>并保留Filter與Filter之間的引用關系即可,如一個Filter的next指針指向下一個Filter;定義FilterChain的原因我想也是開發者考慮到封裝和更好的變化。

如果直接上手閱讀源碼,很容易被層層的方法帶亂自己的陣腳;帶着問題和目的去閱讀和學習源碼是一件事半功倍的事情,會讓你在學習過程中有一條清晰明朗的線。

繼續閱讀