Dubbo 框架采用 微内核 + 插件 的基本设计原则,自身功能几乎也都通过 SPI 扩展点实现,可以方便地被用户自由扩展和更换。
Dubbo 框架采用分层设计,自上而下共分为十层,各层均为单向依赖,每一层都可以剥离上层被复用。
本篇文章就来介绍一下最底下的 Serialize 序列化层。
序列化 (Serialization) 是将对象的状态信息转换为可以存储或传输的字节序列的过程,与之对应的还有反序列化,即将可存储或传输的字节序列转换为对象的过程。
为什么需要序列化呢???
以 Java 语言为例,它是一门面向对象的编程语言,我们在程序里操作的是对象,方法调用的参数和返回值亦是对象。Dubbo 作为一个 RPC 框架,基于代理帮我们实现了远程调用,屏蔽了底层实现的细节。但是我们得知道,对象本身是无法在网络中传输的,网络传输的只能是字节序列。
所以,Dubbo 在发送请求前,先得把对象序列化成字节序列,对端收到字节序列后再按照同样的规则反序列化成对象,再交给我们的业务代码处理,发送响应结果同样如此。
Dubbo 针对 Serialize 层专门新建了一个dubbo-serialization
模块,其中子模块dubbo-serialization-api
用于定义抽象接口,其它模块负责具体实现。
dubbo-serialization
--dubbo-serialization-api
--dubbo-serialization-hessian2
--dubbo-serialization-fastjson
--dubbo-serialization-kryo
--dubbo-serialization-fst
--dubbo-serialization-jdk
--dubbo-serialization-protostuff
--dubbo-serialization-avro
......
Serialize 层核心接口就三个:
Serialization 是扩展接口,用于生成具体的序列化器和反序列化器,默认使用 hessian2。
@SPI("hessian2")
public interface Serialization {
byte getContentTypeId();
String getContentType();
@Adaptive
ObjectOutput serialize(URL url, OutputStream output) throws IOException;
@Adaptive
ObjectInput deserialize(URL url, InputStream input) throws IOException;
}
ObjectOutput 是序列化器的抽象接口,用于往输出流写对象。它继承自 DataOutput,除了可以写 Object,还支持写 Java 基本类型和字节数组。
public interface ObjectOutput extends DataOutput {
void writeObject(Object obj) throws IOException;
/****下面是Dubbo协议特有的需求,用来写异常、事件、和Map,其它协议未必支持****/
default void writeThrowable(Object obj) throws IOException {
writeObject(obj);
}
default void writeEvent(Object data) throws IOException {
writeObject(data);
}
default void writeAttachments(Map<String, Object> attachments) throws IOException {
writeObject(attachments);
}
}
ObjectInput 是反序列化器的抽象接口,用于从输入流读对象,代码就不贴了。
看到这里,你可能会有一个疑问。
RPC 请求和响应都被封装成 Request、Response 对象了,为什么不只定义一个writeObject
和readObject
方法直接读写整个 Request/Response 对象呢,为什么还要单独提供读写 int、boolean、String 等基本类型的方法???
这是因为,Dubbo 协议的报文包含两部分:协议头部、请求体。序列化只管请求体部分的数据,对于协议头部它是不关心的。而 Request/Response 对象属性是同时包含这两部分的数据的,如果直接序列化整个对象传输,会造成带宽的浪费,传输冗余的数据。
所以 Dubbo 的编解码器DubboCodec
不是一股脑直接读写整个大对象的,而是按照顺序写多个对象,对端再按照相同的顺序读出来就好了。
Dubbo 对 Request 编码的方法是DubboCodec#encodeRequestData
,对于一次 Dubbo 协议的 RPC 调用,Consumer 要依次写入:
@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version);
// https://github.com/apache/dubbo/issues/6138
String serviceName = inv.getAttachment(INTERFACE_KEY);
if (serviceName == null) {
serviceName = inv.getAttachment(PATH_KEY);
}
out.writeUTF(serviceName);
out.writeUTF(inv.getAttachment(VERSION_KEY));
out.writeUTF(inv.getMethodName());
out.writeUTF(inv.getParameterTypesDesc());
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
out.writeAttachments(inv.getObjectAttachments());
}
Provider 再从输入流里按照相同的顺序读出来即可,代码是DecodeableRpcInvocation#decode
。
Serialization 被设计成 SPI 接口,所以它可以很轻松的被替换。
接下来,我们就基于 Fastjson2 写一个序列化模块,替换掉默认的 hessian2,让你对整个序列化过程理解的更加清楚。
首先,我们新建一个dubbo-extension-serialization-fastjson2
模块。因为我们要依赖 Dubbo 提供的接口去实现一套新的序列化组件,所以自然要引入dubbo-serialization-api
模块。又因为我们是基于 fastjson2 实现的,所以也得引入 fastjson2 的依赖。
<dependencies>
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-serialization-apiartifactId>
<version>${dubbo.version}version>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2artifactId>
<version>2.0.44version>
dependency>
dependencies>
新建 Fastjson2Serialization 类实现 Serialization 接口,很简单,返回我们实现的 Fastjson2 对应的序列化器即可。
public class Fastjson2Serialization implements Serialization {
@Override
public byte getContentTypeId() {
return 22;
}
@Override
public String getContentType() {
return "text/json";
}
@Override
public ObjectOutput serialize(URL url, OutputStream output) throws IOException {
return new Fastjson2ObjectOutput(output);
}
@Override
public ObjectInput deserialize(URL url, InputStream input) throws IOException {
return new Fastjson2ObjectInput(input);
}
}
再编写 Fastjson2ObjectOutput 实现 ObjectOutput 接口,实现序列化相关的逻辑。我们这里直接用 JSONB 格式传输,效率比 JSON 字符串更高。
public class Fastjson2ObjectOutput implements ObjectOutput {
private final OutputStream output;
public Fastjson2ObjectOutput(OutputStream output) {
this.output = output;
}
@Override
public void writeObject(Object obj) throws IOException {
JSONWriter jsonWriter = JSONWriter.ofJSONB();
jsonWriter.writeAny(obj);
jsonWriter.flushTo(output);
}
......省略一堆方法
}
序列化器有了,再就是 Fastjson2ObjectInput 反序列化器了,同样的,基于 JSONReader 读即可。
public class Fastjson2ObjectInput implements ObjectInput {
private final InputStream input;
private final JSONReader jsonReader;
public Fastjson2ObjectInput(InputStream input) {
this.input = input;
try {
byte[] bytes = new byte[input.available()];
input.read(bytes);
this.jsonReader = JSONReader.ofJSONB(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Object readObject() throws IOException, ClassNotFoundException {
return jsonReader.readAny();
}
......省略一堆方法
}
至此,我们自定义的基于 Fastjson2 的序列化器就实现好了。
我们可以写个单元测试,看看它是否可以正常工作:
@Test
public void test() throws Exception {
Fastjson2Serialization serialization = new Fastjson2Serialization();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutput objectOutput = serialization.serialize(null, outputStream);
objectOutput.writeUTF("xixi");
objectOutput.writeInt(100);
objectOutput.writeFloat(200.5F);
objectOutput.flushBuffer();
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
ObjectInput objectInput = serialization.deserialize(null, inputStream);
Assertions.assertEquals("xixi", objectInput.readUTF());
Assertions.assertEquals(100, objectInput.readInt());
Assertions.assertEquals(200.5F, objectInput.readFloat());
}
接下来的问题是,如何让 Dubbo 加载我们自己写的序列化器呢???
这就要利用到 Dubbo 的 SPI 机制了,Dubbo 启动时会扫描 ClassPath 下所有的META-INF/dubbo
目录下,以类的全限定名命名的文件,然后读取文件内容,以 Key-Value 的形式加载扩展接口的实现类。
所以,我们也在模块里新建META-INF/dubbo/org.apache.dubbo.common.serialize.Serialization
文件,内容写上:
fastjson2=dubbo.extension.serialization.fastjson2.Fastjson2Serialization
这样 Dubbo 就会去加载我们的序列化实现了。
最后一步,就是让 Provider 和 Consumer 用上我们自定义的序列化器。Provider 可以在 ProtocolConfig 里指定:
ProtocolConfig protocolConfig = new ProtocolConfig("dubbo", 20880);
protocolConfig.setSerialization("fastjson2");
ServiceConfig.setProtocol(protocolConfig);
Consumer 可以在 ReferenceConfig 里设置参数来指定:
Map<String, String> parameters = new HashMap<>();
parameters.put("serialization", "fastjson2");
ReferenceConfig.setParameters(parameters);
接下来你就可以启动 Provider 和 Consumer,发起一次 RPC 调用,看看是否用的你自定义的序列化器。
Dubbo 框架设计自上而下分了十层,最底层就是序列化层,它提供了 Java 对象到字节序列互相转换的能力,以方便参数和返回值可以在网络中传输。核心是利用 Serialization 接口用来生成序列化器和反序列化器,Dubbo 官方内置了很多序列化实现,开发者也可以自己实现一个实例化器,再基于 SPI 机制方便的替换掉默认实现。