一、概念
責任鏈模式讓多個對象都有機會處理同一個請求。它將請求的發送者和處理者之間進行解耦,同時將這些處理者對象連成一條鏈,并沿著這條鏈傳遞該請求,滿足條件的處理者會執行相應的邏輯直至走完整個鏈條。
二、應用場景
如果在一次請求中,需要多個處理者處理多種復雜的邏輯,且希望能夠解耦多個處理者,實現高擴展性,可以考慮使用責任鏈模式。
三、Servlet中的過濾器(Filter)和過濾器鏈(FilterChain)
1.概念
Filter和FilterChain是【責任鏈模式】的一種熱門應用場景,【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;
}
四、在Servlet中的攔截過程
請求資源時,過濾器鏈中的過濾器依次對請求進行處理,并將請求傳遞給下一個過濾器,直到最后將請求傳遞給目標資源。發送響應信息時,則按照相反的順序對響應進行處理,直到將響應返回給客戶端;
啟發
Servlet中的責任鏈模式給我們展示的是利用Filter和FilterChain來過濾和攔截請求;我們在復雜的業務場景中是否也能模仿來實現一個業務處理的責任鏈呢?
我們可以發現,Filter與Filter之間是互相解耦的,我們可以很輕量級的加入一個新的Filter到FilterChain當中;
此外,我們還可以實現多個FilterChain,其中裝載不同的Filter來適配多種業務場景。
五、業務場景應用案例
1.業務場景
在電商平臺的很多業務場景下,涉及到對于數據的多重校驗或多重過濾等操作。而隨著業務的增長,校驗邏輯或者數據處理邏輯會變得越來越復雜,這個時候責任鏈模式就能夠體現出很好的優勢了。
拿創建優惠券活動來舉例;用戶可以自由選擇某些類目、某些商品或者某些門店來參與券活動;并且可以按需導入或者選擇自己需要參與活動的數據;
【系統需要校驗用戶上傳的數據是否滿足業務條件、并將校驗失敗的數據返回給客戶】
2.復雜點
根據上述業務場景,在一次請求中,我們需要做多種校驗邏輯:
- 數據鑒權校驗、過濾用戶無權限的數據
- 若用戶選擇商品,需對商品類型進行校驗;(電商的商品模型有很多種,每一種商品模型都對應一種校驗規則)
- 若用戶選擇門店,需對門店類型進行校驗;(電商的門店類型也有很多,比如線上門店、旗艦店、線下門店等等,需要判斷門店是否能夠參與優惠活動)
- 對于不同投放渠道也有不同渠道的校驗規則
- 我們需要完整的走完所有校驗邏輯,而不能因為中途的一個邏輯校驗不通過而阻斷校驗,因為我們需要返回給用戶一個完整的數據校驗結果;舉個例子,如果用戶上傳的商品當中,既存在無權限的商品,又存在不符合商品類型的數據,那么我們需要走完所有校驗邏輯,一并給用戶返回所有的報錯,而不是只返回無權限的商品;
- 其他校驗規則……
- 如果不使用設計模式
如果使用流水式代碼,將會顯得很臃腫,且有很多ifelse……嵌套,讓人很難看懂和維護。如下:
//校驗數據
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 的邏輯,缺乏代碼的可讀性和可拓展性。
l 如果使用責任鏈模式
我們可以遵守單一職責原則,定義多個Filter對象,每個對象實現自己的業務校驗邏輯;同時主干代碼上僅需要初始化一個FilterChain,并調用doFilter方法執行鏈上每一個filter即可。
3.使用責任鏈模式+改進
參照Servlet的filter與filterChain接口,自己實現了多種不同的過濾器,也在其基礎上結合業務需求進行了相應的改進:
l 定義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) {
}
}
l 繼承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);
}
}
l 定義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();
}
}
l 責任鏈初始化--根據業務場景自行拼接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();
}
}
}
}