1. 程式人生 > 其它 >「Netty系列」介紹下Netty中常用的編碼器和解碼器

「Netty系列」介紹下Netty中常用的編碼器和解碼器

技術標籤:Nettyjavanetty網路協議

​週末 文章走起。前面文章介紹Netty相關知識點。接下來將介紹下在通訊過程中用的編碼器和解碼器。這裡會不會聯想到諜戰戲裡面。傳送情報者怕情報洩露,所以對情報行加密然後傳給接收者。接收者對情報進行解密,得到情報。這裡講的編碼器和解碼器是和情報傳遞很相似?一起檢視這篇文章,來揭祕!!!

一 編解碼器

1.1 什麼叫編解碼器

在網路傳輸的過程中,資料都是以位元組流的方式進行傳遞。客戶端在向服務端傳送資料的時候,將業務中其他型別資料轉化為位元組,叫編碼。服務端接收到資料為位元組流,將位元組流轉化為原來的格式,叫解碼。統稱codec。

編解碼器分為兩部分-編碼器和解碼器,編碼器負責出站,解碼器負責入站。

1.2 解碼器

1.2.1 概述

解碼器負責入站操作,那麼也一定要實現ChannelInboundHandler介面,所以解碼器本質 上也是ChannelHandler。我們自定義編解碼器只需要繼承ByteToMessageDecoder(Netty提供抽象類,繼承 ChannelInboundHandlerAdapter),實現decode()。Netty提供一些常用的解碼器實現, 開箱即用。如下:

1RedisDecoder基於Redis協議的解碼器
2XmlDecoder基於XML格式的解碼器
3JsonObjectDecoder基於json資料格式的解碼器
4HttpObjectDecoder基於http協議的解碼器

Netty也提供了MessageToMessageDecoder,將⼀種格式轉化為另⼀種格式的解碼器,也提供了⼀些 實現,如下:

1StringDecoder將接收到ByteBuf轉化為字串
2ByteArrayDecoder將接收到ByteBuf轉化位元組陣列
3 Base64Decoder 將由ByteBuf或US-ASCII字串編碼的Base64解碼為ByteBuf。

1.2.2 將位元組流轉化為Intger型別(案例)

1. 位元組解碼器

packagecom.haopt.netty.codec;
importio.netty.buffer.ByteBuf;
importio.netty.channel.ChannelHandlerContext;
importio.netty.handler.codec.ByteToMessageDecoder;

importjava.util.List;
publicclassByteToIntegerDecoderextendsByteToMessageDecoder{
/**
*
*@paramctx上下⽂
*@paramin輸⼊的ByteBuf訊息資料
*@paramout轉化後輸出的容器
*@throwsException
*/
@Override
protectedvoiddecode(ChannelHandlerContextctx,ByteBufin,List<Object>out)throwsException{
if(in.readableBytes()>=4){//int型別佔⽤4個位元組,所以需要判斷是否存在有4個位元組,再進⾏讀取
out.add(in.readInt());//讀取到int型別資料,放⼊到輸出,完成資料型別的轉化
}
}
}

2. Handler

packagecom.haopt.netty.codec;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.ChannelInboundHandlerAdapter;
publicclassServerHandlerextendsChannelInboundHandlerAdapter{
@Override
publicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{
Integeri=(Integer)msg;//這⾥可以直接拿到Integer型別的資料
System.out.println("服務端接收到的訊息為:"+i);
}
}

3 在pipeline中新增解碼器

@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline()
.addLast(newByteToIntegerDecoder())
.addLast(newServerHandler());
}

可以將程式碼複製到IDEA執行下,檢視下執行效果

1.3 編碼器

1.3.1 概述

將原來的格式轉化為位元組。我們要實現自定義解碼器只要繼承MessageToByteEncoder(實現了ChannelOutboundHandler接⼝),本質上也是ChannelHandler。Netty中一些實現的編碼器,如下:

1ObjectEncoder將物件(需要實現Serializable接⼝)編碼為位元組流
2SocksMessageEncoder將SocksMessage編碼為位元組流
3HAProxyMessageEncoder將HAProxyMessage編碼成位元組流

Netty也提供了MessageToMessageEncoder,將⼀種格式轉化為另⼀種格式的編碼器,也提供了⼀些 實現:

1RedisEncoder將Redis協議的物件進⾏編碼
2StringEncoder將字串進⾏編碼操作
3Base64Encoder將Base64字串進⾏編碼操作

1.3.2 將Integer型別編碼為位元組進⾏傳遞(案例)

1. 自定義編碼器

packagecom.haopt.netty.codec.client;
importio.netty.buffer.ByteBuf;
importio.netty.channel.ChannelHandlerContext;
importio.netty.handler.codec.MessageToByteEncoder;
publicclassIntegerToByteEncoderextendsMessageToByteEncoder<Integer>{
@Override
protectedvoidencode(ChannelHandlerContextctx,Integermsg,ByteBufout)throwsException{
out.writeInt(msg);
}
}

2. Handler

packagecom.haopt.netty.codec.client;
importio.netty.buffer.ByteBuf;
importio.netty.buffer.Unpooled;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.SimpleChannelInboundHandler;
importio.netty.util.CharsetUtil;
publicclassClientHandlerextendsSimpleChannelInboundHandler<ByteBuf>{
@Override
protectedvoidchannelRead0(ChannelHandlerContextctx,ByteBufmsg)throwsException{
System.out.println("接收到服務端的訊息:"+
msg.toString(CharsetUtil.UTF_8));
}
@Override
publicvoidchannelActive(ChannelHandlerContextctx)throwsException{
ctx.writeAndFlush(123);
}
@Override
publicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{
cause.printStackTrace();
ctx.close();
}
}

3. pipeline

@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline().addLast(newIntegerToByteEncoder());
ch.pipeline().addLast(newClientHandler());
}

二 開發Http伺服器

通過Netty中提供的http的解碼器,進行http伺服器開發。建議程式碼複製下來,執行下看看效果。

2.1 Netty配置

1. server

packagecom.haopt.netty.codec.http;
importio.netty.bootstrap.ServerBootstrap;
importio.netty.channel.ChannelFuture;
importio.netty.channel.ChannelInitializer;
importio.netty.channel.EventLoopGroup;
importio.netty.channel.nio.NioEventLoopGroup;
importio.netty.channel.socket.SocketChannel;
importio.netty.channel.socket.nio.NioServerSocketChannel;
importio.netty.handler.codec.http.HttpObjectAggregator;
importio.netty.handler.codec.http.HttpRequestDecoder;
importio.netty.handler.codec.http.HttpResponseEncoder;
importio.netty.handler.stream.ChunkedWriteHandler;
publicclassNettyHttpServer{
publicstaticvoidmain(String[]args)throwsException{
//主執行緒,不處理任何業務邏輯,只是接收客戶的連線請求
EventLoopGroupboss=newNioEventLoopGroup(1);
//⼯作執行緒,執行緒數預設是:cpu*2
EventLoopGroupworker=newNioEventLoopGroup();
try{
//伺服器啟動類
ServerBootstrapserverBootstrap=newServerBootstrap();
serverBootstrap.group(boss,worker);
//配置server通道
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(newChannelInitializer<SocketChannel>(){
@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline()
//http請求的解碼器
//將http請求中的uri以及請求體聚合成⼀個完整的FullHttpRequest物件
.addLast(newHttpRequestDecoder())
.addLast(newHttpObjectAggregator(1024*128))
.addLast(newHttpResponseEncoder())//http響應的編碼器
.addLast(newChunkedWriteHandler())//⽀持非同步的⼤⽂件傳輸,防⽌記憶體溢位
.addLast(newServerHandler());
}
});//worker執行緒的處理器
ChannelFuturefuture=serverBootstrap.bind(8080).sync();
System.out.println("伺服器啟動完成。。。。。");
//等待服務端監聽端⼝關閉
future.channel().closeFuture().sync();
}finally{
//優雅關閉
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}

2. ServerHandler

packagecom.haopt.netty.codec.http;
importio.netty.buffer.Unpooled;
importio.netty.channel.ChannelFutureListener;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.ChannelInboundHandlerAdapter;
importio.netty.channel.SimpleChannelInboundHandler;
importio.netty.handler.codec.http.*;
importio.netty.util.CharsetUtil;
importjava.util.Map;
publicclassServerHandlerextendsSimpleChannelInboundHandler<FullHttpRequest>{
@Override
publicvoidchannelRead0(ChannelHandlerContextctx,FullHttpRequestrequest)throwsException{
//解析FullHttpRequest,得到請求引數
Map<String,String>paramMap=newRequestParser(request).parse();
Stringname=paramMap.get("name");
//構造響應物件
FullHttpResponsehttpResponse=newDefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK);
httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=utf-8");
StringBuildersb=newStringBuilder();
sb.append("<h1>");
sb.append("你好,"+name);
sb.append("</h1>");
httpResponse.content().writeBytes(Unpooled.copiedBuffer(sb,CharsetUtil.UTF_8));
//操作完成後,將channel關閉
ctx.writeAndFlush(httpResponse).addListener(ChannelFutureListener.CLOSE);
}
}

3. RequestParser

packagecom.haopt.netty.codec.http;
importio.netty.handler.codec.http.FullHttpRequest;
importio.netty.handler.codec.http.HttpMethod;
importio.netty.handler.codec.http.QueryStringDecoder;
importio.netty.handler.codec.http.multipart.Attribute;
importio.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
importio.netty.handler.codec.http.multipart.InterfaceHttpData;
importjava.io.IOException;
importjava.util.HashMap;
importjava.util.List;
importjava.util.Map;
/**
*HTTP請求引數解析器,⽀持GET,POST
*/
publicclassRequestParser{
privateFullHttpRequestfullReq;
/**
*構造⼀個解析器
*@paramreq
*/
publicRequestParser(FullHttpRequestreq){
this.fullReq=req;
}
/**
*解析請求引數
*@return包含所有請求引數的鍵值對,如果沒有引數,則返回空Map
*
*@throwsIOException
*/
publicMap<String,String>parse()throwsException{
HttpMethodmethod=fullReq.method();
Map<String,String>parmMap=newHashMap<>();
if(HttpMethod.GET==method){
//是GET請求
QueryStringDecoderdecoder=newQueryStringDecoder(fullReq.uri());
decoder.parameters().entrySet().forEach(entry->{
//entry.getValue()是⼀個List,只取第⼀個元素
parmMap.put(entry.getKey(),entry.getValue().get(0));
});
}elseif(HttpMethod.POST==method){
//是POST請求
HttpPostRequestDecoderdecoder=new
HttpPostRequestDecoder(fullReq);
decoder.offer(fullReq);
List<InterfaceHttpData>parmList=decoder.getBodyHttpDatas();
for(InterfaceHttpDataparm:parmList){
Attributedata=(Attribute)parm;
parmMap.put(data.getName(),data.getValue());
}
}else{
//不⽀持其它⽅法
thrownewRuntimeException("不⽀持其它⽅法");//可以用自定義異常來替代
}
returnparmMap;
}
}

4. 物件

packagecom.haopt.netty.codec.obj;
publicclassUserimplementsjava.io.Serializable{
privatestaticfinallongserialVersionUID=-89217070354741790L;
privateLongid;
privateStringname;
privateIntegerage;
publicLonggetId(){
returnid;
}
publicvoidsetId(Longid){
this.id=id;
}
publicStringgetName(){
returnname;
}
publicvoidsetName(Stringname){
this.name=name;
}
publicIntegergetAge(){
returnage;
}
publicvoidsetAge(Integerage){
this.age=age;
}
@Override
publicStringtoString(){
return"User{"+
"id="+id+
",name='"+name+'\''+
",age="+age+
'}';
}
}

2.2 服務端

1. NettyObjectServer

packagecom.haopt.netty.codec.obj;
importio.netty.bootstrap.ServerBootstrap;
importio.netty.channel.ChannelFuture;
importio.netty.channel.ChannelInitializer;
importio.netty.channel.EventLoopGroup;
importio.netty.channel.nio.NioEventLoopGroup;
importio.netty.channel.socket.SocketChannel;
importio.netty.channel.socket.nio.NioServerSocketChannel;
importio.netty.handler.codec.serialization.ClassResolvers;
importio.netty.handler.codec.serialization.ObjectDecoder;
publicclassNettyObjectServer{
publicstaticvoidmain(String[]args)throwsException{
//主執行緒,不處理任何業務邏輯,只是接收客戶的連線請求
EventLoopGroupboss=newNioEventLoopGroup(1);
//⼯作執行緒,執行緒數預設是:cpu*2
EventLoopGroupworker=newNioEventLoopGroup();
try{
//伺服器啟動類
ServerBootstrapserverBootstrap=newServerBootstrap();
serverBootstrap.group(boss,worker);
//配置server通道
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(newChannelInitializer<SocketChannel>(){
@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline()
.addLast(newObjectDecoder(ClassResolvers.weakCachingResolver(
this.getClass().getClassLoader()
)))
.addLast(newServerHandler());
}
});//worker執行緒的處理器
ChannelFuturefuture=serverBootstrap.bind(6677).sync();
System.out.println("伺服器啟動完成。。。。。");
//等待服務端監聽端⼝關閉
future.channel().closeFuture().sync();
}finally{
//優雅關閉
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}

2. ServerHandler

packagecom.haopt.netty.codec.obj;
importio.netty.buffer.Unpooled;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.SimpleChannelInboundHandler;
importio.netty.util.CharsetUtil;
publicclassServerHandlerextendsSimpleChannelInboundHandler<User>{
@Override
publicvoidchannelRead0(ChannelHandlerContextctx,Useruser)throwsException{
//獲取到user物件
System.out.println(user);
ctx.writeAndFlush(Unpooled.copiedBuffer("ok",CharsetUtil.UTF_8));
}
}

2.3 客戶端

1. NettyObjectClient

packagecom.haopt.netty.codec.obj;
importio.netty.bootstrap.Bootstrap;
importio.netty.channel.ChannelFuture;
importio.netty.channel.ChannelInitializer;
importio.netty.channel.EventLoopGroup;
importio.netty.channel.nio.NioEventLoopGroup;
importio.netty.channel.socket.SocketChannel;
importio.netty.channel.socket.nio.NioSocketChannel;
importio.netty.handler.codec.serialization.ObjectEncoder;
publicclassNettyObjectClient{
publicstaticvoidmain(String[]args)throwsException{
EventLoopGroupworker=newNioEventLoopGroup();
try{
//伺服器啟動類
Bootstrapbootstrap=newBootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(newChannelInitializer<SocketChannel>(){
@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline().addLast(newObjectEncoder());
ch.pipeline().addLast(newClientHandler());
}
});
ChannelFuturefuture=bootstrap.connect("127.0.0.1",6677).sync();
future.channel().closeFuture().sync();
}finally{
worker.shutdownGracefully();
}
}
}

2. ClientHandler

packagecom.haopt.netty.codec.obj;
importio.netty.buffer.ByteBuf;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.SimpleChannelInboundHandler;
importio.netty.util.CharsetUtil;
publicclassClientHandlerextendsSimpleChannelInboundHandler<ByteBuf>{
@Override
protectedvoidchannelRead0(ChannelHandlerContextctx,ByteBufmsg)throwsException{
System.out.println("接收到服務端的訊息:"+
msg.toString(CharsetUtil.UTF_8));
}
@Override
publicvoidchannelActive(ChannelHandlerContextctx)throwsException{
Useruser=newUser();
user.setId(1L);
user.setName("張三");
user.setAge(20);
ctx.writeAndFlush(user);
}
@Override
publicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{
cause.printStackTrace();
ctx.close();
}
}

2.4 JDK序列化的優化

JDK序列化使⽤是⽐較⽅便,但是效能較差,序列化後的位元組⽐較⼤,所以⼀般在項⽬中不 會使⽤⾃帶的序列化,⽽是會採⽤第三⽅的序列化框架Hessian編解碼。

1. 匯入依賴

<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>

2. User物件

packagecom.haopt.netty.codec.hessian;
publicclassUserimplementsjava.io.Serializable{
privatestaticfinallongserialVersionUID=-8200798627910162221L;
privateLongid;
privateStringname;
privateIntegerage;
publicLonggetId(){
returnid;
}
publicvoidsetId(Longid){
this.id=id;
}
publicStringgetName(){
returnname;
}
publicvoidsetName(Stringname){
this.name=name;
}
publicIntegergetAge(){
returnage;
}
publicvoidsetAge(Integerage){
this.age=age;
}
@Override
publicStringtoString(){
return"User{"+
"id="+id+
",name='"+name+'\''+
",age="+age+
'}';
}
}

3. Hessian序列化⼯具類

packagecom.haopt.netty.codec.hessian.codec;
importcom.caucho.hessian.io.HessianInput;
importcom.caucho.hessian.io.HessianOutput;
importjava.io.ByteArrayInputStream;
importjava.io.ByteArrayOutputStream;
importjava.io.IOException;
/**
*Hessian序列化⼯具類
*
*/
publicclassHessianSerializer{
public<T>byte[]serialize(Tobj){
ByteArrayOutputStreamos=newByteArrayOutputStream();
HessianOutputho=newHessianOutput(os);
try{
ho.writeObject(obj);
ho.flush();
returnos.toByteArray();
}catch(IOExceptione){
thrownewRuntimeException(e);
}finally{
try{
ho.close();
}catch(IOExceptione){
thrownewRuntimeException(e);
}
try{
os.close();
}catch(IOExceptione){
thrownewRuntimeException(e);
}
}
}

public<T>Objectdeserialize(byte[]bytes,Class<T>clazz){
ByteArrayInputStreamis=newByteArrayInputStream(bytes);
HessianInputhi=newHessianInput(is);
try{
return(T)hi.readObject(clazz);
}catch(IOExceptione){
thrownewRuntimeException(e);
}finally{
try{
hi.close();
}catch(Exceptione){
thrownewRuntimeException(e);
}
try{
is.close();
}catch(IOExceptione){
thrownewRuntimeException(e);
}
}
}
}

4. 編碼器

packagecom.haopt.netty.codec.hessian.codec;
importcn.itcast.netty.coder.hessian.User;
importio.netty.buffer.ByteBuf;
importio.netty.channel.ChannelHandlerContext;
importio.netty.handler.codec.MessageToByteEncoder;
publicclassHessianEncoderextendsMessageToByteEncoder<User>{
privateHessianSerializerhessianSerializer=newHessianSerializer();
protectedvoidencode(ChannelHandlerContextctx,Usermsg,ByteBufout)throwsException{
byte[]bytes=hessianSerializer.serialize(msg);
out.writeBytes(bytes);
}
}

5. 解碼器

publicclassHessianDecoderextendsByteToMessageDecoder{
privateHessianSerializerhessianSerializer=newHessianSerializer();

protectedvoiddecode(ChannelHandlerContextctx,ByteBufin,List<Object>
out)throwsException{
//複製⼀份ByteBuf資料,輕複製,⾮完全拷⻉
//避免出現異常:did not readanythingbutdecodedamessage
//Netty檢測沒有讀取任何位元組就會丟擲該異常
ByteBufin2=in.retainedDuplicate();
byte[]dst;
if(in2.hasArray()){//堆緩衝區模式
dst=in2.array();
}else{
dst=newbyte[in2.readableBytes()];
in2.getBytes(in2.readerIndex(),dst);
}
//跳過所有的位元組,表示已經讀取過了
in.skipBytes(in.readableBytes());
//反序列化
Objectobj=hessianSerializer.deserialize(dst,User.class);
out.add(obj);
}
}

6. 服務端

publicclassNettyHessianServer{
publicstaticvoidmain(String[]args)throwsException{
//System.setProperty("io.netty.noUnsafe","true");
//主執行緒,不處理任何業務邏輯,只是接收客戶的連線請求
EventLoopGroupboss=newNioEventLoopGroup(1);
//⼯作執行緒,執行緒數預設是:cpu*2
EventLoopGroupworker=newNioEventLoopGroup();
try{
//伺服器啟動類
ServerBootstrapserverBootstrap=newServerBootstrap();
serverBootstrap.group(boss,worker);
//配置server通道
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(newChannelInitializer<SocketChannel>
(){
@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline()
.addLast(newHessianDecoder())
.addLast(newServerHandler());
}
});//worker執行緒的處理器
//serverBootstrap.childOption(ChannelOption.ALLOCATOR,
UnpooledByteBufAllocator.DEFAULT);
ChannelFuturefuture=serverBootstrap.bind(6677).sync();
System.out.println("伺服器啟動完成。。。。。");
//等待服務端監聽端⼝關閉
future.channel().closeFuture().sync();
}finally{
//優雅關閉
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
publicclassServerHandlerextendsSimpleChannelInboundHandler<User>{
@Override
publicvoidchannelRead0(ChannelHandlerContextctx,Useruser)throws
Exception{
//獲取到user物件
System.out.println(user);
ctx.writeAndFlush(Unpooled.copiedBuffer("ok",CharsetUtil.UTF_8));
}
}

7. 客戶端(配置類)

publicclassNettyHessianClient{
publicstaticvoidmain(String[]args)throwsException{
EventLoopGroupworker=newNioEventLoopGroup();
try{
//伺服器啟動類
Bootstrapbootstrap=newBootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(newChannelInitializer<SocketChannel>(){
@Override
protectedvoidinitChannel(SocketChannelch)throwsException{
ch.pipeline().addLast(newHessianEncoder());
ch.pipeline().addLast(newClientHandler());
}
});
ChannelFuturefuture=bootstrap.connect("127.0.0.1",6677).sync();
future.channel().closeFuture().sync();
}finally{
worker.shutdownGracefully();
}
}
}
publicclassClientHandlerextendsSimpleChannelInboundHandler<ByteBuf>{
@Override
protectedvoidchannelRead0(ChannelHandlerContextctx,ByteBufmsg)throws
Exception{
System.out.println("接收到服務端的訊息:"+
msg.toString(CharsetUtil.UTF_8));
}

@Override
publicvoidchannelActive(ChannelHandlerContextctx)throwsException{
Useruser=newUser();
user.setId(1L);
user.setName("張三");
user.setAge(20);
ctx.writeAndFlush(user);
}

@Override
publicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)
throwsException{
cause.printStackTrace();
ctx.close();
}
}

這篇文章,介紹了什麼是編碼器、解碼器,也講述瞭如何實戰中運用編碼器和解碼器。希望能對有所幫助。在開頭提到的我們本文的編碼器解碼器和情報資訊互動是否相似?在我看來,是相似的。發報人將自己看的懂得資訊,按照某種規則進行加密。收報人接收到資訊是加密後的資料,需要進行按照規則進行解密才能看懂。我們客戶端在進行傳送資料,需要將程式中的資料變為二進位制流傳送。服務端接收到資料,需要將二進位制流轉化為程式可以操作資料型別。留言區,等著各位高見!

---------------------------------------------------------------------END---------------------------------------------------------------------------------------------

公眾號簡介

在上班之餘,學習黑馬架構課程一段時間了。持續的分享一些乾貨,如:Netty、MySQL、快取、中介軟體、中臺、自動化部署、設計模式、JVM、Spring原始碼、MyBatis原始碼。如果你想對該公眾號有深入瞭解,在公眾號回覆【故事】。

公眾號福利

關注公眾號,回覆【架構面試】,獲取架構課程面試題。如果有任何建議或者想說的話,請直接向公眾號發訊息。運營小姐姐看到後會立即處理。煩請耐心等待!蟹蟹!