Dubbo分层设计之Serialize层

前言

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();
  
  @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 对象了,为什么不只定义一个writeObjectreadObject 方法直接读写整个 Request/Response 对象呢,为什么还要单独提供读写 int、boolean、String 等基本类型的方法???
这是因为,Dubbo 协议的报文包含两部分:协议头部、请求体。序列化只管请求体部分的数据,对于协议头部它是不关心的。而 Request/Response 对象属性是同时包含这两部分的数据的,如果直接序列化整个对象传输,会造成带宽的浪费,传输冗余的数据。
所以 Dubbo 的编解码器DubboCodec 不是一股脑直接读写整个大对象的,而是按照顺序写多个对象,对端再按照相同的顺序读出来就好了。

Dubbo 对 Request 编码的方法是DubboCodec#encodeRequestData ,对于一次 Dubbo 协议的 RPC 调用,Consumer 要依次写入:

  1. version(String)Dubbo协议的版本号
  2. serviceName(String)服务名
  3. version(String)服务的版本号
  4. methodName(String)方法名
  5. parameterTypesDesc(String)参数类型描述
  6. args(Object…)方法参数
  7. 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/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 机制方便的替换掉默认实现。

你可能感兴趣的:(Dubbo,dubbo)