关于java:在java程序中使用protobuf

26次阅读

共计 5900 个字符,预计需要花费 15 分钟才能阅读完成。

简介

Protocol Buffer 是 google 出品的一种对象序列化的形式,它的体积小传输快,深得大家的青睐。protobuf 是一种平台无关和语言无关的协定,通过 protobuf 的定义文件,能够轻松的将其转换成多种语言的实现,十分不便。

明天将会给大家介绍一下,protobuf 的根本应用和同 java 联合的具体案例。

为什么应用 protobuf

咱们晓得数据在网络传输中是以二进制进行的,个别咱们应用字节 byte 来示意,一个 byte 是 8bits,如果要在网络上中传输对象,个别须要将对象序列化,序列化的目标就是将对象转换成 byte 数组在网络中传输,当接管方接管到 byte 数组之后,再对 byte 数组进行反序列化,最终转换成 java 中的对象。

那么将 java 对象序列化可能会有如下几种办法:

  1. 应用 JDK 自带的对象序列化,然而 JDK 自带的序列化自身存在一些问题,并且这种序列化伎俩只适宜在 java 程序之间进行传输,如果是非 java 程序,比方 PHP 或者 GO,那么序列化就不通用了。
  2. 你还能够自定义序列化协定,这种形式的灵便水平比拟高,然而不够通用,并且实现起来也比较复杂,很可能呈现意想不到的问题。
  3. 将数据转换成为 XML 或者 JSON 进行传输。XML 和 JSON 的益处在于他们都有能够辨别对象的起始符号,通过判断这些符号的地位就能够读取到残缺的对象。然而不论是 XML 还是 JSON 的毛病都是转换成的数据比拟大。在反序列化的时候对资源的耗费也比拟多。

所以咱们须要一种新的序列化的办法,这就是 protobuf,它是一种灵便、高效、自动化的解决方案。

通过编写一个.proto 的数据结构定义文件,而后调用 protobuf 的编译器,就会生成对应的类,该类以高效的二进制格局实现 protobuf 数据的自动编码和解析。生成的类为定义文件中的数据字段提供了 getter 和 setter 办法,并提供了读写的解决细节。重要的是,protobuf 能够向前兼容,也就是说老的二进制代码也能够应用最新的协定进行读取。

定义.proto 文件

.proto 文件中定义的是你将要序列化的音讯对象。咱们来一个最根本的 student.proto 文件,这个文件定义了 student 这个对象中最根本的属性。

先看一个比较简单的.proto 文件:

syntax = "proto3";

package com.flydean;

option java_multiple_files = true;
option java_package = "com.flydean.tutorial.protos";
option java_outer_classname = "StudentListProtos";

message Student {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
  }

  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}

message StudentList {repeated Student student = 1;}

第一行定义的是 protobuf 中应用的 syntax 协定,默认状况下是 proto2,因为目前最新的协定是 proto3,所以这里咱们应用 proto3 作为例子。

而后咱们定义了所在的 package,这个 package 是指编译的时候生成文件的包。这是一个命名空间,尽管咱们在前面定义了 java_package,然而为了和非 java 语言中的协定相冲突,所以定义 package 还是十分有必要的。

而后是三个专门给 java 程序应用的 option。java_multiple_files, java_package,和 java_outer_classname.

其中 java_multiple_files 指编译过后 java 文件的个数,如果是 true,那么将会一个 java 对象一个类,如果是 false,那么定义的 java 对象将会被蕴含在同一个文件中。

java_package 指定生成的类应该应用的 Java 包名称。如果没有明确的指定,则会应用之前定义的 package 的值。

java_outer_classname 选项定义将示意此文件的包装类的类名。如果没有给 java_outer_classname 赋值,它将通过将文件名转换为大写驼峰来生成。例如,默认状况下,“student.proto”将应用 ”Student” 作为包装类名称。

接下来的局部是音讯的定义,对于简略类型来说能够应用 bool, int32, float, double,和 string 来定义字段的类型。

上例中咱们还应用了简单的组合属性,和嵌套类型。还定义了一个枚举类。

下面咱们为每个属性值调配了 ID,这个 ID 是二进制编码中应用的惟一“标签”。因为在 protobuf 中标记数字 1 -15 比 16 以上的标记数字占用的字节空间要更少,因而作为一种优化,通常将 1 -15 这些标记用于罕用或反复的元素,而将标记 16 和更高的标记用于不太罕用的可选元素。

而后再来看看字段的修饰符,有三个修饰符别离是 optional,repeated 和 required。

optional 示意该字段是可选的,能够设置也能够不设置,如果没有设置,则会使应用默认值,对于简略类型来说,咱们能够自定义默认值,如果不自定义,就会应用零碎的默认值。对于零碎的默认值来说,数字为 0,字符串为空字符串,布尔值为 false。

repeated 示意该字段是能够反复的,这种反复实际上就是一种数组的构造。

required 示意该字段是必须的,如果该字段没有值,那么该字段将会被认为是没有初始化,尝试构建未初始化的音讯将抛出 RuntimeException,解析未初始化的音讯将抛出 IOException。

留神,在 Proto3 中不反对 required 字段。

编译协定文件

定义好 proto 文件之后,就能够应用 protoc 命令对其进行编译了。

protoc 是 protobuf 提供的编译器,个别状况下,能够从 github 的 release 库中间接下载即可。如果你不想间接下载,或者官网提供的库中并没有你须要的版本,则能够应用源代码间接进行编译。

protoc 的应用的命令如下:

protoc --experimental_allow_proto3_optional -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/student.proto

如果编译 proto3, 则须要增加 –experimental_allow_proto3_optional 选项。

咱们运行一下下面的代码。会发现在 com.flydean.tutorial.protos 包外面生成了 5 个文件。别离是:

Student.java              
StudentList.java          
StudentListOrBuilder.java 
StudentListProtos.java    
StudentOrBuilder.java

其中 StudentListOrBuilder 和 StudentOrBuilder 是两个接口,Student 和 StudentList 是这两个类的实现。

详解生成的文件

在 proto 文件中,咱们次要定义了两个类 Student 和 StudentList, 他们中定义了一个外部类 Builder,以 Student 为例,看下这个两个类的定义:

public final class Student extends
    com.google.protobuf.GeneratedMessageV3 implements
    StudentOrBuilder

  public static final class Builder extends
      com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
      com.flydean.tutorial.protos.StudentOrBuilder

能够看到他们实现的接口都是一样的,示意他们可能提供了雷同的性能。实际上 Builder 是对音讯的一个封装器,所有对 Student 的操作都能够由 Builder 来实现。

对于 Student 中的字段来说,Student 类只有这些字段的 get 办法,而 Builder 中同时有 get 和 set 办法。

对于 Student 来说,对于字段的办法有:

// required string name = 1;
public boolean hasName();
public String getName();

// required int32 id = 2;
public boolean hasId();
public int getId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();

// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);

对于 Builder 来说,每个属性多了两个办法:

// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();

// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();

// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();

多出的两个办法是 set 和 clear 办法。clear 是清空字段的内容,让其变回初始状态。

咱们还定义了一个枚举类 PhoneType:

  public enum PhoneType
      implements com.google.protobuf.ProtocolMessageEnum

这个类的实现和一般的枚举类没太大区别。

Builders 和 Messages

如上一节所示,Message 对应的类只有 get 和 has 办法,所以它是不能够变的,音讯对象一旦被结构,就不能被批改。要构建音讯,必须首先构建一个构建器,将要设置的任何字段设置为你抉择的值,而后调用构建器的 build() 办法。

每次调用 Builder 的办法都会返回一个新的 Builder,当然这个返回的 Builder 和原来的 Builder 是同一个,返回 Builder 只是为了不便进行代码的连写。

上面的代码是如何创立一个 Student 实例:

        Student xiaoming =
                Student.newBuilder()
                        .setId(1234)
                        .setName("小明")
                        .setEmail("flydean@163.com")
                        .addPhones(Student.PhoneNumber.newBuilder()
                                        .setNumber("010-1234567")
                                        .setType(Student.PhoneType.HOME))
                        .build();

Student 中提供了一些罕用的办法,如 isInitialized() 检测是否所有必须的字段都设置结束。toString() 将对象转换成为字符串。应用它的 Builder 还能够调用 clear() 用来革除已设置的状态,mergeFrom(Message other) 用来对对象进行合并。

序列化和反序列化

生成的对象中提供了序列化和反序列化办法,咱们只须要在须要的时候对其进行调用即可:

  • byte[] toByteArray();: 序列化音讯并返回一个蕴含其原始字节的字节数组。
  • static Person parseFrom(byte[] data);: 从给定的字节数组中解析一条音讯。
  • void writeTo(OutputStream output);: 序列化音讯并将其写入 OutputStream.
  • static Person parseFrom(InputStream input);: 从一个音讯中读取并解析音讯 InputStream.

通过应用下面的办法,能够很不便的将对象进行序列化和反序列化。

协定扩大

咱们在定义好 proto 之后,如果后续还心愿对其进行批改,那么咱们心愿新的协定对历史数据是兼容的。那么咱们须要思考上面几点:

  1. 不能更改现有字段的 ID 编号。
  2. 不能增加和删除任何必填字段。
  3. 能够 删除可选或反复的字段。
  4. 能够 增加新的可选字段或反复字段,但您必须应用新的 ID 编号。

总结

好了,protocol buf 的根本用法就介绍到这里,下一篇文章咱们会更加具体的介绍 proto 协定的具体内容,敬请期待。

本文的例子能够参考:learn-java-base-9-to-20

本文已收录于 http://www.flydean.com/01-protocolbuf-guide/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

正文完
 0