通過Java帶你了解網(wǎng)絡(luò)IO模型
1.BIO
1.1 簡述
BIO是同步阻塞IO,所有連接都是同步執(zhí)行的,在上一個連接未處理完的時候是無法接收下一個連接
1.2 代碼示例
在上述代碼中,如果啟動一個客戶端起連接服務(wù)端時如果沒有發(fā)送數(shù)據(jù),那么下一個連接將永遠無法進來
public static void main(String[] args) { try { // 監(jiān)聽端口 ServerSocket serverSocket = new ServerSocket(8080); // 等待客戶端的連接過來,如果沒有連接過來,就會阻塞 while (true) { // 阻塞IO中一個線程只能處理一個連接 Socket socket = serverSocket.accept(); System.out.println("客戶端建立連接:"+socket.getPort()); String line = null; try { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); line = bufferedReader.readLine(); System.out.println("客戶端的數(shù)據(jù):" + line); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bufferedWriter.write("ok\n"); bufferedWriter.flush(); } catch (IOException e) { e.printStackTrace(); } } } catch (IOException e) { e.printStackTrace(); } }
1.3優(yōu)點和缺點
優(yōu)點:
簡單易用,代碼實現(xiàn)比較簡單。
對于低并發(fā)量的場景,因為每個連接都有獨占的線程處理IO操作,因此可以保證每個連接的IO操作都能夠及時得到處理。
對于數(shù)據(jù)量較小的IO操作,同步阻塞IO模型的性能表現(xiàn)較好。
缺點:
由于每一個客戶端連接都需要開啟一個線程,因此無法承載高并發(fā)的場景。
線程切換的開銷比較大,會導(dǎo)致系統(tǒng)性能下降。
對于IO操作較慢的情況下,會占用大量的線程資源,導(dǎo)致系統(tǒng)負載過高。
對于處理大量連接的服務(wù)器,BIO模型的性能較低,無法滿足需求。
1.4 思考
問:既然每個連接進來都會阻塞,那么是否可以使用多線程的方式接收處理?
答:當然可以,但是這樣如果有1w個連接那么就要啟動1w個線程去處理嗎,線程是非常寶貴的資源,頻繁使用線程對系統(tǒng)的開銷是非常大的
2. NoBlockingIO
2.1 簡述
NoBlockingIO是同步非阻塞IO,相對比阻塞IO,他在接收數(shù)據(jù)的時候是非阻塞的,會一直輪詢去問內(nèi)核是否準備好數(shù)據(jù),直到有數(shù)據(jù)返回
ps: NoBlockingIO并不是真正意義上的NIO
2.2 代碼示例
在下述代碼中,將BIO中的ServerSocket修改為ServerSocketChannel,然后configureBlocking為false則為非阻塞,從而數(shù)據(jù)都是在channel的buffer(緩沖區(qū))中獲取,不理解沒關(guān)系,就當作是設(shè)置非阻塞IO的方式就好
此時在accept中是非阻塞的,不斷的等待客戶端進來
注意
- accept是非阻塞,不斷輪詢,如果為空則跳過,不為空則添加連接
- 讀數(shù)據(jù)是非阻塞,不斷的輪詢連接,等待客戶端寫入數(shù)據(jù)
public static List<SocketChannel> channelList = new ArrayList<>(); public static void main(String[] args) { try { // 相當于serverSocket // 1.支持非阻塞 2.數(shù)據(jù)總是寫入buffer,讀取也是從buffer中去讀 3.可以同時讀寫 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 設(shè)置非阻塞 serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8080)); while (true){ // 這里將不再阻塞 SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ socketChannel.configureBlocking(false); channelList.add(socketChannel); }else { System.out.println("沒有請求過來?。?!"); } for (SocketChannel client : channelList){ ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 也不阻塞 int num = client.read(byteBuffer); if(num>0){ System.out.println("客戶端端口:"+ client.socket().getPort()+",客戶端收據(jù):"+new String(byteBuffer.array())); }else { System.out.println("等待客戶端寫數(shù)據(jù)"); } } } } catch (IOException e) { e.printStackTrace(); } }
2.3 優(yōu)點和缺點
優(yōu)點:
非阻塞I/O可以同時處理多個客戶端連接,提高服務(wù)器的并發(fā)處理能力。
由于非阻塞I/O的模式下,一個線程可以處理多個I/O操作,因此可以減少線程切換次數(shù),提高系統(tǒng)性能
缺點:
- 有很多無效訪問,因為沒有連接的時候accept也不會阻塞,很多為空的accpet
- 如果客戶端沒有寫數(shù)據(jù),會一直向內(nèi)核訪問,每次都是一個系統(tǒng)調(diào)用,非常浪費系統(tǒng)資源
2.4 思考
問 :既然一直輪詢會產(chǎn)生很多的無效輪詢,并浪費系統(tǒng)資源,那么有沒有更好的辦法呢
答: 通過事件注冊的方式(多路復(fù)用器)
3. NIO(NewIO)
3.1 簡述
NewIO才是真正意義上的NIO,NoBlockingIO只能算是NIO的前身,因為NewIO在NoBlockingIO上加上了多路復(fù)用器,使得NIO更加完美
在下圖中,channel不再是直接循環(huán)調(diào)用內(nèi)核,而是將連接,接收,讀取,寫入等事件注冊到多路復(fù)用器中,如果沒有事件到來將會阻塞等待
NIO三件套(記):
- channel: 介于字節(jié)緩沖區(qū)(buffer)和套接字(socket)之間,可以同時讀寫,支持異步IO
- buffer: 字節(jié)緩沖區(qū),是應(yīng)用程序和通道之間進行IO數(shù)據(jù)傳輸?shù)闹修D(zhuǎn)
- selector:多路復(fù)用器,監(jiān)聽服務(wù)端和客戶端的管道上注冊的事件
3.2 代碼示例
從代碼示例可以看到,在沒有連接的時候會在selector.select()中阻塞,然后等待客戶端連接或者寫入數(shù)據(jù),不同的監(jiān)聽事件會有不同的處理方法
具體流程:
服務(wù)端創(chuàng)建Selector,并注冊O(shè)P_ACCEPT接受連接事件,然后調(diào)用select阻塞等待連接進來
客戶端注冊O(shè)P_CONNECT事件,表示連接客戶端,連接成功后會調(diào)用handlerConnect方法
2.1 handlerConnect方法會注冊O(shè)P_READ事件并向服務(wù)端寫數(shù)據(jù)
這時候服務(wù)端會收到OP_ACCEPT后就會走到handlerAccept方法,表示接受連接
3.1handlerAccept方法也會注冊一個OP_READ事件并向客戶端寫數(shù)據(jù)
客戶端接收到服務(wù)端的數(shù)據(jù)后會再次喚醒select方法,然后判斷為isReadable(讀事件,服務(wù)端寫入給客戶端,那么客戶端就是讀),handlerRead方法將會把服務(wù)端寫入的數(shù)據(jù)讀取
反之亦然,服務(wù)端也會收到客戶端寫入的數(shù)據(jù),然后通過讀事件將數(shù)據(jù)讀取
服務(wù)端代碼
public class NewIOServer { static Selector selector; public static void main(String[] args) { try { selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 需要把serverSocketChannel注冊到多路復(fù)用器上 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 阻塞 selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isAcceptable()) { handlerAccept(key); } else if (key.isReadable()) { handlerRead(key); }else if(key.isWritable()){ } } } } catch (IOException e) { e.printStackTrace(); } } private static void handlerRead(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer allocate = ByteBuffer.allocate(1024); try { socketChannel.read(allocate); System.out.println("server msg:" + new String(allocate.array())); } catch (IOException e) { e.printStackTrace(); } } private static void handlerAccept(SelectionKey key) { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); // 不阻塞 try { SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.write(ByteBuffer.wrap("It‘s server msg".getBytes())); // 讀取客戶端的數(shù)據(jù) socketChannel.register(selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } } }
客戶端代碼
public class NewIOClient { static Selector selector; public static void main(String[] args) { try { selector = Selector.open(); SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("localhost", 8080)); // 需要把socketChannel注冊到多路復(fù)用器上 socketChannel.register(selector, SelectionKey.OP_CONNECT); while (true) { // 阻塞 selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isConnectable()) { handlerConnect(key); } else if (key.isReadable()) { handlerRead(key); } else if (key.isWritable()) { } } } } catch (IOException e) { e.printStackTrace(); } } private static void handlerRead(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer allocate = ByteBuffer.allocate(1024); try { socketChannel.read(allocate); System.out.println("client msg:" + new String(allocate.array())); } catch (IOException e) { e.printStackTrace(); } } private static void handlerConnect(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); if (socketChannel.isConnectionPending()) { socketChannel.finishConnect(); } socketChannel.configureBlocking(false); socketChannel.write(ByteBuffer.wrap("it‘s client msg".getBytes())); socketChannel.register(selector,SelectionKey.OP_READ); } }
3.3 優(yōu)點和缺點
優(yōu)點:
NIO使用了非阻塞IO,可以大大提高系統(tǒng)的吞吐量和并發(fā)性能。
NIO提供了可擴展的選擇器,可以監(jiān)控多個通道的狀態(tài),從而實現(xiàn)高效的事件驅(qū)動模型。
NIO采用直接內(nèi)存緩沖區(qū),可以避免Java堆內(nèi)存的GC問題,提高內(nèi)存管理的效率。
缺點:
- NIO的編程模型相比傳統(tǒng)的IO模型更加復(fù)雜,需要掌握較多的API和概念。
- NIO的實現(xiàn)難度較高,需要處理很多細節(jié)問題,如緩沖區(qū)的管理、選擇器的使用等。
- NIO的可靠性不如傳統(tǒng)的IO模型,容易出現(xiàn)空輪詢、系統(tǒng)負載過高等問題。
3.4 思考
問:select方法不是也阻塞嗎,那跟BIO有什么區(qū)別?
答:雖然他是在select阻塞,但是他通過事件注冊的方式,可以將多個selectKey同時加載到selectionKeys集合中,通過for循環(huán)處理不同的事件,而BIO只能由一個連接處理完才能處理下一個連接
問:什么是多路復(fù)用?
答:
多路:是指多個連接的管道,通道
復(fù)用:復(fù)用一個系統(tǒng)調(diào)用,原本多次系統(tǒng)調(diào)用變成一次
4. 擴展select/poll、epoll
4.1 簡述
由第三部分的NIO可知,多路復(fù)用把for循環(huán)的系統(tǒng)調(diào)用變成了一次調(diào)用,那么他具體是怎么實現(xiàn)的?
其實我們仔細思考一下就能知道,他主要實現(xiàn)就是在selector.select()
,由他去阻塞和觸發(fā)動作。然而在實現(xiàn)這些功能的時候,就用到了三種模型,select、poll、epoll。因為select和poll很相似,所以大家都會把他們歸為一類。
4.2 select/poll
我們先來說說什么是select?
實現(xiàn)過程
- 每一個socket調(diào)用select()方法后,socket的等待隊列就會放線程的引用,該線程就是你調(diào)用select的那個線程
- 當其中一個socket發(fā)送數(shù)據(jù)的時候,他會將每一個socket在等待隊列中移除放入就緒隊列,這就表明一定有一個客戶端寫了數(shù)據(jù)過來,但是注意,這并不表示所有都有客戶端寫了數(shù)據(jù)過來
- 這時候喚醒主線程,然后去就緒隊列中遍歷找到客戶端寫的數(shù)據(jù)并返回
具體如下圖所示:
產(chǎn)生問題
- 因為fd(file)是個數(shù)組,所以socket容量會有上限
- 只要有一個socket寫入就會遍歷所有socket,雖然減少了空輪詢問題,但是每次都要在所有socket中去找到已準備好的那個socket需要消耗性能
什么是poll?
因為fd是個數(shù)組,所以容量會達到上限,而poll則將這個數(shù)據(jù)結(jié)構(gòu)改成了鏈表,所以解決了select模型中上限的問題,但是遍歷socket的問題還是存在
select和poll的本質(zhì)區(qū)別就是一個是用數(shù)組存放socket,一個是用鏈表存放,其他地方?jīng)]有任何區(qū)別
4.3 epoll
epoll和select/poll相比,采用了事件回調(diào)的機制,并且使用紅黑樹去維護注冊的socket,如下圖所示
實現(xiàn)過程:
- 調(diào)用Selector.open的時候會創(chuàng)建一個eventpoll的文件,里面主要含有等待隊列,rbr(紅黑樹),就緒列表
- 然后在建立連接的時候調(diào)用epoll_ctl函數(shù)將socket放入epitem中
- 調(diào)用epoll_wait函數(shù)將線程放入等待隊列中,等待數(shù)據(jù)過來時喚醒
- 有數(shù)據(jù)寫入的時候會觸發(fā)epitem的回調(diào)方法,將該epitem移除并加入rdlist就緒列表中
- 當有數(shù)據(jù)在就緒列表的時候,就會喚醒等待對列中的線程并處理數(shù)據(jù)
這樣通過紅黑樹來維護連接和通過就緒列表來處理數(shù)據(jù)就可以保證可以存放最大限度的socket數(shù)量,并且在喚醒線程處理去處理就緒列表的時候肯定都是需要處理并且已就緒的socket。完美的解決了select/poll中的問題
總結(jié)
epoll相較于select/poll的優(yōu)勢:
采用了事件驅(qū)動的方式,可以處理大量的連接,效率更高。
支持邊緣觸發(fā)(ET)和水平觸發(fā)(LT)兩種模式,可以更靈活地處理IO事件。
記錄了上次處理的位置,可以避免重復(fù)的遍歷,更加高效。
高效利用了內(nèi)核空間和用戶空間的交互,避免了復(fù)制文件描述符。
4.4 擴展話題
對于epoll的一些擴展,有興趣的可以了解下,不感興趣可以略過
4.4.1 什么是ET和LT?
ET和LT是epoll工作模式中的兩種觸發(fā)方式,分別表示邊緣觸發(fā)(Edge Triggered)和水平觸發(fā)(Level Triggered)。
邊緣觸發(fā)(ET)
在ET模式下,當一個文件描述符上出現(xiàn)事件時,epoll_wait函數(shù)只會通知一次,即只有在文件描述符狀態(tài)發(fā)生變化時才會返回。如果應(yīng)用程序沒有處理完這個事件,那么下一次調(diào)用epoll_wait函數(shù)時,它不會再返回這個事件,直到下一次狀態(tài)變化。
ET模式下的事件處理更為高效,因為它只會在必要的時候通知應(yīng)用程序,避免了重復(fù)通知的問題。但是,由于ET模式只在狀態(tài)變化時通知一次,因此應(yīng)用程序需要及時處理事件,否則可能會錯過某些事件。
水平觸發(fā)(LT)
在LT模式下,當一個文件描述符上出現(xiàn)事件時,epoll_wait函數(shù)會重復(fù)通知應(yīng)用程序,直到該文件描述符上的事件被處理完畢為止。如果應(yīng)用程序沒有處理完這個事件,那么下一次調(diào)用epoll_wait函數(shù)時,它會再次返回這個事件,直到應(yīng)用程序處理完為止。
LT模式下的事件處理比較簡單,因為它會重復(fù)通知應(yīng)用程序,直到應(yīng)用程序處理完為止。但是,由于重復(fù)通知的問題,LT模式下可能會導(dǎo)致一些性能問題。同時,在LT模式下,應(yīng)用程序需要及時處理事件,否則可能會導(dǎo)致文件描述符上的事件積壓,影響系統(tǒng)的性能。
4.4.2 什么是驚群?
epoll的驚群(Thundering Herd)指的是多個線程或進程同時等待同一個epoll文件描述符上的事件,
當文件描述符上出現(xiàn)事件時,內(nèi)核會通知所有等待的線程或進程,但只有一個線程或進程能夠真正處理該事件,其他線程或進程會被喚醒但不能處理該事件,從而造成資源浪費和性能降低的問題。
驚群問題是由于內(nèi)核通知等待線程或進程的方式引起的。在epoll中,當文件描述符上出現(xiàn)事件時,內(nèi)核會通知所有等待的線程或進程,而不是通知一個線程或進程。因此,如果有多個線程或進程等待同一個文件描述符,那么當該文件描述符上出現(xiàn)事件時,內(nèi)核會通知所有等待的線程或進程,導(dǎo)致驚群問題。
為了解決驚群問題,可以采用以下兩種方式:
- 使用邊緣觸發(fā)(ET)模式:在ET模式下,當文件描述符上出現(xiàn)事件時,內(nèi)核只會通知一個等待的線程或進程,從而避免了驚群問題。
- 采用互斥量或條件變量等機制:在多個線程或進程等待同一個文件描述符時,可以使用互斥量或條件變量等機制來控制線程或進程的喚醒,從而避免驚群問題。
5. AIO
5.1簡述
在上面將的BIO,NIO中都是同步IO,BIO叫做同步阻塞,NIO叫做同步非阻塞,那么AIO則是異步IO,全名(Asynchronous I/O)
5.2 代碼示例
從代碼示例可以看到,以下代碼都是基于回調(diào)機制實現(xiàn)的,并不會像BIO和NIO一樣使用輪詢的方式,他不需要像同步IO一樣需要查找就緒socket,只要客戶端有數(shù)據(jù)寫入就會回調(diào)給服務(wù)端,既然是異步的所以就不會存在阻塞
服務(wù)端代碼
public class AIOServer { public static void main(String[] args) throws Exception { // 創(chuàng)建一個SocketChannel并綁定了8080端口 final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { try { // 打印線程的名字 System.out.println("2--"+Thread.currentThread().getName()); System.out.println(socketChannel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); // socketChannel異步的讀取數(shù)據(jù)到buffer中 socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer buffer) { // 打印線程的名字 System.out.println("3--"+Thread.currentThread().getName()); buffer.flip(); System.out.println(new String(buffer.array(), 0, result)); socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer buffer) { exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); System.out.println("1--"+Thread.currentThread().getName()); Thread.sleep(Integer.MAX_VALUE); } }
客戶端代碼
public class AIOClient { private final AsynchronousSocketChannel client; public AIOClient() throws IOException { client = AsynchronousSocketChannel.open(); } public static void main(String[] args) throws Exception { new AIOClient().connect("localhost",8080); } public void connect(String host, int port) throws Exception { // 客戶端向服務(wù)端發(fā)起連接 client.connect(new InetSocketAddress(host, port), null, new CompletionHandler<Void, Object>() { @Override public void completed(Void result, Object attachment) { try { client.write(ByteBuffer.wrap("這是一條測試數(shù)據(jù)".getBytes())).get(); System.out.println("已發(fā)送到服務(wù)端"); } catch (Exception e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); final ByteBuffer bb = ByteBuffer.allocate(1024); // 客戶端接收服務(wù)端的數(shù)據(jù),獲取的數(shù)據(jù)寫入到bb中 client.read(bb, null, new CompletionHandler<Integer, Object>() { @Override public void completed(Integer result, Object attachment) { // 服務(wù)端返回數(shù)據(jù)的長度result System.out.println("I/O操作完成:" + result); System.out.println("獲取反饋結(jié)果:" + new String(bb.array())); } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } } }
5.3 優(yōu)點和缺點
優(yōu)勢:
更加高效:AIO采用回調(diào)方式,可以避免輪詢等操作對CPU的占用,減少CPU的負擔(dān),從而提高了系統(tǒng)的性能。
可以更好地利用系統(tǒng)資源:AIO能夠在I/O操作完成之前把線程釋放出來,可以更好地利用系統(tǒng)資源,提高系統(tǒng)的并發(fā)處理能力。
適用于高并發(fā)場景:AIO適用于高并發(fā)場景,能夠支持大量的并發(fā)連接,提高系統(tǒng)的處理能力。
缺點:
學(xué)習(xí)成本高:相比于NIO,AIO的編程模型更加復(fù)雜,需要學(xué)習(xí)更多的知識,學(xué)習(xí)成本更高。
實現(xiàn)難度大:AIO的實現(xiàn)難度比較大,需要對操作系統(tǒng)的底層機制有深入的了解,因此開發(fā)成本較高。
并非所有操作系統(tǒng)都支持:AIO并非所有操作系統(tǒng)都支持,只有Linux 2.6以上的內(nèi)核才支持AIO,因此跨平臺的支持較差。
ps: 說白了AIO很好用,但是太復(fù)雜
以上就是通過Java帶你了解網(wǎng)絡(luò)IO模型的詳細內(nèi)容,更多關(guān)于Java 網(wǎng)絡(luò)IO模型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于TreeMap自定義排序規(guī)則的兩種方式
這篇文章主要介紹了關(guān)于TreeMap自定義排序規(guī)則的兩種方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08帶你了解Java數(shù)據(jù)結(jié)構(gòu)和算法之無權(quán)無向圖
這篇文章主要為大家介紹了Java數(shù)據(jù)結(jié)構(gòu)和算法之無權(quán)無向圖?,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-01-01Java中關(guān)于String StringBuffer StringBuilder特性深度解析
這篇文章主要介紹了Java中關(guān)于String StringBuffer StringBuilder特性深度解析,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09mybatis-puls中的resultMap數(shù)據(jù)映射
這篇文章主要介紹了mybatis-puls中的resultMap數(shù)據(jù)映射,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08Serializable接口的作用_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細介紹了java中Serializable接口的作用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05SpringBoot項目優(yōu)雅的全局異常處理方式(全網(wǎng)最新)
這篇文章主要介紹了SpringBoot項目優(yōu)雅的全局異常處理方式(全網(wǎng)最新),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04