一分鐘學會、三分鐘上手、五分鐘應用,快速上手責任鏈框架詳解 | 京東雲技術團隊

京東雲技術團隊發表於2023-05-08

作者:京東物流 覃玉傑

1. pie 簡介

責任鏈模式是開發過程中常用的一種設計模式,在SpringMVC、Netty等許多框架中均有實現。我們日常的開發中如果要使用責任鏈模式,通常需要自己來實現,但自己臨時實現的責任鏈既不通用,也很容易產生框架與業務程式碼耦合不清的問題,增加Code Review 的成本。

Netty的程式碼向來以優雅著稱,早年我在閱讀Netty的原始碼時,萌生出將其責任鏈的實現應用到業務開發中的想法,之後花了點時間將Netty中責任鏈的實現程式碼抽取出來,形成了本專案,也就是pie。pie的核心程式碼均來自Netty,絕大部分的 API 與 Netty 是一致的。

pie 是一個可快速上手的責任鏈框架,開發者只需要專注業務,開發相應的業務Handler,即可完成業務的責任鏈落地。

一分鐘學會、三分鐘上手、五分鐘應用,歡迎 star。

pie 原始碼地址:https://github.com/feiniaojin/pie.git

pie 案例工程原始碼地址:https://github.com/feiniaojin/pie-example.git

2. 快速入門

2.1 引入 maven 依賴

pie 目前已打包釋出到 maven 中央倉庫,開發者可以直接透過 maven 座標將其引入到專案中。

<dependency>
    <groupId>com.feiniaojin.ddd.ecosystem</groupId>
    <artifactId>pie</artifactId>
    <version>1.0</version>
</dependency>


目前最新的版本是 1.0

2.2 實現出參工廠

出參也就是執行結果,一般的執行過程都要求有執行結果返回。實現 OutboundFactory 介面,用於產生介面預設返回值。

例如:

public class OutFactoryImpl implements OutboundFactory {
    @Override
    public Object newInstance() {
        Result result = new Result();
        result.setCode(0);
        result.setMsg("ok");
        return result;
    }
}


2.3 實現 handler 介面完成業務邏輯

在 pie 案例工程( https://github.com/feiniaojin/pie-example.git )的 Example1 中,為了展示 pie 的使用方法,實現了一個虛擬的業務邏輯:CMS類專案修改文章標題、正文,大家不要關注修改操作放到兩個 handler 中是否合理,僅作為講解案例。

三個 Handler 功能如下:

CheckParameterHandler:用於引數校驗。

ArticleModifyTitleHandler:用於修改文章的標題。

ArticleModifyContentHandler:用於修改文章的正文。

CheckParameterHandler 的程式碼如下:

public class CheckParameterHandler implements ChannelHandler {

    private Logger logger = LoggerFactory.getLogger(CheckParameterHandler.class);

    @Override
    public void channelProcess(ChannelHandlerContext ctx,
                               Object in,
                               Object out) throws Exception {

        logger.info("引數校驗:開始執行");

        if (in instanceof ArticleTitleModifyCmd) {
            ArticleTitleModifyCmd cmd = (ArticleTitleModifyCmd) in;
            String articleId = cmd.getArticleId();
            Objects.requireNonNull(articleId, "articleId不能為空");
            String title = cmd.getTitle();
            Objects.requireNonNull(title, "title不能為空");
            String content = cmd.getContent();
            Objects.requireNonNull(content, "content不能為空");
        }
        logger.info("引數校驗:校驗透過,即將進入下一個Handler");
        ctx.fireChannelProcess(in, out);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause,
                                Object in,
                                Object out) throws Exception {
        logger.error("引數校驗:異常處理邏輯", cause);
        Result re = (Result) out;
        re.setCode(400);
        re.setMsg("引數異常");
    }
}


ArticleModifyTitleHandler 的程式碼如下:

public class ArticleModifyTitleHandler implements ChannelHandler {

    private Logger logger = LoggerFactory.getLogger(ArticleModifyTitleHandler.class);

    @Override
    public void channelProcess(ChannelHandlerContext ctx,
                               Object in,
                               Object out) throws Exception {

        logger.info("修改標題:進入修改標題的Handler");

        ArticleTitleModifyCmd cmd = (ArticleTitleModifyCmd) in;

        String title = cmd.getTitle();
        //修改標題的業務邏輯
        logger.info("修改標題:title={}", title);

        logger.info("修改標題:執行完成,即將進入下一個Handler");
        ctx.fireChannelProcess(in, out);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause,
                                Object in,
                                Object out) throws Exception {
        logger.error("修改標題:異常處理邏輯");
        Result re = (Result) out;
        re.setCode(1501);
        re.setMsg("修改標題發生異常");
    }
}


ArticleModifyContentHandler 的程式碼如下:

public class ArticleModifyContentHandler implements ChannelHandler {

    private Logger logger = LoggerFactory.getLogger(ArticleModifyContentHandler.class);

    @Override
    public void channelProcess(ChannelHandlerContext ctx,
                               Object in,
                               Object out) throws Exception {

        logger.info("修改正文:進入修改正文的Handler");
        ArticleTitleModifyCmd cmd = (ArticleTitleModifyCmd) in;
        logger.info("修改正文,content={}", cmd.getContent());
        logger.info("修改正文:執行完成,即將進入下一個Handler");
        ctx.fireChannelProcess(in, out);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause,
                                Object in,
                                Object out) throws Exception {

        logger.error("修改標題:異常處理邏輯");

        Result re = (Result) out;
        re.setCode(1502);
        re.setMsg("修改正文發生異常");
    }
}


2.4 透過 BootStrap 拼裝並執行

public class ArticleModifyExample1 {

    private final static Logger logger = LoggerFactory.getLogger(ArticleModifyExample1.class);

    public static void main(String[] args) {
        //構造入參
        ArticleTitleModifyCmd dto = new ArticleTitleModifyCmd();
        dto.setArticleId("articleId_001");
        dto.setTitle("articleId_001_title");
        dto.setContent("articleId_001_content");

        //建立引導類
        BootStrap bootStrap = new BootStrap();

        //拼裝並執行
        Result result = (Result) bootStrap
                .inboundParameter(dto)//入參
                .outboundFactory(new ResultFactory())//出參工廠
                .channel(new ArticleModifyChannel())//自定義channel
                .addChannelHandlerAtLast("checkParameter", new CheckParameterHandler())//第一個handler
                .addChannelHandlerAtLast("modifyTitle", new ArticleModifyTitleHandler())//第二個handler
                .addChannelHandlerAtLast("modifyContent", new ArticleModifyContentHandler())//第三個handler
                .process();//執行
        //result為執行結果
        logger.info("result:code={},msg={}", result.getCode(), result.getMsg());
    }
}


2.5 執行結果

以下是執行 ArticleModifyExample1 的 main 方法打出的日誌,可以看到我們定義的 handler 被逐個執行了。

3. 異常處理

3.1 Handler 異常處理

當某個Handler執行發生異常時,我們可將其異常處理邏輯實現在當前 Handler 的 exceptionCaught 方法中。

在 pie 案例工程( https://github.com/feiniaojin/pie-example.git )的 example2 包中,展示了某個 Handler 丟擲異常時的處理方式。

假設 ArticleModifyTitleHandler 的業務邏輯會丟擲異常,例項程式碼如下:

public class ArticleModifyTitleHandler implements ChannelHandler {

    private Logger logger = LoggerFactory.getLogger(ArticleModifyTitleHandler.class);

    @Override
    public void channelProcess(ChannelHandlerContext ctx,
                               Object in,
                               Object out) throws Exception {

        logger.info("修改標題:進入修改標題的Handler");
        ArticleTitleModifyCmd cmd = (ArticleTitleModifyCmd) in;
        String title = cmd.getTitle();
        //此處的異常用於模擬執行過程中出現異常的場景
        throw new RuntimeException("修改title發生異常");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause,
                                Object in,
                                Object out) throws Exception {
        logger.error("修改標題:異常處理邏輯");
        Result re = (Result) out;
        re.setCode(1501);
        re.setMsg("修改標題發生異常");
    }
}


此時 ArticleModifyTitleHandler 的 channelProcess 方法一定會丟擲異常, 在當前 Handler 的 exceptionCaught 方法中對異常進行了處理。

執行 ArticleModifyExample2 的 main 方法,輸出如下:

3.2 全域性異常處理

有時候,我們不想每個 handler 都處理一遍異常,我們希望在執行鏈的最後統一進行處理。
在 ArticleModifyExample3 中,我們展示了透過一個全域性異常進行最後的異常處理,其實現主要分為以下幾步:

3.2.1 業務 Handler 傳遞異常

如果業務 Handler 實現了 ChannelHandler 介面,那麼需要手工呼叫 ctx.fireExceptionCaught 方法向下傳遞異常。
例如 CheckParameterHandler 捕獲到異常時的示例如下:


@Override
public class XXXHandler implements ChannelHandler {

    //省略其他邏輯

    //異常處理
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause,
                                Object in,
                                Object out) throws Exception {

        logger.info("引數校驗的異常處理邏輯:不處理直接向後傳遞");
        ctx.fireExceptionCaught(cause, in, out);
    }
}


如果業務 Handler 繼承了 ChannelHandlerAdapter,如果沒有重寫 fireExceptionCaught 方法,則預設將異常向後傳遞。

3.2.2 實現全域性異常處理的 Handler

我們把業務異常處理邏輯放到最後的 Handler 中進行處理,該 Handler 繼承了ChannelHandlerAdapter,只需要重寫異常處理的exceptionCaught
方法。
示例程式碼如下:

public class ExceptionHandler extends ChannelHandlerAdapter {

    private Logger logger = LoggerFactory.getLogger(ExceptionHandler.class);

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause,
                                Object in,
                                Object out) throws Exception {

        logger.error("異常處理器中的異常處理邏輯");
        Result re = (Result) out;
        re.setCode(500);
        re.setMsg("系統異常");
    }
}


3.2.3 將 ExceptionHandler 加入到執行鏈中

直接透過 BootStrap 加入到執行鏈最後即可,示例程式碼如下:


public class ArticleModifyExample3 {

    private final static Logger logger = LoggerFactory.getLogger(ArticleModifyExample3.class);

    public static void main(String[] args) {
        //入參
        ArticleTitleModifyCmd dto = new ArticleTitleModifyCmd();
        dto.setArticleId("articleId_001");
        dto.setTitle("articleId_001_title");
        dto.setContent("articleId_001_content");
        //建立引導類
        BootStrap bootStrap = new BootStrap();

        Result result = (Result) bootStrap
                .inboundParameter(dto)//入參
                .outboundFactory(new ResultFactory())//出參工廠
                .channel(new ArticleModifyChannel())//自定義channel
                .addChannelHandlerAtLast("checkParameter", new CheckParameterHandler())//第一個handler
                .addChannelHandlerAtLast("modifyTitle", new ArticleModifyTitleHandler())//第二個handler
                .addChannelHandlerAtLast("modifyContent", new ArticleModifyContentHandler())//第三個handler
                .addChannelHandlerAtLast("exception", new ExceptionHandler())//異常處理handler
                .process();//執行
        //result為執行結果
        logger.info("result:code={},msg={}", result.getCode(), result.getMsg());
    }
}


3.2.4 執行 ArticleModifyExample3

執行 ArticleModifyExample3 的 main 方法,控制檯輸出如下,可以看到異常被傳遞到最後的 ExceptionHandler 中進行處理。

相關文章