前言
为什么要用它呢?思考一个场景。
场景:
我们的通信内容包含很多不同类型的实体类,比如user、role、goods、order等等pojo对象需要传递,那么在我们handler的处理器channelRead0方法中,要怎么才能快速知道这个数据是什么对象呢,如果用String或者bytebuf等等,那么要知道这是什么对象,需要进行很多的逻辑处理,所以Object编码器就应运而生了,它可以帮我们直接在channelRead0方法中拿到数据类型,无需做其他的解析处理。
在集成之前,先来看一下bject编解码器怎么用。
Object编解码器分析
首先简单的看一下源码,大致了解一下编解码器的原理。
标红的地方可以看出来,使用Object的编解码器,首先我们要传输的数据需要实现Serializable接口
,其次,也可以看出来,这个编码器在传输的时候在传输数据其实位置加了4个字节,再看一下解码器:
解码器继承了LengthFieldBasedFrameDecoder,解析的时候去掉了编码器加的这四个字节,那么就说明Object编解码器是自带防止拆包、粘包功能的
,所以,如无特殊需求,不用再去另外实现一套拆包、粘包的方法了。
另外,值得注意的地方是,Object编解码器能够将对象转成流传输,然后又能将流转成对象,这个原理是因为在编码的时候,它将对象的类名也传输了过去
,所以才能做到精准解码成对应的对象,举个简单的例子说明:
可以看到,在客户端
项目com.hwdz.compute.entity
包路径下,有一个BerthGuide实体类,那么在编码的时候会将实体类BerthGuide的类名com.hwdz.compute.entity.BerthGuide
通过反射的方式拿到,并且封装起来,一块传递到服务端,服务端在解码的时候,会拿到类名,然后再将数据流转换成对应的实体类BerthGuide。
总结,为了能够让对象在转流传输之后,还能解析还原成对应的对象,我们必须保证客户端和服务端的对象所在包名路径一致
。
但是如果因为特殊需求我们不能保证项目的包名路径一致的话,那只能自定义Object解码器了,但是最少要保证实体类名称一致。
自定义Object解码器
根据现有的解码器实现原理,照葫芦画瓢,自定义一个差不多的解码器即可,具体实现代码如下:
/**
* @author: zhouwenjie
* @description:
* @create: 2022-07-21 11:19
**/
public class MyObjectDecoder extends LengthFieldBasedFrameDecoder {
private String classPath;
public MyObjectDecoder(String classPath) {
this(1048576, classPath);
}
public MyObjectDecoder(int maxObjectSize, String classPath) {
super(maxObjectSize, 0, 4, 0, 4);
this.classPath = classPath;
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ObjectInputStream ois = null;
ByteBuf byteBuf = in.retainedDuplicate();
try {
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) {
//不能这么写,否则会报错数据太长的错误
//in.resetReaderIndex();
//ByteBuf byteBuf = in.retainedDuplicate();
//in.skipBytes(in.readableBytes());
return byteBuf;
}
ois = new MyCompactObjectInputStream(new ByteBufInputStream(frame, true), classPath);
return ois.readObject();
} catch (Exception e) {
//in.resetReaderIndex();
//ByteBuf byteBuf = in.retainedDuplicate();
//in.skipBytes(in.readableBytes());
return byteBuf;
} finally {
if (ois != null) {
ois.close();
}
}
}
}
/**
* @author: zhouwenjie
* @description:
* @create: 2022-07-22 16:43
**/
public class MyCompactObjectInputStream extends ObjectInputStream {
private String classPath;
public MyCompactObjectInputStream(InputStream in, String classPath) throws IOException {
super(in);
this.classPath = classPath;
}
@Override
protected void readStreamHeader() throws IOException {
int version = readByte() & 0xFF;
if (version != STREAM_VERSION) {
throw new StreamCorruptedException(
"Unsupported version: " + version);
}
}
@Override
protected ObjectStreamClass readClassDescriptor()
throws IOException, ClassNotFoundException {
int type = read();
if (type < 0) {
throw new EOFException();
}
switch (type) {
case 0:
return super.readClassDescriptor();
case 1:
String className = readUTF();
//com.hwdz.netty.entity.BerthGuide
if (!classPath.equals(className)) {
String[] split = className.split("\\.");
String s = split[split.length - 1];
className = classPath + s;
}
Class<?> clazz = Class.forName(className);
return ObjectStreamClass.lookupAny(clazz);
default:
throw new StreamCorruptedException(
"Unexpected class descriptor type: " + type);
}
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class<?> clazz;
try {
clazz = Class.forName(desc.getName());
} catch (ClassNotFoundException ignored) {
clazz = super.resolveClass(desc);
}
return clazz;
}
}
实现自动转换的逻辑就是:首先获取本项目pojo所在的包路径,然后在获取传递过来的包路径逻辑中,直接替换掉实体类的路径,这样就转成实体类对象在本项目下的路径了。
当然,如果闲麻烦可以直接继承现成的ObjectDecoder也是可以的
项目集成
集成原始的编解码器
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new ObjectEncoder());
pipeline.addLast(lightsHandler);
}
对象消息处理器,主要接收处理对象消息
集成自定义的编解码器