分類
發燒車訊

Netty學習篇⑤–編、解碼源碼分析

前言

學習Netty也有一段時間了,Netty作為一個高性能的異步框架,很多RPC框架也運用到了Netty中的知識,在rpc框架中豐富的數據協議及編解碼可以讓使用者更加青睞;
Netty支持豐富的編解碼框架,其本身內部提供的編解碼也可以應對各種業務場景;
今天主要就是學習下Netty中提供的編、解碼類,之前只是簡單的使用了下Netty提供的解碼類,今天更加深入的研究下Netty中編、解碼的源碼及部分使用。

編、解碼的概念

  • 編碼(Encoder)

    編碼就是將我們發送的數據編碼成字節數組方便在網絡中進行傳輸,類似Java中的序列化,將對象序列化成字節傳輸
  • 解碼(Decoder)

    解碼和編碼相反,將傳輸過來的字節數組轉化為各種對象來進行展示等,類似Java中的反序列化
    如:
    // 將字節數組轉化為字符串
    new String(byte bytes[], Charset charset)

編、解碼超類

ByteToMessageDecoder: 解碼超類,將字節轉換成消息

解碼解碼一般用於將獲取到的消息解碼成系統可識別且自己需要的數據結構;因此ByteToMessageDecoder需要繼承ChannelInboundHandlerAdapter入站適配器來獲取到入站的數據,在handler使用之前通過channelRead獲取入站數據進行一波解碼;
ByteToMessageDecoder類圖

源碼分析

通過channelRead獲取入站數據,將數據緩存至cumulation數據緩衝區,最後在傳給decode進行解碼,在read完成之後清空緩存的數據

1. 獲取入站數據

/**
*  通過重寫channelRead方法來獲取入站數據
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 檢測是否是byteBuf對象格式數據
    if (msg instanceof ByteBuf) {
        // 實例化字節解碼成功輸出集合 即List<Object> out
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 獲取到的請求的數據
            ByteBuf data = (ByteBuf) msg;
            // 如果緩衝數據區為空則代表是首次觸發read方法
            first = cumulation == null;
            if (first) {
                // 如果是第一次read則當前msg數據為緩衝數據
                cumulation = data;
            } else {
                // 如果不是則觸發累加,將緩衝區的舊數據和新獲取到的數據通過        expandCumulation 方法累加在一起存入緩衝區cumulation
                // cumulator 累加類,將緩衝池中數據和新數據進行組合在一起
                // private Cumulator cumulator = MERGE_CUMULATOR;
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            // 將緩衝區數據cumulation進行解碼
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new DecoderException(t);
        } finally {
            // 在解碼完畢后釋放引用和清空全局字節緩衝區
            if (cumulation != null && !cumulation.isReadable()) {
                numReads = 0;
                cumulation.release();
                cumulation = null;
                // discardAfterReads為netty中設置的讀取多少次后開始丟棄字節 默認值16
                // 可通過setDiscardAfterReads(int n)來設置值不設置默認16次
            } else if (++ numReads >= discardAfterReads) {
                // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                // 在我們讀取了足夠的數據可以嘗試丟棄一些字節已保證不出現內存溢出的異常
                // 
                // See https://github.com/netty/netty/issues/4275
                // 讀取次數重置為0
                numReads = 0;
                // 重置讀寫指針或丟棄部分已讀取的字節
                discardSomeReadBytes();
            }
            // out為解碼成功的傳遞給下一個handler
            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            // 結束當前read傳遞到下個ChannelHandler
            fireChannelRead(ctx, out, size);
            // 回收響應集合 將insertSinceRecycled設置為false;
            // insertSinceRecycled用於channelReadComplete判斷使用
            out.recycle();
        }
    } else {
        // 不是的話直接fire傳遞給下一個handler
        ctx.fireChannelRead(msg);
    }
}
2. 初始化字節緩衝區計算器: Cumulator主要用於全局字節緩衝區和新讀取的字節緩衝區組合在一起擴容
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
    
    /**
    * alloc ChannelHandlerContext分配的字節緩衝區
    * cumulation 當前ByteToMessageDecoder類全局的字節緩衝區
    * in 入站的字節緩衝區
    **/
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        final ByteBuf buffer;
        // 如果全局ByteBuf寫入的字節+當前入站的字節數據大於全局緩衝區最大的容量或者全局緩衝區的引用數大於1個或全局緩衝區只讀
        if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
            || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
            // Expand cumulation (by replace it) when either there is not more room in the buffer
            // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
            // duplicate().retain() or if its read-only.
            //
            // See:
            // - https://github.com/netty/netty/issues/2327
            // - https://github.com/netty/netty/issues/1764
            // 進行擴展全局字節緩衝區(容量大小 = 新數據追加到舊數據末尾組成新的全局字節緩衝區)
            buffer = expandCumulation(alloc, cumulation, in.readableBytes());
        } else {
            buffer = cumulation;
        }
        // 將新數據寫入緩衝區
        buffer.writeBytes(in);
        // 釋放當前的字節緩衝區的引用
        in.release();
        
        return buffer;
    }
};


/**
* alloc 字節緩衝區操作類
* cumulation 全局累加字節緩衝區
* readable 讀取到的字節數長度
*/
// 字節緩衝區擴容方法
static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
    // 舊數據
    ByteBuf oldCumulation = cumulation;
    // 通過ByteBufAllocator將緩衝區擴大到oldCumulation + readable大小
    cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
    // 將舊數據重新寫入到新的字節緩衝區
    cumulation.writeBytes(oldCumulation);
    // 舊字節緩衝區引用-1
    oldCumulation.release();
    return cumulation;
}
3. ByteBuf釋放當前字節緩衝區的引用: 通過調用ReferenceCounted接口中的release方法來釋放
@Override
public boolean release() {
    return release0(1);
}

@Override
public boolean release(int decrement) {
    return release0(checkPositive(decrement, "decrement"));
}

/**
* decrement 減量
*/
private boolean release0(int decrement) {
    for (;;) {
        int refCnt = this.refCnt;
        // 當前引用小於減量
        if (refCnt < decrement) {
            throw new IllegalReferenceCountException(refCnt, -decrement);
        }
        // 這裏就利用里線程併發中的知識CAS,線程安全的設置refCnt的值
        if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
            // 如果減量和引用量相等
            if (refCnt == decrement) {
                // 全部釋放
                deallocate();
                return true;
            }
            return false;
        }
    }
}

4. 將全局字節緩衝區進行解碼

/**
* ctx ChannelHandler的上下文,用於傳輸數據與下一個handler來交互
* in 入站數據
* out 解析之後的出站集合 (此出站不是返回給客戶端的而是傳遞給下個handler的)
*/
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        // 如果入站數據還有沒解析的
        while (in.isReadable()) {
            // 解析成功的出站集合長度
            int outSize = out.size();
            // 如果大於0則說明解析成功的數據還沒被消費完,直接fire掉給通道中的後續handler繼續                消費
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();

                // Check if this handler was removed before continuing with decoding.
                // 在這個handler刪除之前檢查是否還在繼續解碼
                // If it was removed, it is not safe to continue to operate on the buffer.
                // 如果移除了,它繼續操作緩衝區是不安全的
                //
                // See:
                // - https://github.com/netty/netty/issues/4635
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }
            // 入站數據字節長度
            int oldInputLength = in.readableBytes();
            // 開始解碼數據
            decodeRemovalReentryProtection(ctx, in, out);

            // Check if this handler was removed before continuing the loop.
            // 
            // If it was removed, it is not safe to continue to operate on the buffer.
            //
            // See https://github.com/netty/netty/issues/1664
            if (ctx.isRemoved()) {
                break;
            }

            // 解析完畢跳出循環
            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }

            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                    StringUtil.simpleClassName(getClass()) +
                    ".decode() did not read anything but decoded a message.");
            }

            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 設置解碼狀態為正在解碼  STATE_INIT = 0; STATE_CALLING_CHILD_DECODE = 1;             STATE_HANDLER_REMOVED_PENDING = 2; 分別為初始化; 解碼; 解碼完畢移除
        decodeState = STATE_CALLING_CHILD_DECODE;
        try {
            // 具體的解碼邏輯(netty提供的解碼器或自定義解碼器中重寫的decode方法)
            decode(ctx, in, out);
        } finally {
            // 此時decodeState為正在解碼中 值為1,返回false
            boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
            // 在設置為初始化等待解碼
            decodeState = STATE_INIT;
            // 解碼完成移除當前ChannelHandler標記為不處理
            // 可以看看handlerRemoved源碼。如果緩衝區還有數據直接傳遞給下一個handler
            if (removePending) {
                handlerRemoved(ctx);
            }
        }
    }
5. 執行channelReadComplete
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    // 讀取次數重置
    numReads = 0;
    // 重置讀寫index
    discardSomeReadBytes();
    // 在channelRead meth中定義賦值 decodeWasNull = !out.insertSinceRecycled();
    // out指的是解碼集合List<Object> out; 咱們可以點進
    if (decodeWasNull) {
        decodeWasNull = false;
        if (!ctx.channel().config().isAutoRead()) {
            ctx.read();
        }
    }
    // fire掉readComplete傳遞到下一個handler的readComplete
    ctx.fireChannelReadComplete();
}

/**
*  然後我們可以搜索下insertSinceRecucled在什麼地方被賦值了
* Returns {@code true} if any elements where added or set. This will be reset once {@link #recycle()} was called.
*/
boolean insertSinceRecycled() {
    return insertSinceRecycled;
}


// 搜索下insert的調用我們可以看到是CodecOutputList類即為channelRead中的out集合,眾所周知在    decode完之後,解碼數據就會被調用add方法,此時insertSinceRecycled被設置為true
private void insert(int index, Object element) {
    array[index] = element;
    insertSinceRecycled = true;
}


/**
* 清空回收數組內部的所有元素和存儲空間
* Recycle the array which will clear it and null out all entries in the internal storage.
*/
// 搜索recycle的調用我么可以知道在channelRead的finally邏輯中 調用了out.recycle();此時        insertSinceRecycled被設置為false
void recycle() {
    for (int i = 0 ; i < size; i ++) {
        array[i] = null;
    }
    clear();
    insertSinceRecycled = false;
    handle.recycle(this);
}

至此ByteToMessageDecoder解碼類應該差不多比較清晰了!!!

MessageToByteEncoder: 編碼超類,將消息轉成字節進行編碼發出

何謂編碼,就是將發送數據轉化為客戶端和服務端約束好的數據結構和格式進行傳輸,我們可以在編碼過程中將消息體body的長度和一些頭部信息有序的設置到ByteBuf字節緩衝區中;方便解碼方靈活的運用來判斷(是否完整的包等)和處理業務;解碼是繼承入站數據,反之編碼應該繼承出站的數據;接下來我們看看編碼類是怎麼進行編碼的;
MessageToByteEncoder類圖如下

源碼分析

既然是繼承出站類,我們直接看看write方法是怎麼樣的

/**
* 通過write方法獲取到出站的數據即要發送出去的數據
* ctx channelHandler上下文
* msg 發送的數據 Object可以通過繼承類指定的泛型來指定
* promise channelPromise異步監聽,類似ChannelFuture,只不過promise可以設置監聽的結果,future只能通過獲取監聽的成功失敗結果;可以去了解下promise和future的區別
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        // 檢測發送數據的類型 通過TypeParameterMatcher類型匹配器
        if (acceptOutboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            // 分配字節緩衝區 preferDirect默認為true
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                // 進行編碼
                encode(ctx, cast, buf);
            } finally {
                // 完成編碼后釋放對象的引用
                ReferenceCountUtil.release(cast);
            }
            // 如果緩衝區有數據則通過ctx發送出去,promise可以監聽數據傳輸並設置是否完成
            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                // 如果沒有數據則釋放字節緩衝區的引用併發送一個empty的空包
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            // 非TypeParameterMatcher類型匹配器匹配的類型直接發送出去
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
}

// 初始化設置preferDirect為true
protected MessageToByteEncoder() {
    this(true);
}
protected MessageToByteEncoder(boolean preferDirect) {
    matcher = TypeParameterMatcher.find(this, MessageToByteEncoder.class, "I");
    this.preferDirect = preferDirect;
}

編碼: 重寫encode方法,根據實際業務來進行數據編碼

// 此處就是我們需要重寫的編碼方法了,我們和根據約束好的或者自己定義好想要的數據格式發送給對方

// 下面是我自己寫的demo的編碼方法;頭部設置好body的長度,服務端可以根據長度來判斷是否是完整的包,僅僅自學寫的簡單的demo非正常線上運營項目的邏輯
public class MyClientEncode extends MessageToByteEncoder<String> {

    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
        if (null != msg) {
            byte[] request = msg.getBytes(Charset.forName("UTF-8"));
            out.writeInt(request.length);
            out.writeBytes(request);
        }
    }
}

編碼類相對要簡單很多,因為只需要將發送的數據序列化,按照一定的格式進行發送數據!!!

項目實戰

項目主要簡單的實現下自定義編解碼器的運用及LengthFieldBasedFrameDecoder的使用

  • 項目結構如下
    │  hetangyuese-netty-06.iml
    │  pom.xml
    │
    ├─src
    │  ├─main
    │  │  ├─java
    │  │  │  └─com
    │  │  │      └─hetangyuese
    │  │  │          └─netty
    │  │  │              ├─client
    │  │  │              │      MyClient06.java
    │  │  │              │      MyClientChannelInitializer.java
    │  │  │              │      MyClientDecoder.java
    │  │  │              │      MyClientEncode.java
    │  │  │              │      MyClientHandler.java
    │  │  │              │      MyMessage.java
    │  │  │              │
    │  │  │              └─server
    │  │  │                      MyChannelInitializer.java
    │  │  │                      MyServer06.java
    │  │  │                      MyServerDecoder.java
    │  │  │                      MyServerDecoderLength.java
    │  │  │                      MyServerEncoder.java
    │  │  │                      MyServerHandler.java
    │  │  │
    │  │  └─resources
    │  └─test
    │      └─java
    
  • 服務端

    Serverhandler: 只是簡單的將解碼的內容輸出

    public class MyServerHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客戶端連接成功 time: " + new Date().toLocaleString());
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客戶端斷開連接 time: " + new Date().toLocaleString());
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("content:" + body);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 出現異常關閉通道
            cause.printStackTrace();
            ctx.close();
        }
    }

    解碼器

    public class MyServerDecoder extends ByteToMessageDecoder {
    
        // 此處我頭部只塞了長度字段佔4個字節,別問為啥我知道,這是要客戶端和服務端約束好的
        private static int min_head_length = 4;
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
            // 解碼的字節長度
            int size = in.readableBytes();
            if(size < min_head_length) {
                System.out.println("解析的數據長度小於頭部長度字段的長度");
                return ;
            }
            // 讀取的時候指針已經移位到長度字段的尾端
            int length = in.readInt();
            if (size < length) {
                System.out.println("解析的數據長度與長度不符合");
                return ;
            }
    
            // 上面已經讀取到了長度字段,後面的長度就是body
            ByteBuf decoderArr = in.readBytes(length);
            byte[] request = new byte[decoderArr.readableBytes()];
            // 將數據寫入空數組
            decoderArr.readBytes(request);
            String body = new String(request, Charset.forName("UTF-8"));
            out.add(body);
        }
    }

    將解碼器加入到channelHandler中:記得加到業務handler的前面否則無效

    public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
    //                .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0))
    //                .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0))
                    .addLast(new MyServerDecoder())
                    .addLast(new MyServerHandler())
            ;
        }
    }
  • 客戶端

    ClientHandler

    public class MyClientHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("與服務端連接成功");
            for (int i = 0; i<10; i++) {
                ctx.writeAndFlush("hhhhh" + i);
            }
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("與服務端斷開連接");
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("收到服務端消息:" +msg+ " time: " + new Date().toLocaleString());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }

    編碼器

    public class MyClientEncode extends MessageToByteEncoder<String> {
    
        @Override
        protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
            if (null != msg) {
                byte[] request = msg.getBytes(Charset.forName("UTF-8"));
                out.writeInt(request.length);
                out.writeBytes(request);
            }
        }
    }

    將編碼器加到ClientHandler的前面

    public class MyClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
                    .addLast(new MyClientDecoder())
                    .addLast(new MyClientEncode())
                    .addLast(new MyClientHandler())
            ;
    
        }
    }
  • 服務端運行結果
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 16:35:47
    content:hhhhh0
    content:hhhhh1
    content:hhhhh2
    content:hhhhh3
    content:hhhhh4
    content:hhhhh5
    content:hhhhh6
    content:hhhhh7
    content:hhhhh8
    content:hhhhh9
  • 如果不用自定義的解碼器怎麼獲取到body內容呢

    將自定義編碼器換成LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)

    public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
    //                .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0))
                    .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0))
    //                .addLast(new MyServerDecoder())
                    .addLast(new MyServerHandler())
            ;
        }
    }
    
    // 怕忘記的各個參數的含義在這在說明一次,自己不斷的修改每個值觀察結果就可以更加深刻的理解
    /**
    * maxFrameLength:消息體的最大長度,好像默認最大值為1024*1024
    * lengthFieldOffset 長度字段所在字節數組的下標 (我這是第一個write的所以下標是0)
    * lengthFieldLength 長度字段的字節長度(int類型佔4個字節)
    * lengthAdjustment 長度字段補償的數值 (lengthAdjustment =  數據包長度 - lengthFieldOffset - lengthFieldLength - 長度域的值),解析需要減去對應的數值
    * initialBytesToStrip 是否去掉長度字段(0不去除,對應長度域字節長度)
    */
    public LengthFieldBasedFrameDecoder(
                int maxFrameLength,
                int lengthFieldOffset, int lengthFieldLength,
                int lengthAdjustment, int initialBytesToStrip)
    結果: 前都帶上了長度
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh0, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh1, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh2, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh3, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh4, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh5, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh6, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh7, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh8, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh9, time: 2019-11-19 17:53:42

    如果我們在客戶端的長度域中做手腳 LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)

    舊: out.writeInt(request.length);
    新: out.writeInt(request.length + 1);
    // 看結果就不正常,0後面多了一個0;但是不知道為啥只解碼了一次??? 求解答
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 17:56:55
    收到客戶端發來的消息:   hhhhh0 , time: 2019-11-19 17:56:55
    
    // 正確修改為 LengthFieldBasedFrameDecoder(10240, 0, 4, -1, 0)
    // 結果:
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh0, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh1, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh2, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh3, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh4, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh5, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh6, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh7, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh8, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh9, time: 2019-11-19 18:02:18

    捨棄長度域 :LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 4)

    // 結果
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh0, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh1, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh2, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh3, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh4, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh5, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh6, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh7, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh8, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh9, time: 2019-11-19 18:03:44
    分析源碼示例中的 lengthAdjustment = 消息字節長度 – lengthFieldOffset-lengthFieldLength-長度域中的值
  • 源碼中的示例
     * <pre>
     * lengthFieldOffset   =  0
     * lengthFieldLength   =  2
     * <b>lengthAdjustment</b>    = <b>-2</b> (= the length of the Length field)
     * initialBytesToStrip =  0
     *
     * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
     * +--------+----------------+      +--------+----------------+
     * | Length | Actual Content |----->| Length | Actual Content |
     * | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
     * +--------+----------------+      +--------+----------------+
     * </pre>
    長度域中0x000E為16進制,轉換成10進制是14,說明消息體長度為14;根據公式:14-0-2-14 = -2
    * <pre>
     * lengthFieldOffset   = 0
     * lengthFieldLength   = 3
     * <b>lengthAdjustment</b>    = <b>2</b> (= the length of Header 1)
     * initialBytesToStrip = 0
     *
     * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
     * +----------+----------+----------------+      +----------+----------+----------------+
     * |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
     * | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
     * +----------+----------+----------------+      +----------+----------+----------------+
     * </pre>
    從上的例子可以知道;lengthAdjustment(2) = 17- 12(00000C)-lengthFieldOffset(0) - lengthFieldLength(3);

    …….等等

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

分類
發燒車訊

美國蒙大拿州密蘇拉(Missoula)聯邦地區法官克里斯坦森(Dana Christensen)與環保人士及美國原住民站在同一陣線

美國蒙大拿州密蘇拉(Missoula)聯邦地區法官克里斯坦森(Dana Christensen)與環保人士及美國原住民站在同一陣線,駁回美國魚類暨野生動物管理局(US Fish and Wildlife Service)將灰熊從瀕危物種名單除名的決定。

環保人士主張,根據瀕臨滅絕物種保護法,對這些灰熊與蒙大拿州和下48州(Lower 48)的其他灰熊族群採取差別待遇,是生物學上靠不住且非法行為,法官也同意這類說法。

環保人士說,儘管灰熊數量有所回升,倘若沒有受到聯邦持續保護,牠們的復育情況就會受到影響。此外,氣候變遷導致灰熊食物供給出現變化和人為死亡率高,也對灰熊生存構成威脅。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

分類
發燒車訊

米其林啟動循環經濟 2048年輪胎永續用料達80%、百分百回收

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

法國輪胎製造商米其林提出「Ambition 2048」計畫,要在2048年達到輪胎用料80%為永續材料,並且回收再利用率達到100%。



米其林新概念輪胎「VISION」。圖片來源:米其林Michelin

2018年全球報廢輪胎達10億個

世界永續發展工商理事會估計,2018年全世界將產生10億個報廢輪胎,約2500萬噸。今日全球輪胎再製率為70%,回收率為50%,多的20%轉化為能量。相較之下,塑膠包裝或容器每年回收率僅14%。

為實現「Ambition 2048」,米其林正在投資高科技回收技術,以期將永續材料比例拉高到80%。

米其林總部位於克萊蒙費朗,目前在全球有11萬1700名員工,遍佈170個國家,在17個國家擁有68個生產基地,2016年共生產1.87億個輪胎。

「Biobutterfly」計畫 啟動輪胎循環經濟

米其林計畫用它的新輪胎「VISION」建立循環經濟。這種新概念輪胎不需要充氣,將採用生物來源和回收材料製成,胎面可由生物分解,透過3D列印再製。



米其林新輪胎「VISION」。圖片來源:米其林Michelin

今日輪胎的成份包含超過200種原料。輪胎工業中使用的橡膠中有60%是用石油衍生碳氫化合物製成,剩下的40%仍然是天然橡膠。

米其林的「Ambition 2048」永續發展目標包括致力研究生物來源材料。2012年米其林與「Axens」石化公司和法國石油能源研究所(IFP Energies Nouvelles)共同啟動「Biobutterfly」計畫。

「Biobutterfly」計畫致力於透過生物材料製作合成橡膠,例如木材、禾稈(straw)和甜菜。

專利技術 回收輪胎轉化為永續材料

米其林也試著將更多可回收和可再生材料整合進輪胎中。2017年底米其林收購了總部位於美國喬治亞州的「Lehigh Technologies」化學公司,該公司的專利技術是把回收輪胎轉製成高科技微粉化橡膠粉末。



微粉化橡膠粉末。圖片來源:Lehigh Technologies

這些創新材料減少了輪胎生產所需的非再生原料數量,如合成橡膠或碳煙。

微粉化橡膠粉末是一種低成本的永續材料,可代替輪胎製程中使用的其他材料,以及塑膠、瀝青和建築材料。

世界許多輪胎大廠以及瀝青和建築材料專業公司已經是「Lehigh Technologies」公司的客戶。

藉著「Ambition 2048」,米其林估計將實現:

*每年省下3300萬桶石油,足以填滿16.5個超級油輪
*法國每個人一個月的總能量消耗
*每年省下一台一般轎車(8L / 100公里)跑650億公里的油耗

Sustainable Ambitions: Michelin Plans for 2048 CLERMONT-FERRAND, France, September 24, 2018 (ENS)

To the French tire manufacturer Michelin, Ambition 2048 means a whole new strategy of using sustainable materials in tire manufacturing and recycling. It means that in the year 2048 Michelin plans to manufacture its tires using 80 percent sustainable materials, and that 100 percent of those tires will be recycled.



圖片來源:米其林Michelin

Headquartered in Clermont-Ferrand, Michelin is present in 170 of the world’s 197 countries, has 111,700 employees and operates 68 production facilities in 17 countries, which collectively produced 187 million tires in 2016.

The World Business Council for Sustainable Development estimates that in 2018 there will be one billion end-of-life tires generated in the world – around 25 million tons.

Today the worldwide recovery rate for tires is 70 percent and the recycling rate is 50 percent. The remaining 20 percent are transformed into energy. By comparison, 14 percent of plastic packaging or containers are recovered each year.

To accomplish Ambition 2048, Michelin is investing in high technology recycling technologies that will enable the company to increase this content to 80 percent sustainable material.

Michelin plans to help create a circular economy with a new tire concept called VISION. This airless tire would be made of bio-sourced and recycled products with a biodegradable tread that is renewable with a 3D printer. 

Today, over 200 raw materials go into tire composition. Sixty percent of the rubber used in the tire industry is synthetic, produced from petroleum-derived hydrocarbons, although natural rubber is still necessary for the remaining 40 percent.

Michelin’s Ambition 2048 sustainable development goal includes a commitment to research into bio-sourced materials, such as Biobutterfly, a program launched in 2012 with Axens and IFP Energies Nouvelles.

Biobutterfly involves the creation of synthetic elastomers from biomass such as wood, straw or beet.

Michelin is integrating more recycled and renewable materials in its tires. This strategy motivated the acquisition in late 2017 of the American company Lehigh Technologies, based in Georgia, which specializes in high technology micronized rubber powders derived from recycled tires.

These innovative materials reduce the amount of non-renewable raw materials needed for tire production, such as elastomers or carbon black.

Micronized rubber powder is a low cost sustainable material that can substitute for other components used in the manufacture of tires, as well as plastics, asphalt and construction materials.

Major world tire manufacturers, as well as companies specialized in asphalt and construction materials are already Lehigh Technologies’ customers.

When Ambition 2048 is achieved, Michelin estimates that the potential savings will be equivalent to:

* – 33 million barrels of oil saved per year, enough to fill 16.5 supertankers
* – One month’s total energy consumption of everyone in France
* – 65 billion kilometers driven by an average sedan (8L / 100 km) per year

※ 全文及圖片詳見:

作者

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

延伸閱讀

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

分類
發燒車訊

特斯拉擴產,2018目標年產50萬輛車

因應龐大需求,電動車龍頭廠商特斯拉(Tesla)表示將積極擴產,目標在2018年達成年產量50萬輛車。

特斯拉2015年的產量約為5萬輛,但Model 3甫一推出就獲得40萬輛的超大筆訂單。為滿足這些預購車主的需求,特斯拉表示將積極擴產五倍,將原先2020年年產量50萬輛的目標提前到2018年實現,從中可望取得可觀的美國政府補助。而2016年,特斯拉預計將交車8~9萬輛。

特斯拉執行長Elon Musk於5月4日發表公司經營狀況時表示已將自己的辦公桌「移至生產線尾端,整個團隊都全力以赴」,彰顯急速擴張的決心。但他也坦言這個目標極具挑戰,若能成功,全球電動車市場將產生結構性的變化。

特斯拉財報:首季虧損

特斯拉在4日所發表的財報顯示,該公司今年第一季營收較去年同期大幅增加22%,為11.5億美元。但由於公司正在高速擴張,使營業費用比去年同期增加四成,至5億美元;加上Model X交車進度略有落後,使首季出現虧損,淨損為2.83億美元,高於去年第一季的淨損1.54億美元。扣除非常態性項目後,相當於每股虧損0.57美元。

未見起色的財報直接衝擊特斯拉股情。4日當天,特斯拉股價一度下滑4.2%,為222.56美元。不過,當Musk再宣布目標於2018年擴產至50萬輛後,股價再度回彈,最高漲至每股239.72美元。

特斯拉能否如期實現年產能50萬輛的目標,仍待觀察。近期有兩位高階副總即將離職,且特斯拉營運燒錢速度快於盈利回收,也是一隱憂。但特斯拉表示目前仍不需要依靠融資。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

分類
發燒車訊

清華大學與日產聯手研發電動車與自動駕駛技術

5月16日,“清華大學(汽車系)-日產智慧出行聯合研究中心”成立儀式在北京舉行,日產汽車攜手清華大學,針對中國市場的電動汽車和自動駕駛技術開展研發工作。

合作框架包括:電動汽車以及電池相關技術、自動駕駛和未來中國道路系統三個方面,雙方將共用資源,調研中國道路情況以及各地的駕駛習慣,助力發展中國智慧出行交通方式。與此同時,日產汽車與清華大學還將培養優秀本地人才。

據瞭解,日產汽車與清華大學有著多年的合作歷史,過去10年間,雙方在汽車核心技術開發,汽車發展戰略研究等領域一直保持密切合作,日產汽車將通過此次合作深入研究中國道路交通網絡,以最佳方式實現“日產智慧出行”。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

分類
發燒車訊

abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之一(二十七)

 



 

一.前言

       通過前面的文章的學習,我們已經有實現了傳統的ASP.NET Core MVC+EasyUI的增刪改查功能。本篇文章我們要實現了使用ABP提供的WebAPI方式+EasyUI來實現增刪改查的功能。本文中我們將不在使用DataGrid表格控件,而是使用樹形表格(TreeGrid)控件。

二、樹形表格(TreeGrid)介紹

       我先上圖,讓我們來看一下功能完成之後的組織管理信息列表頁面。如下圖。

       這個組織管理列表頁面使用TreeGrid來實現的。我們接下來介紹一下TreeGrid。

     首先、在定義TreeGrid時有兩個屬性必須要有一個是idField,這個要唯一;另一個是treeField的定義,這是樹節點的值,必須要有。 

     其次、easyui加載treegrid的json數據格式有三種,我就介紹我常用的這種。其他兩種方式,請查看easyui的相關文檔。

  {"total":7,"rows":[

           {"id":1,"name":"All Tasks","begin":"3/4/2010","end":"3/20/2010","progress":60,"iconCls":"icon-ok"},      
{"id":2,"name":"Designing","begin":"3/4/2010","end":"3/10/2010","progress":100,"_parentId":1,"state":"closed"},
{"id":21,"name":"Database","persons":2,"begin":"3/4/2010","end":"3/6/2010","progress":100,"_parentId":2},
{"id":22,"name":"UML","persons":1,"begin":"3/7/2010","end":"3/8/2010","progress":100,"_parentId":2}, {"id":23,"name":"Export Document","persons":1,"begin":"3/9/2010","end":"3/10/2010","progress":100,"_parentId":2},
{"id":3,"name":"Coding","persons":2,"begin":"3/11/2010","end":"3/18/2010","progress":80},
{"id":4,"name":"Testing","persons":1,"begin":"3/19/2010","end":"3/20/2010","progress":20} ],"footer":[ {"name":"Total Persons:","persons":7,"iconCls":"icon-sum"} ]}

     下面介紹一下上面數據中的幾個重要屬性:

     1)  _parentId :字段_parentId必不可少,且名稱唯一。記得前面有“_” ,他是用來記錄父級節點,沒有這個屬性,是沒法展示父級節點 其次就是這個父級節點必須存在,不然信息也是展示不出來,在後台遍歷組合的時候,如果父級節點不存在或為0時,此時 _parentId 應該不賦值,或設為“”。如果賦值 “0” 則在表格中不显示數據。

    2) state:是否展開

     3) checked:是否選中(用於複選框)

    4) iconCls:選項前面的圖標,如果自己不設定,父級節點默認為文件夾圖標,子級節點為文件圖標

 

    下面我們來開始實現組織管理頁面的相關功能。首先我們要創建一個組織信息實體。

三、創建Org實體

       1. 在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“ABP.TPLMS.Core”項目的“Entitys”文件夾,在彈出菜單中選擇“添加” >

 > “類”。 將類命名為 Org,然後選擇“添加”。

      2.創建Org類繼承自Entity<int>,通過實現審計模塊中的IHasCreationTime來實現保存創建時間。根據TreeGrid所需要的數據格式的要求。代碼如下:

using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
 

namespace ABP.TPLMS.Entitys
{

   public partial class Org : Entity<int>, IHasCreationTime
{
      int m_parentId = 0;
        public Org()
        {

            this.Id = 0;
            this.Name = string.Empty;
            this.HotKey = string.Empty;
            this.ParentId = 0;
            this.ParentName = string.Empty;

            this.IconName = string.Empty;

            this.Status = 0;

            this.Type = 0;

            this.BizCode = string.Empty;
            this.CustomCode = string.Empty;
            this.CreationTime = DateTime.Now;
            this.UpdateTime = DateTime.Now;
            this.CreateId = 0;

            this.SortNo = 0;

        }

        [Required]
        [StringLength(255)]
        public string Name { get; set; }

        [StringLength(255)]
        public string HotKey { get; set; }

        public int ParentId { get { return m_parentId; } set { m_parentId = value; } }

        [Required]
        [StringLength(255)]
        public string ParentName { get; set; }

        public bool IsLeaf { get; set; }

        public bool IsAutoExpand { get; set; }

        [StringLength(255)]
        public string IconName { get; set; } 

        public int Status { get; set; }

        public int Type { get; set; }

        [StringLength(255)]
        public string BizCode { get; set; }

        [StringLength(100)]
        public string CustomCode { get; set; }

        public DateTime CreationTime { get; set; }
        public DateTime UpdateTime { get; set; }
        public int CreateId { get; set; }

        public int SortNo { get; set; }
public int? _parentId {
            get {
                if (m_parentId == 0)                

                {
                    return null;
                }

                return m_parentId;
            }           

        }
    }
}

      3.定義好實體之後,我們去“ABP.TPLMS.EntityFrameworkCore”項目中的“TPLMSDbContext”類中定義實體對應的DbSet,以應用Code First 數據遷移。添加以下代碼

 

using Microsoft.EntityFrameworkCore;
using Abp.Zero.EntityFrameworkCore;
using ABP.TPLMS.Authorization.Roles;
using ABP.TPLMS.Authorization.Users;
using ABP.TPLMS.MultiTenancy;
using ABP.TPLMS.Entitys;
 

namespace ABP.TPLMS.EntityFrameworkCore
{

    public class TPLMSDbContext : AbpZeroDbContext<Tenant, Role, User, TPLMSDbContext>
    {

        /* Define a DbSet for each entity of the application */
    
        public TPLMSDbContext(DbContextOptions<TPLMSDbContext> options)
            : base(options)

        {
        }

        public DbSet<Module> Modules { get; set; }
        public DbSet<Supplier> Suppliers { get; set; }
  public DbSet<Cargo> Cargos { get; set; }
          public DbSet<Org> Orgs { get; set; }

    }
}

 

 

 

      4.從菜單中選擇“工具->NuGet包管理器器—>程序包管理器控制台”菜單。

     5. 在PMC中,默認項目選擇EntityframeworkCore對應的項目后。輸入以下命令:Add-Migration AddEntityOrg,創建遷移。如下圖。

 

       6. 在上面的命令執行完畢之後,創建成功后,會在Migrations文件夾下創建時間_AddEntityOrg格式的類文件,這些代碼是基於DbContext指定的模型。如下圖。

 

     7.在程序包管理器控制台,輸入Update-Database,回車執行遷移。執行成功后,如下圖。

 

     8. 在SQL Server Management Studio中查看數據庫,Orgs表創建成功。

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

分類
發燒車訊

Github PageHelper 原理解析

任何服務對數據庫的日常操作,都離不開增刪改查。如果一次查詢的紀錄很多,那我們必須採用分頁的方式。對於一個Springboot項目,訪問和查詢MySQL數據庫,持久化框架可以使用MyBatis,分頁工具可以使用github的 PageHelper。我們來看一下PageHelper的使用方法:

 1 // 組裝查詢條件
 2 ArticleVO articleVO = new ArticleVO();
 3 articleVO.setAuthor("劉慈欣");
 4 
 5 // 初始化返回類
 6 // ResponsePages類是這樣一種返回類,其中包括返回代碼code和返回消息msg
 7 // 還包括返回的數據和分頁信息
 8 // 其中,分頁信息就是 com.github.pagehelper.Page<?> 類型
 9 ResponsePages<List<ArticleVO>> responsePages = new ResponsePages<>();
10 
11 // 這裏為了簡單,寫死分頁參數。正確的做法是從查詢條件中獲取
12 // 假設需要獲取第1頁的數據,每頁20條記錄
13 // com.github.pagehelper.Page<?> 類的基本字段如下
14 // pageNum: 當前頁
15 // pageSize: 每頁條數
16 // total: 總記錄數
17 // pages: 總頁數
18 com.github.pagehelper.Page<?> page = PageHelper.startPage(1, 20);
19 
20 // 根據條件獲取文章列表
21 List<ArticleVO> articleList = articleMapper.getArticleListByCondition(articleVO);
22 
23 // 設置返回數據
24 responsePages.setData(articleList);
25 
26 // 設置分頁信息
27 responsePages.setPage(page);

  

如代碼所示,page 是組裝好的分頁參數,即每頁显示20條記錄,並且显示第1頁。然後我們執行mapper的獲取文章列表的方法,返回了結果。此時我們查看 responsePages 的內容,可以看到 articleList 中有20條記錄,page中包括當前頁,每頁條數,總記錄數,總頁數等信息。   使用方法就是這麼簡單,但是僅僅知道如何使用還不夠,還需要對原理有所了解。下面就來看看,PageHelper 實現分頁的原理。   我們先來看看 startPage 方法。進入此方法,發現一堆方法重載,最後進入真正的 startPage 方法,有5個參數,如下所示:

 1 /**
 2  * 開始分頁
 3  *
 4  * @param pageNum      頁碼
 5  * @param pageSize     每頁显示數量
 6  * @param count        是否進行count查詢
 7  * @param reasonable   分頁合理化,null時用默認配置
 8  * @param pageSizeZero true 且 pageSize=0 時返回全部結果,false時分頁, null時用默認配置
 9  */
10 public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
11     Page<E> page = new Page<E>(pageNum, pageSize, count);
12     page.setReasonable(reasonable);
13     page.setPageSizeZero(pageSizeZero);
14     // 當已經執行過orderBy的時候
15     Page<E> oldPage = SqlUtil.getLocalPage();
16     if (oldPage != null && oldPage.isOrderByOnly()) {
17         page.setOrderBy(oldPage.getOrderBy());
18     }
19     SqlUtil.setLocalPage(page);
20     return page;
21 }

  

getLocalPage 和 setLocalPage 方法做了什麼操作?我們進入基類 BaseSqlUtil 看一下:

 1 package com.github.pagehelper.util;
 2 ...
 3 
 4 public class BaseSqlUtil {
 5     // 省略其他代碼
 6 
 7     private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
 8     
 9     /**
10      * 從 ThreadLocal<Page> 中獲取 page
11      */ 
12     public static <T> Page<T> getLocalPage() {
13         return LOCAL_PAGE.get();
14     }
15     
16     /**
17      * 將 page 設置到 ThreadLocal<Page>
18      */
19     public static void setLocalPage(Page page) {
20         LOCAL_PAGE.set(page);
21     }
22 
23     // 省略其他代碼
24 }

 

原來是將 page 放入了 ThreadLocal<Page> 中。ThreadLocal 是每個線程獨有的變量,與其他線程不影響,是放置 page 的好地方。 setLocalPage 之後,一定有地方 getLocalPage,我們跟蹤進入代碼來看。   有了MyBatis動態代理的知識后,我們知道最終執行SQL的地方是 MapperMethod 的 execute 方法,作為回顧,我們來看一下:

 1 package org.apache.ibatis.binding;
 2 ...
 3 
 4 public class MapperMethod {
 5 
 6     public Object execute(SqlSession sqlSession, Object[] args) {
 7         Object result;
 8         if (SqlCommandType.INSERT == command.getType()) {
 9             // 省略
10         } else if (SqlCommandType.UPDATE == command.getType()) {
11             // 省略
12         } else if (SqlCommandType.DELETE == command.getType()) {
13             // 省略
14         } else if (SqlCommandType.SELECT == command.getType()) {
15             if (method.returnsVoid() && method.hasResultHandler()) {
16                 executeWithResultHandler(sqlSession, args);
17                 result = null;
18             } else if (method.returnsMany()) {
19                 /**
20                  * 獲取多條記錄
21                  */
22                 result = executeForMany(sqlSession, args);
23             } else if ...
24                 // 省略
25         } else if (SqlCommandType.FLUSH == command.getType()) {
26             // 省略
27         } else {
28             throw new BindingException("Unknown execution method for: " + command.getName());
29         }
30         ...
31         
32         return result;
33     }
34 }

  

由於執行的是select操作,並且需要查詢多條紀錄,所以我們進入 executeForMany 這個方法中,然後進入 selectList 方法,然後是 executor.query 方法。再然後突然進入到了 mybatis 的 Plugin 類的 invoke 方法,這是為什麼?   這裏就必須提到 mybatis 提供的 Interceptor 接口。
Intercept 機制讓我們可以將自己製作的分頁插件 intercept 到查詢語句執行的地方,這是MyBatis對外提供的標準接口。藉助於Java的動態代理,標準的攔截器可以攔截在指定的數據庫訪問流程中,執行攔截器自定義的邏輯,比如在執行SQL之前攔截,拼裝一個分頁的SQL並執行。   讓我們回到MyBatis初始化的時候,我們發現 MyBatis 為我們組裝了 sqlSessionFactory,所有的 sqlSession 都是生成自這個 Factory。在這篇文章中,我們將重點放在 interceptorChain 上。程序啟動時,MyBatis 或者是 mybatis-spring 會掃描代碼中所有實現了 interceptor 接口的插件,並將它們以【攔截器集合】的方式,存儲在 interceptorChain 中。如下所示:

# sqlSessionFactory 中的重要信息

sqlSessionFactory
    configuration
        environment        
        mapperRegistry
            config         
            knownMappers   
        mappedStatements   
        resultMaps         
        sqlFragments       
        interceptorChain   # MyBatis攔截器調用鏈
            interceptors   # 攔截器集合,記錄了所有實現了Interceptor接口,並且使用了invocation變量的類

  

如果MyBatis檢測到有攔截器,它就會在攔截器指定的執行點,首先執行 Plugin 的 invoke 方法,喚醒攔截器,然後執行攔截器定義的邏輯。因此,當 query 方法即將執行的時候,其實執行的是攔截器的邏輯。   MyBatis官網的說明: MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

  如果想了解更多攔截器的知識,可以看文末的參考資料。   我們回到主線,繼續看Plugin類的invoke方法:

 1 package org.apache.ibatis.plugin;
 2 ...
 3 
 4 public class Plugin implements InvocationHandler {
 5     ...
 6 
 7     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 8         try {
 9            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
10            if (methods != null && methods.contains(method)) {
11                // 執行攔截器的邏輯
12                return interceptor.intercept(new Invocation(target, method, args));
13            }
14            return method.invoke(target, args);
15        } catch (Exception e) {
16            throw ExceptionUtil.unwrapThrowable(e);
17        }
18    }
19    ...
20 }

 

我們去看 intercept 方法的實現,這裏我們進入【PageHelper】類來看:

 1 package com.github.pagehelper;
 2 ...
 3 
 4 /**
 5  * Mybatis - 通用分頁攔截器
 6  */
 7 @SuppressWarnings("rawtypes")
 8 @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
 9 public class PageHelper extends BasePageHelper implements Interceptor {
10     private final SqlUtil sqlUtil = new SqlUtil();
11 
12     @Override
13     public Object intercept(Invocation invocation) throws Throwable {
14         // 執行 sqlUtil 的攔截邏輯
15         return sqlUtil.intercept(invocation);
16     }
17 
18     @Override
19     public Object plugin(Object target) {
20         return Plugin.wrap(target, this);
21     }
22 
23     @Override
24     public void setProperties(Properties properties) {
25         sqlUtil.setProperties(properties);
26     }
27 }

 

可以看到最終調用了 SqlUtil 的intercept 方法,裏面的 doIntercept 方法是 PageHelper 原理中最重要的方法。跟進來看:

  1 package com.github.pagehelper.util;
  2 ...
  3 
  4 public class SqlUtil extends BaseSqlUtil implements Constant {
  5     ...
  6     
  7     /**
  8      * 真正的攔截器方法
  9      *
 10      * @param invocation
 11      * @return
 12      * @throws Throwable
 13      */
 14     public Object intercept(Invocation invocation) throws Throwable {
 15         try {
 16             return doIntercept(invocation);  // 執行攔截
 17         } finally {
 18             clearLocalPage();  // 清空 ThreadLocal<Page>
 19         }
 20     }
 21     
 22     /**
 23      * 真正的攔截器方法
 24      *
 25      * @param invocation
 26      * @return
 27      * @throws Throwable
 28      */
 29     public Object doIntercept(Invocation invocation) throws Throwable {
 30         // 省略其他代碼
 31         
 32         // 調用方法判斷是否需要進行分頁
 33         if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {
 34             ResultHandler resultHandler = (ResultHandler) args[3];
 35             // 當前的目標對象
 36             Executor executor = (Executor) invocation.getTarget();
 37             
 38             /**
 39              * getBoundSql 方法執行后,boundSql 中保存的是沒有 limit 的sql語句
 40              */
 41             BoundSql boundSql = ms.getBoundSql(parameterObject);
 42             
 43             // 反射獲取動態參數
 44             Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
 45             // 判斷是否需要進行 count 查詢,默認需要
 46             if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {
 47                 // 省略代碼
 48                 
 49                 // 執行 count 查詢
 50                 Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
 51                 Long count = (Long) ((List) countResultList).get(0);
 52                 
 53                 // 處理查詢總數,從 ThreadLocal<Page> 中取出 page 並設置 total
 54                 runtimeDialect.afterCount(count, parameterObject, rowBounds);
 55                 if (count == 0L) {
 56                     // 當查詢總數為 0 時,直接返回空的結果
 57                     return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);
 58                 }
 59             }
 60             // 判斷是否需要進行分頁查詢
 61             if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {
 62                 /**
 63                  * 生成分頁的緩存 key
 64                  * pageKey變量是分頁參數存放的地方
 65                  */
 66                 CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
 67                 /**
 68                  * 處理參數對象,會從 ThreadLocal<Page> 中將分頁參數取出來,放入 pageKey 中
 69                  * 主要邏輯就是這樣,代碼就不再單獨貼出來了,有興趣的同學可以跟進驗證
 70                  */
 71                 parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);
 72                 /**
 73                  * 調用方言獲取分頁 sql
 74                  * 該方法執行后,pageSql中保存的sql語句,被加上了 limit 語句
 75                  */
 76                 String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
 77                 BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
 78                 //設置動態參數
 79                 for (String key : additionalParameters.keySet()) {
 80                     pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
 81                 }
 82                 /**
 83                  * 執行分頁查詢
 84                  */
 85                 resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
 86             } else {
 87                 resultList = new ArrayList();
 88             }
 89         } else {
 90             args[2] = RowBounds.DEFAULT;
 91             // 不需要分頁查詢,執行原方法,不走代理
 92             resultList = (List) invocation.proceed();
 93         }
 94         /**
 95          * 主要邏輯:
 96          * 從 ThreadLocal<Page> 中取出 page
 97          * 將 resultList 塞進 page,並返回
 98          */
 99         return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);
100     }
101     ...
102 }

 

Count 查詢語句 countBoundSql 被執行了,分頁查詢語句 pageBoundSql 也被執行了。然後從 ThreadLocal<Page> 中將page 取出來,設置記錄總數,每頁條數等信息,同時也將查詢到的記錄塞進page,最後返回。再之後就是mybatis的常規後續操作了。

 

知識拓展

我們來看看 PageHelper 支持哪些數據庫的分頁操作:

  1. Oracle
  2. Mysql
  3. MariaDB
  4. SQLite
  5. Hsqldb
  6. PostgreSQL
  7. DB2
  8. SqlServer(2005,2008)
  9. Informix
  10. H2
  11. SqlServer2012
  12. Derby
  13. Phoenix

  原來 PageHelper 支持這麼多數據庫,那麼持久化工具mybatis為什麼不一口氣把分頁也做了呢? 其實mybatis也有自帶的分頁方法:RowBounds。
RowBounds簡單地來說包括 offset 和 limit。實現原理是將所有符合條件的記錄獲取出來,然後丟棄 offset 之前的數據,只獲取 limit 條數據。這種做法效率低下,個人猜想mybatis只想把數據庫連接和SQL執行這方面做精做強,至於如分頁之類的細節,本身提供Intercept接口,讓第三方實現該接口來完成分頁。PageHelper 就是這樣的第三方分頁插件。甚至你可以實現該接口,製作你自己的業務邏輯,攔截到任何MyBatis允許你攔截的地方。  

總結

PageHelper 的分頁原理,最核心的部分是實現了 MyBatis 的 Interceptor 接口,從而將分頁參數攔截在執行sql之前,拼裝出分頁sql到數據庫中執行。 初始化的時候,因為 PageHelper 的 SqlUtil 中實例化了 intercept 方法,因此MyBatis 將它視作一個攔截器,記錄在 interceptorChain 中。 執行的時候,PageHelper首先將 page 需求記錄在 ThreadLocal<Page> 中,然後在攔截的時候,從 ThreadLocal<Page> 中取出 page,拼裝出分頁sql,然後執行。 同時將結果分頁信息(包括當前頁,每頁條數,總頁數,總記錄數等)設置回page,讓業務代碼可以獲取。  

參考資料

  • PageHelper淺析:
  • MyBatis攔截器:
  • ThreadLocal理解:

 

創作時間:2019-11-20 21:21

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

分類
發燒車訊

工信部第285批新車公示218款新能源入選

根據《中華人民共和國行政許可法》和《國務院對確需保留的行政審批專案設定行政許可的決定》的規定,工信部日前將許可的汽車、摩托車、三輪汽車和低速貨車生產企業及產 品(第285批)予以了公告,共有218款新能源車型入選。進入該公告的新能源汽車可開展生產銷售,但是要獲得補貼,還需再獲得《新能源汽車推廣應用推薦車型目錄》准入。

純電動轎車/乘用車方面,北汽、長城、禦捷馬、卡威、吉利等12款車型入選。

插電式乘用車方面,比亞迪、寶馬、之諾、上汽等7款車型入選。

純電動客車方面,安凱、江淮、安源、北奔、北方、福田、比亞迪、白雲、五菱、陸地方舟、尼歐凱、友誼、青年、海格、開沃、依維柯、飛燕、大通、象牌、野馬、華新、金龍、金旅、宇通、黃河、中通、中植汽車、穗通等28個品牌75款車型入選。

插電式混動客車方面, 安凱、海格、易聖達、金龍、金旅、宇通6個品牌21款車型入選。

新能源專用車方面,福田、北京、大運、黃海、東風、華神、揚子江、東風、福建、環球、藍速、田野、中悅 、卡威、陸地方舟、青年曼、康迪、五菱、暢達、躍進、金龍、凱馬、時風、太行成功、東風、金杯、邢牛、海德、解放、神州、宇通、長帆汽車、迪馬、炫虎等33個品牌103款車型入選。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

分類
發燒車訊

財政部將公開新能源汽車推廣騙補核查和處理結果

5月28日,財政部發佈聲明稱,關於新能源汽車推廣騙補核查,現場核查已經完成,目前處於會審階段。核查及處理情況,將按資訊公開有關規定及時公開。

中國新能源汽車產業的發展受到政策的強力推動。從2010年開始,我國便實施新能源汽車補貼政策,由於監督機制不完善,騙補隨之愈演愈烈。2016年1月份,工信部、財政部、科技部、發改委聯合啟動對新能源汽車相關情況的專項核查工作,新能源汽車生產企業、運營企業、租賃企業、企事業單位等新能源汽車使用者全部列入核查物件。國務院也把遏制新能源汽車騙補行為作為重點工作之一。

此前,央視曝光了10家涉及騙補的企業,分別是蘇州吉姆西客車製造有限公司、陝西通家汽車股份有限公司、重慶力帆乘用車有限公司、江蘇陸地方舟新能源電動汽車有限公司、奇瑞萬達貴州客車股份有限公司、國宏汽車有限公司、江蘇奧新新能源汽車有限公司、蕪湖寶騏汽車製造有限公司、重慶力帆汽車有限公司,以及金華青年汽車製造有限公司。這些企業的共同特點是,2015年12月單月產量(主要依據是機動車出廠合格證)均超過全年產量的50%。

對於騙補行為,工信部部長苗圩曾公開表示,“局部地區確實存在少部分企業騙補的現象,對於騙補企業,沒補貼的錢不會下發,已補貼的錢一定要扣回。依法進行處置,直至取消這些企業的資質”。

對於騙補的企業到底有哪些?將受到怎樣的處罰?這些問題一直受到業界的關注和猜測。此次,財政部公開發佈新能源汽車推廣核查有關情況的聲明,表示“現場核查已經完成,目前處於會審階段”。聲明特別強調“財政部和部內有關司局至今未接受過媒體採訪,核查及處理情況,將按資訊公開有關規定及時公開”。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

分類
發燒車訊

Model 3將不會向使用者提供免費充電服務

據外媒報導,Model 3實惠的價格引發了消費者的熱烈預訂,不過特斯拉CEO艾隆•馬斯克表示,Model 3將不會向使用者提供免費充電服務,消費者需要購買額外的服務包。

目前為止,特斯拉對於所有購買了電動車的使用者均提供免費充電服務。這也是得益於他們在美國、澳大利亞中國等地建設了大量的充電站和充電樁。電力消耗成本低廉,特斯拉已經在把充電服務作為電動車的一個增值服務。

但是隨著特斯拉用戶的增加,充電站以及充電樁的數量卻不能無限的增長,這也給特斯拉帶來了極大的壓力。馬斯克在回答提問時表示,除非使用者購買額外充電服務,否則Model 3使用者將不會獲得免費長途充電服務。不過馬斯克並未詳細介紹充電服務包的內容和價格。

馬斯克認為,免費充電服務也是有一定的成本的,因此把Model 3本身的成本和充電的成本分開是非常有必要的。Model 3的付費充電服務會非常便宜,遠遠低於加油的費用。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表