Netty核心功能之?dāng)?shù)據(jù)容器ByteBuf詳解
正文
網(wǎng)絡(luò)數(shù)據(jù)的基本單位總是字節(jié),Java NIO 提供了 ByteBuffer 作為它的字節(jié)容器,但是這個(gè)類(lèi)使用起來(lái)過(guò)于復(fù)雜,而且也有些繁瑣。 Netty 的 ByteBuffer 替代品是 ByteBuf,一個(gè)強(qiáng)大的實(shí)現(xiàn),既解決了 JDK API 的局限性,又為網(wǎng)絡(luò)應(yīng)用程序的開(kāi)發(fā)者提供了更好的 API。
1、簡(jiǎn)介
Netty 的數(shù)據(jù)處理 API 通過(guò)兩個(gè)組件暴露——abstract class ByteBuf
和 interface ByteBufHolder
,下面是一些 ByteBuf API 的優(yōu)點(diǎn):
- 它可以被用戶(hù)自定義的緩沖區(qū)類(lèi)型擴(kuò)展;
- 通過(guò)內(nèi)置的復(fù)合緩沖區(qū)類(lèi)型實(shí)現(xiàn)了透明的零拷貝;
- 容量可以按需增長(zhǎng)(類(lèi)似于 JDK 的 StringBuilder);
- 在讀和寫(xiě)這兩種模式之間切換不需要調(diào)用 ByteBuffer 的 flip()方法;
- 讀和寫(xiě)使用了不同的索引;
- 支持方法的鏈?zhǔn)秸{(diào)用;
- 支持引用計(jì)數(shù);
- 支持池化。
對(duì)比ByteBuffer的缺點(diǎn):
ByteBuffer
長(zhǎng)度固定,一旦分配完成,它的容量不能動(dòng)態(tài)擴(kuò)展和收縮,當(dāng)需要編碼的POJO對(duì)象大于ByteBuffer
的容量時(shí),會(huì)發(fā)生索引越界異常;ByteBuffer
只有一個(gè)標(biāo)識(shí)位置的指針position
,讀寫(xiě)的時(shí)候需要手工調(diào)用flip()
和rewind()
等,使用者必須小心謹(jǐn)慎地處理這些API,否則很容易導(dǎo)致程序處理失??;ByteBuffer
的API功能有限,一些高級(jí)和實(shí)用的特性它不支持,需要使用者自己編程實(shí)現(xiàn)。
2、ByteBuf 類(lèi)——Netty 的數(shù)據(jù)容器
所有的網(wǎng)絡(luò)通信都涉及字節(jié)序列的移動(dòng),所以高效易用的數(shù)據(jù)結(jié)構(gòu)明顯是必不可少的。所以理解Netty 的 ByteBuf 是如何滿(mǎn)足這些需求的很重要。
2.1 工作原理
ByteBuf
工作機(jī)制:ByteBuf
維護(hù)了兩個(gè)不同的索引,一個(gè)用于讀取,一個(gè)用于寫(xiě)入。readerIndex
和writerIndex
的初始值都是0,當(dāng)從ByteBuf
中讀取數(shù)據(jù)時(shí),它的readerIndex
將會(huì)被遞增(它不會(huì)超過(guò)writerIndex
),當(dāng)向ByteBuf
寫(xiě)入數(shù)據(jù)時(shí),它的writerIndex
會(huì)遞增。
ByteBuf
的幾個(gè)特點(diǎn):
- 名稱(chēng)以
readXXX
或者writeXXX
開(kāi)頭的ByteBuf
方法,會(huì)推進(jìn)對(duì)應(yīng)的索引,而以setXXX
或getXXX
開(kāi)頭的操作不會(huì)。 - 在讀取之后,
0~readerIndex
的就被視為discard
的,調(diào)用discardReadBytes
方法,可以釋放這部分空間,它的作用類(lèi)似ByteBuffer
的compact()
方法。 readerIndex
和writerIndex
之間的數(shù)據(jù)是可讀取的,等價(jià)于ByteBuffer
的position
和limit
之間的數(shù)據(jù)。writerIndex
和capacity
之間的空間是可寫(xiě)的,等價(jià)于ByteBuffer
的limit
和capacity
之間的可用空間。
2.2 ByteBuf的三種類(lèi)型
堆緩沖區(qū)
最常用的 ByteBuf 模式是將數(shù)據(jù)存儲(chǔ)在 JVM 的堆空間中。這種模式被稱(chēng)為支撐數(shù)組(backing array),它能在沒(méi)有使用池化的情況下提供快速的分配和釋放
優(yōu)點(diǎn)
:由于數(shù)據(jù)存儲(chǔ)在JVM的堆中可以快速創(chuàng)建和快速釋放,并且提供了數(shù)組的直接快速訪(fǎng)問(wèn)的方法。
缺點(diǎn)
:每次讀寫(xiě)數(shù)據(jù)都要先將數(shù)據(jù)拷貝到直接緩沖區(qū)(相關(guān)閱讀:Java NIO 直接緩沖區(qū)和非直接緩沖區(qū)對(duì)比)再進(jìn)行傳遞。
示例:
// 創(chuàng)建一個(gè)堆緩沖區(qū) ByteBuf buffer = Unpooled.buffer(10); String s = "waylau"; buffer.writeBytes(s.getBytes()); // 檢查是否是支撐數(shù)組 if (buffer.hasArray()) { // 獲取支撐數(shù)組的引用 byte[] array = buffer.array(); // 計(jì)算第一個(gè)字節(jié)的偏移量 int offset = buffer.readerIndex() + buffer.arrayOffset(); // 可讀字節(jié)數(shù) int length = buffer.readableBytes(); // 使用數(shù)組、偏移量和長(zhǎng)度作為參數(shù)調(diào)用自定義的使用方法 printBuffer(array, offset, length); } /** * 打印出Buffer的信息 * * @param buffer */ private static void printBuffer(byte[] array, int offset, int len) { System.out.println("array:" + array); System.out.println("array->String:" + new String(array)); System.out.println("offset:" + offset); System.out.println("len:" + len); } /** 輸出結(jié)果: array:[B@5b37e0d2 array->String:waylau offset:0 len:6 */
直接緩沖區(qū)
Direct Buffer在堆之外直接分配內(nèi)存,直接緩沖區(qū)不會(huì)占用堆的容量。
優(yōu)點(diǎn)
:在使用Socket傳遞數(shù)據(jù)時(shí)性能很好,由于數(shù)據(jù)直接在內(nèi)存中,不存在從JVM拷貝數(shù)據(jù)到直接緩沖區(qū)的過(guò)程,性能好。
缺點(diǎn)
:因?yàn)镈irect Buffer是直接在內(nèi)存中,所以分配內(nèi)存空間和釋放內(nèi)存比堆緩沖區(qū)更復(fù)雜和慢。
示例:
// 創(chuàng)建一個(gè)直接緩沖區(qū) ByteBuf buffer = Unpooled.directBuffer(10); String s = "waylau"; buffer.writeBytes(s.getBytes()); // 檢查是否是支撐數(shù)組. // 不是支撐數(shù)組,則為直接緩沖區(qū) if (!buffer.hasArray()) { // 計(jì)算第一個(gè)字節(jié)的偏移量 int offset = buffer.readerIndex(); // 可讀字節(jié)數(shù) int length = buffer.readableBytes(); // 獲取字節(jié)內(nèi)容 byte[] array = new byte[length]; buffer.getBytes(offset, array); // 使用數(shù)組、偏移量和長(zhǎng)度作為參數(shù)調(diào)用自定義的使用方法 printBuffer(array, offset, length); } /** * 打印出Buffer的信息 * * @param buffer */ private static void printBuffer(byte[] array, int offset, int len) { System.out.println("array:" + array); System.out.println("array->String:" + new String(array)); System.out.println("offset:" + offset); System.out.println("len:" + len); } /** 輸出結(jié)果: array:[B@6d5380c2 array->String:waylau offset:0 len:6 */
復(fù)合緩沖區(qū)
復(fù)合緩沖區(qū)是 Netty 特有的緩沖區(qū)。本質(zhì)上類(lèi)似于提供一個(gè)或多個(gè) ByteBuf
的組合視圖,可以根據(jù)需要添加和刪除不同類(lèi)型的 ByteBuf
。
優(yōu)點(diǎn)
:提供了一種訪(fǎng)問(wèn)方式讓使用者自由地組合多個(gè)ByteBuf
,避免了復(fù)制和分配新的緩沖區(qū)。
缺點(diǎn)
:不支持訪(fǎng)問(wèn)其支撐數(shù)組。因此如果要訪(fǎng)問(wèn),需要先將內(nèi)容復(fù)制到堆內(nèi)存中,再進(jìn)行訪(fǎng)問(wèn)。
示例:
// 創(chuàng)建一個(gè)堆緩沖區(qū) ByteBuf heapBuf = Unpooled.buffer(3); String way = "way"; heapBuf.writeBytes(way.getBytes()); // 創(chuàng)建一個(gè)直接緩沖區(qū) ByteBuf directBuf = Unpooled.directBuffer(3); String lau = "lau"; directBuf.writeBytes(lau.getBytes()); // 創(chuàng)建一個(gè)復(fù)合緩沖區(qū) CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer(10); compositeBuffer.addComponents(heapBuf, directBuf); // 將緩沖區(qū)添加到符合緩沖區(qū) // 檢查是否是支撐數(shù)組. // 不是支撐數(shù)組,則為復(fù)合緩沖區(qū) if (!compositeBuffer.hasArray()) { for (ByteBuf buffer : compositeBuffer) { // 計(jì)算第一個(gè)字節(jié)的偏移量 int offset = buffer.readerIndex(); // 可讀字節(jié)數(shù) int length = buffer.readableBytes(); // 獲取字節(jié)內(nèi)容 byte[] array = new byte[length]; buffer.getBytes(offset, array); // 使用數(shù)組、偏移量和長(zhǎng)度作為參數(shù)調(diào)用自定義的使用方法 printBuffer(array, offset, length); } } /** * 打印出Buffer的信息 * * @param buffer */ private static void printBuffer(byte[] array, int offset, int len) { System.out.println("array:" + array); System.out.println("array->String:" + new String(array)); System.out.println("offset:" + offset); System.out.println("len:" + len); } /** 輸出結(jié)果: array:[B@4d76f3f8 array->String:way offset:0 len:3 array:[B@2d8e6db6 array->String:lau offset:0 len:3 */
3、字節(jié)級(jí)操作
ByteBuf 提供了許多超出基本讀、寫(xiě)操作的方法用于修改它的數(shù)據(jù)。
3.1 隨機(jī)訪(fǎng)問(wèn)索引和順序訪(fǎng)問(wèn)索引
如同在普通的 Java 字節(jié)數(shù)組中一樣,ByteBuf 的索引是從零開(kāi)始的:第一個(gè)字節(jié)的索引是 0,最后一個(gè)字節(jié)的索引總是 數(shù)組容量 - 1。在ByteBuf的實(shí)現(xiàn)類(lèi)中都有一個(gè)方法可以快速獲得容量值,那就是capacity()。有了這個(gè)capcity()方法,我們就能很簡(jiǎn)單的實(shí)現(xiàn)隨機(jī)訪(fǎng)問(wèn)索引:
for (int i = 0;i<buf.capacity();i++){ char b = (char)buf.getByte(i);//通過(guò) getBytes 系列接口來(lái)對(duì)ByteBuf進(jìn)行隨機(jī)訪(fǎng)問(wèn)。 System.out.println(b); }
Tips: 用getBytes隨機(jī)訪(fǎng)問(wèn)不會(huì)改變r(jià)eaderIndex
通過(guò) readerIndex() 和 writerIndex()
獲取讀Index和寫(xiě)Index。
3.2 可丟棄字節(jié)
在上圖中標(biāo)記為可丟棄字節(jié)的分段包含了已經(jīng)被讀過(guò)的字節(jié)。通過(guò)調(diào)用 discardReadBytes
()方法,可以丟棄它們并回收空間。這個(gè)分段的初始大小為 0,存儲(chǔ)在 readerIndex
中, 會(huì)隨著 read 操作的執(zhí)行而增加(get操作不會(huì)移動(dòng) readerIndex
)。
下圖展示了在上圖的緩沖區(qū)上調(diào)用discardReadBytes
()方法后的結(jié)果,需要注意的是丟棄并不是字節(jié)把已經(jīng)讀的字段的字節(jié)不要了,而是把尚未讀的字節(jié)數(shù)移到最開(kāi)始。(這樣做對(duì)可寫(xiě)分段的內(nèi)容并沒(méi)有任何的保證,因?yàn)橹皇且苿?dòng)了可以讀取的字節(jié)以及 writerIndex,而沒(méi)有對(duì)所有可寫(xiě)入的字節(jié)進(jìn)行擦除寫(xiě))
3.3 可讀字節(jié)
ByteBuf
的可讀字節(jié)分段存儲(chǔ)了實(shí)際數(shù)據(jù)。新分配的、包裝的或者復(fù)制的緩沖區(qū)的默認(rèn)的 readerIndex
值為 0。任何名稱(chēng)以 read
或者 skip
開(kāi)頭的操作都將檢索或者跳過(guò)位于當(dāng)前 readerIndex
之前的數(shù)據(jù),并且在readerIndex
的基礎(chǔ)上增加已讀字節(jié)數(shù)。 如果被調(diào)用的方法需要一個(gè) ByteBuf
參數(shù)作為寫(xiě)入的目標(biāo),并且沒(méi)有指定目標(biāo)索引參數(shù), 那么該目標(biāo)緩沖區(qū)的 writerIndex
也將被增加。
3.4 可寫(xiě)字節(jié)
可寫(xiě)字節(jié)分段是指一個(gè)擁有未定義內(nèi)容的、寫(xiě)入就緒的內(nèi)存區(qū)域。新分配的緩沖區(qū)的 writerIndex 的默認(rèn)值為 0。任何名稱(chēng)以 write
開(kāi)頭的操作都將從當(dāng)前的 writerIndex
處 開(kāi)始寫(xiě)數(shù)據(jù),并且在writerIndex
的基礎(chǔ)上增加已寫(xiě)字節(jié)數(shù)。如果寫(xiě)操作的目標(biāo)也是 ByteBuf
,并且沒(méi)有指定 源索引的值,則源緩沖區(qū)的 readerIndex
也同樣會(huì)被增加相同的大小。
3.5 索引管理
JDK 的 InputStream
定義了 mark(int readlimit)
和 reset()
方法,這些方法分別 被用來(lái)將流中的當(dāng)前位置標(biāo)記為指定的值,以及將流重置到該位置。
同樣,可以通過(guò)調(diào)用 markReaderIndex()
、markWriterIndex()
、resetWriterIndex()
和 resetReaderIndex()
來(lái)標(biāo)記和重置 ByteBuf
的 readerIndex
和 writerIndex
。這些和 InputStream
上的調(diào)用類(lèi)似,只是沒(méi)有 readlimit
參數(shù)來(lái)指定標(biāo)記什么時(shí)候失效
也可以通過(guò)調(diào)用 readerIndex(int)
或者 writerIndex(int)
來(lái)將索引移動(dòng)到指定位置。試 圖將任何一個(gè)索引設(shè)置到一個(gè)無(wú)效的位置都將導(dǎo)致一個(gè) IndexOutOfBoundsException
。
可以通過(guò)調(diào)用 clear()
方法來(lái)將 readerIndex
和 writerIndex
都設(shè)置為 0。注意,這 并不會(huì)清除內(nèi)存中的內(nèi)容。調(diào)用后的存儲(chǔ)結(jié)構(gòu)如下圖所示:
調(diào)用 clear()比調(diào)用 discardReadBytes()輕量得多,因?yàn)樗鼘⒅皇侵刂盟饕粫?huì)復(fù) 制任何的內(nèi)存
3.6 派生緩沖區(qū)
派生緩沖區(qū)為 ByteBuf
提供了以專(zhuān)門(mén)的方式來(lái)呈現(xiàn)其內(nèi)容的視圖。這類(lèi)視圖是通過(guò)以下方法被創(chuàng)建的:
- duplicate();
- slice();
- slice(int, int);
- Unpooled.unmodifiableBuffer(…);
- order(ByteOrder);
- readSlice(int)。
每個(gè)這些方法都將返回一個(gè)新的 ByteBuf 實(shí)例,它具有自己的讀索引、寫(xiě)索引和標(biāo)記 索引。其內(nèi)部存儲(chǔ)和 JDK 的 ByteBuffer 一樣也是共享的。這使得派生緩沖區(qū)的創(chuàng)建成本是很低廉的,但是這也意味著,如果你修改了它的內(nèi)容,也同時(shí)修改了其對(duì)應(yīng)的源實(shí)例,所以要小心。
ByteBuf 復(fù)制 如果需要一個(gè)現(xiàn)有緩沖區(qū)的真實(shí)副本,請(qǐng)使用 copy()或者 copy(int, int)方 法。不同于派生緩沖區(qū),由這個(gè)調(diào)用所返回的 ByteBuf 擁有獨(dú)立的數(shù)據(jù)副本。
3.7 讀/寫(xiě)操作
有兩種類(lèi)別的讀/寫(xiě)操作:
- get()和 set()操作,從給定的索引開(kāi)始,并且保持索引不變;
- read()和 write()操作,從給定的索引開(kāi)始,并且會(huì)根據(jù)已經(jīng)訪(fǎng)問(wèn)過(guò)的字節(jié)數(shù)對(duì)索引進(jìn)行調(diào)整。
常用的 get()方法如下圖:
常用的 set()方法如下圖:
常用的 read()方法如下圖:
常用的 write()方法如下圖:
4、ByteBufHolder 接口
4.1 按需分配:ByteBufAllocator 接口
Netty 中內(nèi)存分配有一個(gè)最頂層的抽象就是ByteBufAllocator,負(fù)責(zé)分配所有ByteBuf 類(lèi)型的內(nèi)存。他的一些操作如下:
可以通過(guò) Channel
(每個(gè)都可以有一個(gè)不同的 ByteBufAllocator
實(shí)例)或者綁定到 ChannelHandler
的 ChannelHandlerContext
獲取一個(gè)到 ByteBufAllocator
的引用,如下圖所示:
Netty提供了兩種ByteBufAllocator
的實(shí)現(xiàn):PooledByteBufAllocator
和UnpooledByteBufAllocator
。前者池化了ByteBuf的實(shí)例以提高性能并最大限度地減少內(nèi)存碎片,這是通過(guò)一種 叫做jemalloc
的方法來(lái)分配內(nèi)存的(閱讀資料:jemalloc剖析)。后者的實(shí)現(xiàn)不池化ByteBuf
實(shí)例,并且在每次它被調(diào)用時(shí)都會(huì)返回一個(gè)新的實(shí)例。
4.2 Unpooled 緩沖區(qū)
可能某些情況下,你未能獲取一個(gè)到 ByteBufAllocator 的引用。對(duì)于這種情況,Netty 提 供了一個(gè)簡(jiǎn)單的稱(chēng)為 Unpooled 的工具類(lèi),它提供了靜態(tài)的輔助方法來(lái)創(chuàng)建未池化的 ByteBuf 實(shí)例。他的一些操作如下:
4.3 ByteBufUtil 類(lèi)
ByteBufUtil
提供了用于操作 ByteBuf
的靜態(tài)的輔助方法。這個(gè) API 是通用的,并且和池化無(wú)關(guān)。
5、引用計(jì)數(shù)
引用計(jì)數(shù)是一種通過(guò)在某個(gè)對(duì)象所持有的資源不再被其他對(duì)象引用時(shí)釋放該對(duì)象所持有的資源來(lái)優(yōu)化內(nèi)存使用和性能的技術(shù)。Netty
在第 4 版中為 ByteBuf
和 ByteBufHolder
引入了 引用計(jì)數(shù)技術(shù),它們都實(shí)現(xiàn)了 interface ReferenceCounted
。
5.1 基本原理
一個(gè)新創(chuàng)建的引用計(jì)數(shù)對(duì)象的初始引用計(jì)數(shù)是1。
ByteBuf buf = ctx.alloc().directbuffer(); assert buf.refCnt() == 1;
當(dāng)你釋放掉引用計(jì)數(shù)對(duì)象,它的引用次數(shù)減1.如果一個(gè)對(duì)象的引用計(jì)數(shù)到達(dá)0,該對(duì)象就會(huì)被釋放或者歸還到創(chuàng)建它的對(duì)象池。
assert buf.refCnt() == 1; // release() returns true only if the reference count becomes 0. boolean destroyed = buf.release(); assert destroyed; assert buf.refCnt() == 0;
訪(fǎng)問(wèn)引用計(jì)數(shù)為0的引用計(jì)數(shù)對(duì)象會(huì)觸發(fā)一次IllegalReferenceCountException。
assert buf.refCnt() == 0; try { buf.writeLong(0xdeadbeef); throw new Error("should not reach here"); } catch (IllegalReferenceCountExeception e) { // Expected }
只要引用計(jì)數(shù)對(duì)象未被銷(xiāo)毀,就可以通過(guò)調(diào)用retain()方法來(lái)增加引用次數(shù)。
ByteBuf buf = ctx.alloc().directBuffer(); assert buf.refCnt() == 1; buf.retain(); assert buf.refCnt() == 2; boolean destroyed = buf.release(); assert !destroyed; assert buf.refCnt() == 1;
5.2 誰(shuí)來(lái)銷(xiāo)毀
一般的原則是,最后訪(fǎng)問(wèn)引用計(jì)數(shù)對(duì)象的部分負(fù)責(zé)對(duì)象的銷(xiāo)毀。更具體地來(lái)說(shuō):
- 如果一個(gè)[發(fā)送]組件要傳遞一個(gè)引用計(jì)數(shù)對(duì)象到另一個(gè)[接收]組件,發(fā)送組件通常不需要 負(fù)責(zé)去銷(xiāo)毀對(duì)象,而是將這個(gè)銷(xiāo)毀的任務(wù)推延到接收組件
- 如果一個(gè)組件消費(fèi)了一個(gè)引用計(jì)數(shù)對(duì)象,并且不知道誰(shuí)會(huì)再訪(fǎng)問(wèn)它(例如,不會(huì)再將引用 發(fā)送到另一個(gè)組件),那么,這個(gè)組件負(fù)責(zé)銷(xiāo)毀工作。
5.3 內(nèi)存泄漏問(wèn)題
引用計(jì)數(shù)的缺點(diǎn)是,引用計(jì)數(shù)對(duì)象容易發(fā)生泄露。因?yàn)镴VM并不知道Netty的引用計(jì)數(shù)實(shí)現(xiàn),當(dāng)引用計(jì)數(shù)對(duì)象不 可達(dá)時(shí),JVM就會(huì)將它們GC掉,即時(shí)此時(shí)它們的引用計(jì)數(shù)并不為0。一旦對(duì)象被GC就不能再訪(fǎng)問(wèn),也就不能歸還到緩沖池,所以會(huì)導(dǎo)致內(nèi)存泄露。 慶幸的是,盡管發(fā)現(xiàn)內(nèi)存泄露很難,但是Netty會(huì)對(duì)分配的緩沖區(qū)的1%進(jìn)行采樣,來(lái)檢查你的應(yīng)用中是否存在內(nèi)存泄露。
內(nèi)存泄露檢查等級(jí)
總共有4個(gè)內(nèi)存泄露檢查等級(jí):
- DISABLED – 完全禁用檢查。不推薦。
- SIMPLE – 檢查1%的緩沖區(qū)是否存在內(nèi)存泄露。默認(rèn)。
- ADVANCED – 檢查1%的緩沖區(qū),并提示發(fā)生內(nèi)存泄露的位置
- PARANOID – 與ADVANCED等級(jí)一樣,不同的是會(huì)檢查所有的緩沖區(qū)。對(duì)于自動(dòng)化測(cè)試很有用,你可以讓構(gòu)建測(cè)試失敗 如果構(gòu)建輸出中包含’LEAK’ 用JVM選項(xiàng)
-Dio.netty.leakDetectionLevel
來(lái)指定內(nèi)存泄露檢查等級(jí)
避免泄露最佳實(shí)踐
- 指定SIMPLE和PARANOI等級(jí),運(yùn)行單元測(cè)試和集成測(cè)試
- 在將你的應(yīng)用部署到整個(gè)集群前,盡可能地用足夠長(zhǎng)的時(shí)間,使用SIMPLE級(jí)別去調(diào)試你的程序,來(lái)看是否存在內(nèi)存泄露
- 如果存在內(nèi)存泄露,使用ADVANCED級(jí)別去調(diào)試程序,去獲取內(nèi)存泄漏的位置信息
- 不要將存在內(nèi)存泄漏的應(yīng)用部署到整個(gè)集群
以上就是Netty核心功能之?dāng)?shù)據(jù)容器ByteBuf詳解的詳細(xì)內(nèi)容,更多關(guān)于Netty 數(shù)據(jù)容器ByteBuf的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot2.0+elasticsearch5.5+rabbitmq搭建搜索服務(wù)的坑
這篇文章主要介紹了springboot2.0+elasticsearch5.5+rabbitmq搭建搜索服務(wù)的坑,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06java程序員自己的圖片轉(zhuǎn)文字OCR識(shí)圖工具分享
這篇文章主要介紹了java程序員自己的圖片轉(zhuǎn)文字OCR識(shí)圖工具,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11Springboot mybatisplus如何解決分頁(yè)組件IPage失效問(wèn)題
這篇文章主要介紹了Springboot mybatisplus如何解決分頁(yè)組件IPage失效問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08Java使用組合模式實(shí)現(xiàn)表示公司組織結(jié)構(gòu)功能示例
這篇文章主要介紹了Java使用組合模式實(shí)現(xiàn)表示公司組織結(jié)構(gòu)功能,簡(jiǎn)單描述了組合模式的概念、功能并結(jié)合實(shí)例形式分析了Java使用組合模式實(shí)現(xiàn)公司組織結(jié)構(gòu)表示功能具體操作步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2018-05-05springboot配置nacos的實(shí)現(xiàn)示例
本文將介紹如何在Spring?Boot中配置Nacos,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-09-09spring-security關(guān)閉登錄框的實(shí)現(xiàn)示例
這篇文章主要介紹了spring-security關(guān)閉登錄框的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05java_IO向文件中寫(xiě)入和讀取內(nèi)容代碼實(shí)例
這篇文章主要介紹了java_IO向文件中寫(xiě)入和讀取內(nèi)容,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03