前言
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:序列化接口,用于生成序列化器和反序列化器
- ObjectOutput:对象序列化器接口
- ObjectInput:对象反序列化器接口
Serialization 是扩展接口,用于生成具体的序列化器和反序列化器,默认使用 hessian2。
@SPI("hessian2")
public interface Serialization {byte getContentTypeId();String getContentType();@AdaptiveObjectOutput serialize(URL url, OutputStream output) throws IOException;@AdaptiveObjectInput 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 要依次写入:
- version(String)Dubbo协议的版本号
- serviceName(String)服务名
- version(String)服务的版本号
- methodName(String)方法名
- parameterTypesDesc(String)参数类型描述
- args(Object…)方法参数
- attachments(Map)隐式参数
@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/6138String 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.dubbo</groupId><artifactId>dubbo-serialization-api</artifactId><version>${dubbo.version}</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.44</version></dependency>
</dependencies>
新建 Fastjson2Serialization 类实现 Serialization 接口,很简单,返回我们实现的 Fastjson2 对应的序列化器即可。
public class Fastjson2Serialization implements Serialization {@Overridepublic byte getContentTypeId() {return 22;}@Overridepublic String getContentType() {return "text/json";}@Overridepublic ObjectOutput serialize(URL url, OutputStream output) throws IOException {return new Fastjson2ObjectOutput(output);}@Overridepublic 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;}@Overridepublic 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);}}@Overridepublic 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 机制方便的替换掉默认实现。