聊一聊責任鏈模式

来源:https://www.cnblogs.com/cicada-smile/archive/2022/11/01/16841111.html
-Advertisement-
Play Games

責任鏈模式(Chain of Responsibility Pattern)是將鏈中每一個節點看作是一個對象,每個節點處理的請求均不同,且內部自動維護一個下一節點對象。 ...


將一堆“事情”串聯在一起,有序執行,就叫責任鏈

一、概述

責任鏈模式(Chain of Responsibility Pattern)是將鏈中每一個節點看作是一個對象,每個節點處理的請求均不同,且內部自動維護一個下一節點對象。當一個請求從鏈式的首端發出時,會沿著鏈的路徑依次傳遞給每一個節點對象,直至有對象處理這個請求為止,屬於行為型模式。
下麵放一張足球比賽的圖,通過層層傳遞,最終射門。通過這張圖,可以更好的理解責任鏈模式。

二、入門案例

2.1 類圖

2.2 基礎類介紹

抽象介面RequestHandler

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 13:41
 * @description
 */
public interface RequestHandler {

    void doHandler(String req);
}

抽象類BaseRequestHandler

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 13:45
 * @description
 */
public abstract class BaseRequestHandler implements RequestHandler {

    protected RequestHandler next;

    public void next(RequestHandler next) {
        this.next = next;
    }
}

具體處理類AHandler

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 14:00
 * @description
 */
public class AHandler extends BaseRequestHandler {

    @Override
    public void doHandler(String req) {
        // 處理自己的業務邏輯
        System.out.println("A中處理自己的邏輯");
        // 傳遞給下個類(若鏈路中還有下個處理類)
        if (next != null) {
            next.doHandler(req);
        }
    }
}

當然還有具體的處理類B、C等等,這裡不展開贅述。
使用類Client

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 14:06
 * @description
 */
public class Client {
    public static void main(String[] args) {
        BaseRequestHandler a = new AHandler();
        BaseRequestHandler b = new BHandler();
        BaseRequestHandler c = new CHandler();
        a.next(b);
        b.next(c);
        a.doHandler("鏈路待處理的數據");
    }
}

2.3 處理流程圖

三、應用場景

3.1 場景舉例

場景一

前兩年,在一家金融公司待過一段時間,其中就有一個業務場景:一筆訂單進來,會先在後臺通過初審人員進行審批,初審不通過,訂單流程結束。初審通過以後,會轉給終審人員進行審批,不通過,流程結束;通過,流轉到下個業務場景。
對於這塊業務代碼,之前一代目是一個叫知了的同事,他擼起袖子就是乾,一套if-else乾到底。後來,技術老大CodeReview,點名要求改掉這塊。於是乎,想到用用設計模式吧,然後就噼里啪啦一頓改。(當然,比較複雜的情況,還是可以用工作流來處理這個場景,當時礙於時間成本,也就放棄了)。

場景二

上家公司對接甲方爸爸的時候,對方會調用我們介面,將數據同步過來。同樣,我們需要將處理好的數據,傳給他們。由於雙方傳輸數據都是加密傳輸,所以在接受他們數據之前,需要對數據進行解密,驗簽,參數校驗等操作。同樣,我們給他們傳數據也需要進行加簽,加密操作。

具體案例

話不多說,對於場景二,我來放一些偽代碼,跟大家一起探討下。
1、一切從註解開始,我這裡自定義了一個註解@Duty,這個註解有spring的@Component註解,也就是標記了這個自定義註解的類,都是交給spring的bean容器去管理。
註解中,有兩個屬性:1.type,定義相同的type類型的bean,會被放到一個責任鏈集合中。2.order,同一個責任鏈集合中,bean的排序,數值越小,會放到鏈路最先的位置,優先處理。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 16:11
 * @description
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Service
public @interface Duty {
    /**
     * 標記具體業務場景
     * @return
     */
    String type() default "";

    /**
     * 排序:數值越小,排序越前
     * @return
     */
    int order() default 0;
}

2、定義一個頂層的抽象介面IHandler,傳入2個泛型參數,供後續自定義。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 15:31
 * @description 責任鏈頂層抽象類
 */
public interface IHandler<T, R> {
    /**
     * 抽象處理類
     * @param t
     * @return
     */
    R handle(T t);
}

3、定義一個責任鏈bean的管理類HandleChainManager,用來存放不同業務下的責任鏈路集合。在該類中,有一個Map和兩個方法。

  1. handleMap:這個map會存放責任鏈路中,具體的執行類,key是註解@Duty中定義的type值,value是標記了@Duty註解的bean集合,也就是具體的執行類集合。
  2. setHandleMap:傳入具體執行bean的集合,存放在map中。
  3. executeHandle:從map中找到具體的執行bean集合,並依次執行。
/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 16:00
 * @description 責任鏈管理類
 */
public class HandleChainManager {
    /**
     * 存放責任鏈路上的具體處理類
     * k-具體業務場景名稱
     * v-具體業務場景下的責任鏈路集合
     */
    private Map<String, List<IHandler>> handleMap;

    /**
     * 存放系統中責任鏈具體處理類
     * @param handlerList
     */
    public void setHandleMap(List<IHandler> handlerList) {
        handleMap = handlerList
                .stream()
                .sorted(Comparator.comparingInt(h -> AnnotationUtils.findAnnotation(h.getClass(), Duty.class).order()))
                .collect(Collectors.groupingBy(handler -> AnnotationUtils.findAnnotation(handler.getClass(), Duty.class).type()));
    }

    /**
     * 執行具體業務場景中的責任鏈集合
     * @param type 對應@Duty註解中的type,可以定義為具體業務場景
     * @param t 被執行的參數
     */
    public <T, R> R executeHandle(String type, T t) {
        List<IHandler> handlers = handleMap.get(type);
        R r = null;
        if (CollectionUtil.isNotEmpty(handlers)) {
            for (IHandler<T, R> handler : handlers) {
               r = handler.handle(t);
            }
        }
        return r;
    }
}

4、定義一個配置類PatternConfiguration,用於裝配上面的責任鏈管理器HandleChainManager

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 15:35
 * @description 設計模式配置類
 */
@Configuration
public class PatternConfiguration {

    @Bean
    public HandleChainManager handlerChainExecute(List<IHandler> handlers) {
        HandleChainManager handleChainManager = new HandleChainManager();
        handleChainManager.setHandleMap(handlers);
        return handleChainManager;
    }

}

5、具體的處理類:SignChainHandlerEncryptionChainHandlerRequestChainHandler,這裡我以SignChainHandler為例。
在具體處理類上標記自定義註解@Duty,該類會被註入到bean容器中,實現IHandler介面,只需關心自己的handle方法,處理具體的業務邏輯。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/10/25 15:31
 * @description 加簽類
 */
@Duty(type = BusinessConstants.REQUEST, order = 1)
public class SignChainHandler implements IHandler<String, String> {
    /**
     * 處理加簽邏輯
     * @param s
     * @return
     */
    @Override
    public String handle(String s) {
        // 加簽邏輯
        System.out.println("甲方爸爸要求加簽");
        return "加簽";
    }
}

6、具體怎麼調用?這裡我寫了個測試controller直接調用,具體如下:

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/9/6 17:32
 * @description
 */
@RestController
@Slf4j
public class TestController {

    @Resource
    private HandleChainManager handleChainManager;

    @PostMapping("/send")
    public String duty(@RequestBody String requestBody) {
        String response = handleChainManager.executeHandle(BusinessConstants.REQUEST, requestBody);
        return response;
    }
}

7、執行結果,會按照註解中標記的order依次執行。

至此,完工。又可以開心的擼代碼了,然後在具體的執行類中,又是一頓if-else。。。

四、源碼中運用

4.1Mybatis源碼中的運用

Mybatis中的緩存介面Cache,cache作為一個緩存介面,最主要的功能就是添加和獲取緩存的功能,作為介面它有11個實現類,分別實現不同的功能,下麵是介面源碼和實現類。

package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

public interface Cache {
    String getId();

    void putObject(Object var1, Object var2);

    Object getObject(Object var1);

    Object removeObject(Object var1);

    void clear();

    int getSize();

    default ReadWriteLock getReadWriteLock() {
        return null;
    }
}

下麵,我們來看下其中一個子類LoggingCache的源碼。主要看他的putObject方法和getObject方法,它在方法中直接傳給下一個實現去執行。這個實現類其實是為了在獲取緩存的時候列印緩存的命中率的。

public class LoggingCache implements Cache {
    private final Log log;
    private final Cache delegate;
    protected int requests = 0;
    protected int hits = 0;

    public LoggingCache(Cache delegate) {
        this.delegate = delegate;
        this.log = LogFactory.getLog(this.getId());
    }

    // ...
    public void putObject(Object key, Object object) {
        this.delegate.putObject(key, object);
    }

    public Object getObject(Object key) {
        ++this.requests;
        Object value = this.delegate.getObject(key);
        if (value != null) {
            ++this.hits;
        }

        if (this.log.isDebugEnabled()) {
            this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());
        }

        return value;
    }
    // ...
}

最後,經過Cache介面各種實現類的處理,最終會到達PerpetualCache這個實現類。與之前的處理類不同的是,這個類中有一個map,在map中做存取,也就是說,最終緩存還是會保存在map中的。

public class PerpetualCache implements Cache {
    private final String id;
    private final Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

	// ...

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }
	// ...

}

4.2spring源碼中的運用

4.2.1DispatcherServlet類

DispatcherServlet 核心方法 doDispatch。HandlerExecutionChain只是維護HandlerInterceptor的集合,可以向其中註冊相應的攔截器,本身不直接處理請求,將請求分配給責任鏈上註冊處理器執行,降低職責鏈本身與處理邏輯之間的耦合程度。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;
		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
		try {
			ModelAndView mv = null;
			Exception dispatchException = null;
			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);
				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}
				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}
				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

4.2.2HandlerExecutionChain類

這裡分析的幾個方法,都是從DispatcherServlet類的doDispatch方法中請求的。

  • 獲取攔截器,執行preHandle方法
boolean applyPreHandle(HttpServletRequest request, 
                       HttpServletResponse response) throws Exception {
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
            HandlerInterceptor interceptor = interceptors[i];
            if (!interceptor.preHandle(request, response, this.handler)) {
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }
    }
    return true;
}
  • 在applyPreHandle方法中,執行triggerAfterCompletion方法
void triggerAfterCompletion(HttpServletRequest request, 
                            HttpServletResponse response, Exception ex) throws Exception {
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        for(int i = this.interceptorIndex; i >= 0; --i) {
            HandlerInterceptor interceptor = interceptors[i];
            try {
                interceptor.afterCompletion(request, response, this.handler, ex);
            } catch (Throwable var8) {
                logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
            }
        }
    }
}
  • 獲取攔截器,執行applyPostHandle方法
void applyPostHandle(HttpServletRequest request, 
                     HttpServletResponse response, ModelAndView mv) 
                     throws Exception {
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        for(int i = interceptors.length - 1; i >= 0; --i) {
            HandlerInterceptor interceptor = interceptors[i];
            interceptor.postHandle(request, response, this.handler, mv);
        }
    }
}

五、總結

5.1 優點

  1. 將請求與處理解耦。
  2. 請求處理者(節點對象)只需要關註自己感興趣的請求進行處理即可,對於不感興趣的請求,轉發給下一個節點。
  3. 具備鏈式傳遞處理請求功能,請求發送者無需知曉鏈路結構,只需等待請求處理結果。
  4. 鏈路結構靈活,可以通過改變鏈路的結構動態的新增或刪減責任。
  5. 易於擴展新的請求處理類(節點),符合開閉原則

5.2 缺點

  1. 責任鏈太長或者處理時間過長,會影響整體性能。
  2. 如果節點對象存在迴圈引用時,會造成死迴圈,導致系統崩潰。

六、參考源碼

編程文檔:
https://gitee.com/cicadasmile/butte-java-note

應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent
Gitee主頁: https://gitee.com/cicadasmile/butte-java-note
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Rule90 第一次見這東西有點莫名其妙,但是其實看懂了之後就是左移和右移相異或,註意這裡使用的是邏輯右移,會自動補零,不能使用算數左移<<<。 module top_module( input clk, input load, input [511:0] data, output reg[511: ...
  • 1.封裝函數,可以判斷一個數字是否為偶數 def func(n): if n%2==0: print("%d是偶數"%n) else: print("%d是奇數"%n) func(11) # 11是奇數 2.封裝函數,可以實現1-n之間所有偶數的列印 def func(n): for i in ra ...
  • 這不光棍節快到了,表弟準備寫一封情書給他的女神,想在光棍節之前脫單。 為了提高成功率,於是跑來找我給他參謀參謀,本來我是不想理他的,不過誰讓他是我表弟呢(請我洗jio),於是教給他程式員的終極浪漫絕招,先假裝給女神拍照,然後再把情書寫到她的照片上列印出來送給她,嘿嘿~ 實現步驟 想要實現把情書寫在像 ...
  • 前言 大家好,我是棧長。 最近,棧長又參加了騰訊雲小伙伴邀請的Techo Day 技術開放日 2.0的線上活動,這一期又是乾貨滿滿,主要是雲原生和微服務方面的,比如:雲原生網關、容器、安全、雲監控、灰度發佈等等,這些內容都與我們現有的微服務系統息息相關。 令棧長印象最深刻的就是微服務灰度發佈這個主題 ...
  • 在分散式系統盛行的今天,緩存充當著扛壓屏障的作用,一旦緩存出現問題,對系統影響也是致命的。本文我們一起聊聊如何安全且可靠的使用緩存,聊聊緩存擊穿、緩存雪崩、緩存穿透以及數據一致性、熱點數據淘汰機制等。 ...
  • 1.遍歷/匹配(foreach/find/match) Stream也是支持類似集合的遍歷和匹配元素的,只是Stream中的元素是以Optional類型存在的。Stream的遍歷、匹配非常簡單。 List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 7, ...
  • 1,JDK和JRE有什麼區別? JRE:Java Runtime Environment( java 運行時環境)。即java程式的運行時環境,包含了 java 虛擬機,java基礎類庫。 JDK:Java Development Kit( java 開發工具包)。即java語言編寫的程式所需的開發 ...
  • 作者:牛牛碼特 鏈接:https://juejin.cn/post/6844903929281511438 背景 緩存是軟體開發中一個非常有用的概念,資料庫緩存更是在項目中必然會遇到的場景。而緩存一致性的保證,更是在面試中被反覆問到,這裡進行一下總結,針對不同的要求,選擇恰到好處的一致性方案。 緩存 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...