ReplayingDecoder 解碼器:別以為我有多厲害,也只不過是使用了一下裝飾器模式而已~

不送花的程式猿發表於2020-08-23

原文地址

一、設計模式為啥老是用不好?

想要寫出更屌的程式碼,提高程式碼的健壯性和可擴充套件性,那麼設計模式可謂是必學的技能。

關於學習設計模式,大家可能都覺得設計模式的概念太過於抽象,理解起來有點費勁;又或者看的時候是理解了,但是寫起程式碼時,卻毫無頭緒,壓根不知道可以套用哪個設計模式。

對,可以看到我使用了 “套” 這個字眼,正是因為我們無法深入理解設計模式的設計理念和使用場景,所以我們往往是想讓我們的程式碼套用設計模式,而不理會業務場景是否合適。

關於設計模式的學習,我不會推薦任何書,因為我自己也沒看過,哈哈哈。我看過的是龍哥的設計模式系列文章,裡面的文章不但會介紹設計模式的概念,也會用非常有趣的場景去講解設計模式的設計理念,下面先分享一波連結:龍哥設計模式全集

對於我自己而言,關於設計模式的使用,除非是非常深刻的理解了,又或者某種設計模式的使用場景非常的清晰明確(例如建立型設計模式中的單例模式、結構型設計模式中的組合模式、行為型設計模式中的策略模式等等),不然我也不知道該如何使用,和什麼時候使用。

二、在閱讀開源框架原始碼中學習設計模式!

想學習設計模式的使用方式,何不研究一下各大優秀的開源框架的原始碼。

想更深層次的理解設計模式,往往閱讀優秀的框架和中介軟體的原始碼是非常好的方式。優秀的開源框架和中介軟體,裡面都使用了大量的設計模式,使得框架的實用性、可擴充套件性和效能非常的高。

很巧,今天在工作的空餘時間中,我繼續閱讀一本關於併發的書,並看到關於 Netty 的內建解碼器,其中最常用的有 ReplayingDecoder,它是 ByteToMessageDecoder 的子類,作用是: 在讀取ByteBuf緩衝區的資料之前,需要檢查緩衝區是否有足夠的位元組;若ByteBuf中有足夠的位元組,則會正常讀取;反之,如果沒有足夠的位元組,則會停止解碼。

它是如何做到自主控制解碼的時機的呢?其實底層是使用了 ReplayingDecoderByteBuf 這個繼承於 ByteBuf 的實現類。而它使用了裝飾器設計模式。

1、在 Netty 中如何自定義實現整數解碼器?

1.1、ByteToMessageDecoder:

我們需要自定義類需要繼承 ByteToMessageDecoder 抽象類,然後重寫 decode 方法即可。

看程式碼:

/**
 * @author Howinfun
 * @desc
 * @date 2020/8/21
 */
public class MyIntegerDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        while (byteBuf.readableBytes() >= 4){
            
            int num = byteBuf.readInt();
            System.out.println("解碼出一個整數:"+num);
            list.add(num);
        }
    }
}

我們可以看到非常的簡單,就是不斷地判斷緩衝區裡的的可讀位元組數是否大於等於4(Java 中整數的大小);如果是的話就讀取4個位元組大小的內容,然後放到結果集裡面。

1.2、ReplayingDecoder:

我們需要自定義類需要繼承 ReplayingDecoder 類,然後重寫 decode 方法即可。

看程式碼:

/**
 * @author Howinfun
 * @desc
 * @date 2020/8/21
 */
public class MyIntegerDecoder2 extends ReplayingDecoder {

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {

        int num = byteBuf.readInt();
        System.out.println("解碼出一個整數:"+num);
        list.add(num);
    }
}

這個實現更加簡單,那就是去掉判斷,直接呼叫 ByteBuf 的 readInt() 方法去獲取整數即可。

1.3、測試用例:

1.3.1、自定義業務處理器:

先建立一個業務處理器 IntegerProcessHandler,用於處理上面的自定義解碼器解碼之後的 Java Integer 整數。其功能是:讀取上一站的入站資料,把它轉換成整數,並且輸出到Console控制檯上。

碼如下:

/**
 * @author Howinfun
 * @desc
 * @date 2020/8/21
 */
public class IntegerProcessorHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Integer integer = (Integer) msg;
        System.out.println("列印出一個整數:"+integer);
    }
}

這個業務處理器非常的簡單,直接繼承 ChannelInBoundHandlerAdapter,然戶重寫 channelRead() 方法即可。

1.3.2、利用 EmbeddedChannel 進行測試:

為了測試入站處理器,需要確保通道能接收到 ByteBuf 入站資料。這裡呼叫 writeInbound 方法,模擬入站資料的寫入,向嵌入式通道 EmbeddedChannel 寫入100次 ByteBuf 入站緩衝;每一次寫入僅僅包含一個整數。

EmbeddedChannel 的 writeInbound 方法模擬入站資料,會被流水線上的兩個入站處理器所接收和處理。接著,這些入站的二進位制位元組被解碼成一個一個的整數,然後逐個地輸出到控制檯上。

看程式碼:

public class Test{
    public static void main(String[] args){
      ChannelInitializer i = new ChannelInitializer<EmbeddedChannel>() {
          @Override
          protected void initChannel(EmbeddedChannel channel) throws Exception {
              // 繼承 ByteToMessageDecoder 抽象類的自定義解碼器
              // channel.pipeline().addLast(new MyIntegerDecoder()).addLast(new IntegerProcessorHandler());
              // 繼承 ReplayingDecoder 類的自定義解碼器
              channel.pipeline().addLast(new MyIntegerDecoder2()).addLast(new IntegerProcessorHandler());
          }
      };
        EmbeddedChannel channel = new EmbeddedChannel(i);
        for (int j = 0;j < 20;j++){
            ByteBuf byteBuf = Unpooled.buffer();
            byteBuf.writeInt(j);
            channel.writeInbound(byteBuf);
        }
        ThreadUtil.sleep(Integer.MAX_VALUE);
    }
}

通過測試,兩個自定義 Decoder 都是沒問題的。而他們的最大不同點在於:繼承抽象類 ByteToMessageDecoder 的解碼器需要判斷可讀位元組數是否大於等於4,大於等於才可以讀取一個整數出來;而繼承 ReplayingDecoder 的解碼器直接呼叫 readInt() 方法即可。

2、解讀 ReplayingDecoder 的原理

其實其中的原理非常的簡單,我們可以直接從 ReplayingDecoder 的原始碼入手:

2.1、ReplayingDecoder的建構函式:

首先是建構函式,此處我們用了無參建構函式:

protected ReplayingDecoder() {
    this((Object)null);
}

protected ReplayingDecoder(S initialState) {
    this.replayable = new ReplayingDecoderByteBuf();
    this.checkpoint = -1;
    this.state = initialState;
}

我們可以看到,主要是初始化了 ReplayingDecoderByteBuf(其實就是加了點料的 ByteBuf)、checkpoint(讀指標下標) 和 state。我們這篇文章不需要理會 state 屬性,這個屬性是稍微高階一點的用法。
我們最需要關注的是 ReplayingDecoderByteBuf 這個類。

2.2、繼續探討 ReplayingDecoderByteBuf:

那麼接下來看看 ReplayingDecoderByteBuf 的原始碼。

2.2.1、ReplayingDecoderByteBuf 的屬性:

final class ReplayingDecoderByteBuf extends ByteBuf {
    private static final Signal REPLAY;
    private ByteBuf buffer;
    private boolean terminated;
    private SwappedByteBuf swapped;
    static final ReplayingDecoderByteBuf EMPTY_BUFFER;

    ReplayingDecoderByteBuf() {
    }
    //...
}

我們可以看到,它繼承了 ByteBuf 抽象類,並且裡面包含一個 ByteBuf 型別的 buffer 屬性,剩餘的其他屬性暫時不需要看懂。

2.2.2、瞧一瞧 readInt() 方法:

那麼接下來,我們就是直接看 ReplayingDecoderByteBuf 的 readInt() 方法了,因為我們知道,在上面的自定義解碼器 MyIntegerDecoder2 的 decode() 方法中,只需要直接呼叫 ByteBuf(也就是 ReplayingDecoderByteBuf) 的 readInt() 方法即可解碼一個整數。

public int readInt() {
    this.checkReadableBytes(4);
    return this.buffer.readInt();
}

readInt() 方法非常簡單,首先是呼叫 checkReadableBytes() 方法,並且傳入 4。根據方法名,我們就可以猜到,先判斷緩衝區中是否有4個可讀位元組;如果是的話,就呼叫 buffer 的 readInt() 方法,讀取一個整數。

2.2.3、繼續看看 checkReadableBytes() 方法:

程式碼如下:

private void checkReadableBytes(int readableBytes) {
    if (this.buffer.readableBytes() < readableBytes) {
        throw REPLAY;
    }
}

方法非常簡單,其實和我們上面的 MyIntegerDecoder 一樣,就是判斷緩衝區中是否有 4個位元組的可讀資料,如果不是的話,則丟擲異常。

2.2.4、Signal 異常:

而我們最需要關注的就是這個異常,這個異常是 ReplayingDecoder 的靜態成員變數。它是繼承了 error 的異常類,是 netty 提供配合 ReplayingDecoder 一起使用的。

至於如何使用,我們可以看到 ReplayingDecoder 的 callDecode() 方法:

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    // 呼叫 ReplayingDecoderByteBuf 的 setCumulation() 方法,使用 ReplayingDecoderByteBuf 裝飾 ByteBuf
    this.replayable.setCumulation(in);

    try {
        while(in.isReadable()) {
            int oldReaderIndex = this.checkpoint = in.readerIndex();
            int outSize = out.size();
            if (outSize > 0) {
                // 將結果集流到下一個 InBoundChannel
                fireChannelRead(ctx, out, outSize);
                out.clear();
                if (ctx.isRemoved()) {
                    break;
                }

                outSize = 0;
            }

            S oldState = this.state;
            int oldInputLength = in.readableBytes();

            try {
                // 呼叫自定義解碼器的 decode() 方法進行解碼
                this.decodeRemovalReentryProtection(ctx, this.replayable, out);
                if (ctx.isRemoved()) {
                    break;
                }

                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes() && oldState == this.state) {
                        throw new DecoderException(StringUtil.simpleClassName(this.getClass()) + ".decode() must consume the inbound data or change its state if it did not decode anything.");
                    }
                    continue;
                }
            } catch (Signal var10) {
                // 如果不是 Sinal 異常,則往外拋
                var10.expect(REPLAY);
                if (!ctx.isRemoved()) {
                    // 設定讀指標為原來的位置
                    int checkpoint = this.checkpoint;
                    if (checkpoint >= 0) {
                        in.readerIndex(checkpoint);
                    }
                }
                break;
            }

            // ......
        }

    } catch (DecoderException var11) {
        throw var11;
    } catch (Exception var12) {
        throw new DecoderException(var12);
    }
}

到這裡,我們可以捋一下思路:

  1. 當緩衝區資料流到繼承 ReplayingDecoder 的解碼器時,會先判斷結果集是否有資料,如果有則流入到下一個 InBoundChannel;
  2. 接著會呼叫自定義解碼器的 decode() 方法,而這裡就是是直接呼叫 ByteBuf 的 readInt() 方法,即 ReplayingDecoderByteBuf 的 readInt() 方法;裡面會先判斷可讀位元組大小是否大於 4,如果大於則讀取,否則丟擲 Signal 這個 Error 型別的異常。
  3. 如果 ReplayingDecoder 捕捉 Signal 這個異常,會先判斷 checkpoint(即讀指標下標不) 是否為零,如果不是則重新設定讀指標下標,然後跳出讀迴圈。

ReplayingDecoder 能做到自主控制解碼的時機,是因為使用 ReplayingDecoderByteBuf 對 ByteBuf 進行修飾,在呼叫 ByteBuf 的方法前,會先呼叫自己的判斷邏輯,這也就是我們常說的裝飾器模式。

三、裝飾器模式的特點

首先,被裝飾的類和裝飾類都是繼承同一個類(抽象類)或實現同一個介面。

接著,被裝飾類會作為裝飾類的成員變數。

最後,在執行被裝飾類的方法前後,可能會呼叫裝飾類的方法。

場景總結:

裝飾器模式常用於這麼一個場景:在不修改類的狀態(屬性或行為)下,對類的功能進行擴充套件!

當然啦,這是我自己個人的總結,大家可去閱讀專業的書籍來證實這是否正確。如果有更好的總結,可以留言給我,讓我也學習學習~

相關文章