Protocol Buffer基础:Java

        关于protocol buffers的概览,有篇博客翻译得还可以,https://www.cnblogs.com/chenyangyao/p/5422044.html,官方文档链接https://developers.google.com/protocol-buffers/docs/overview

        本文原文链接https://developers.google.com/protocol-buffers/docs/javatutorial

  本教程为Java程序员提供了一个使用protocol buffers的基本介绍。通过一个完整的创建简单示例应用的例子,我们将会给你展示如何:

  • .proto文件中定义消息(message,后文将使用英文)格式

  • 使用protocol buffer编译器

  • 使用Java版本的protocol buffer API去读写messages

本教程并不是在Java中使用protocol buffer的一个综合性的指导。如果需要更详细的参考信息,请参考protocol buffer语言指导Java API参考Java生成代码指导编码参考

 

为什么要使用Protocol Buffers?

我们将使用一个非常简单的“通讯录”应用作为示例,应用的主要功能是从文件中读取和写入人们的详细联系方式。通讯录中的每一个人都有一个名字,一个ID,一个email地址和一个联系电话。

针对上段提及的结构化数据,你将会如何去序列化和检索它们呢?这里有几个解决方法:

  • 使用Java的序列化机制。作为Java语言的一种内建功能,Java序列化其实是一种默认的方法,但是Java序列化有很多众所周知的问题(参考Effective Java, by Josh Bloch pp. 213),同时,在与C++和Python应用进行数据共享时,Java序列化并不能提供太出色的表现。

  • 你也可以构造一种点对点的方式去把数据项编码成一个单一的字符串,例如把4个int类型的数据编码成“12:3:-23:67”。这是一种简单且灵活的方法,但是这种方法需要编写一次性的编码和解析代码,同时解析代码会带来一些运行时开销。对于非常简单的数据来说,这种方法算是最好的一种选择了。

  • 将数据序列化为XML。这种方法非常具有吸引力,因为XML(某种程度上)具有良好的可读性,并且有许多语言的绑定库。如果你想与其他应用或项目共享数据,那么这种方法是一个不错的选择。然而,XML是一种空间密集型的语言,在这一点上它可谓是臭名昭著,另一方面,编码和解码XML会给应用带来巨大的性能压力。同时,导航一棵XML DOM树要比在类中导航简单字段复杂得多。

Protocol buffers是精确解决这个问题的一种灵活、高效,自动化的解决方法。在protocol buffers中,你会把你想要存储的数据结构写在一个.proto文件中。接下来,protocol buffer编译器会创建一个类,这个类以高效的二进制格式实现protocol buffer数据的自动编码和解析。生成的这个类为组成protocol buffer的字段提供了getter和setter,并负责作为一个单元来读写的protocol buffer的相关操作细节。更重要的是,protocol buffer格式支持随着时间的推移不断扩展已有格式的想法,这意味着代码仍然可以读取以旧格式编码的数据。

从哪获取示例代码

示例代码包含在源代码包中,位于“examples”文件夹下。点击这里下载

定义你的协议格式

为了创造一个属于你自己的通讯录应用,你将会以一个.proto文件开始。.proto文件中的定义很简单:为每一个你想要序列化的数据结构添加一个message,接下来为message中的每一个字段指定一个名字和类型。下面是一个定义了一些messages的.proto文件,addressbook.proto

syntax = "proto2";

package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

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

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

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
 }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

如你所见,上面代码的语法有点类似C++或Java。接下来让我们一起分析一下上述文件的每一部分并看看它到底做了什么。

这个.proto文件以一个包声明作为开头,这有助于防止在不同的项目之间可能存在的命名冲突。在Java中,包名是当作Java包来使用的,除非你显式的指定了一个java_package,就像我们在这个文件中所做的那样。不过即便你提供了一个java_package,你仍然还需要再定义一个普通的package,以避免Protocol Buffers命名空间中的名字冲突以及非Java语言中的命名冲突。

接下来,你能看到两个Java特性的选项:java_packagejava_outer_classnamejava_package指定了这个.proto文件所生成的类应该存放的Java包的位置。如果你没有显示的指定java_package,那么编译器将简单的根据包声明所给定的名称去进行匹配,但是包声明中的名称通常不是一个很规范的Java包名(因为它们通常不以域名作为开头)。java_outer_classname主要用于指定一个类名,这个类包含了上述.proto文件中声明的所有类。如果你不显式的指定java_outer_classname,编译器将把文件名转换成驼峰风格,并以此作为类名。例如,默认情况下,"my_proto.proto"将会以 "MyProto"作为outer class的名字。

接下来就到了messages的定义部分了。一个message其实就是一个包含了一组类型字段的聚合。很多基本数据类型都可以充当字段类型,包括boolint32floatdoublestring。通过把其他的message类型当作字段类型的方式,你可以实现把更复杂的结构添加进你的messages,上例中Person message就包括了PhoneNumber Messages,同时AddressBook message又包含了Person message。你甚至可以在一个message内部嵌套的定义另一个message,在Person中的定义的PhoneNumber就是这样一个例子。如果你想要你的字段拥有一个预先定义好的值,你同样也可以把enum类型引入进来,上例中电话号码的类型MOBILEHOMEWORK就说明了这一点。

每一个元素的" = 1", " = 2"标记标识了这些字段在二进制编码中的唯一“标记”。数字标记1-15在进行二进制编码时,会比更大的数字少用一个字节,所以作为一种优化的选择,你可以把那些常用的或是经常重复的元素,用上1-15这样的标记;而把标记16或是更大的数字留给那些不怎么用的可选元素。每一个需要重复的字段都将会对标记数字进行多次编码,所以重复字段是这种优化方式的一个有力候选者。

每个字段都必须使用下面修饰符中的一个来进行标注:

  • required:必须为这个字段提供一个值,否则这个message就会被认为是未初始化的。尝试去构建一个未经初始化的message将会抛出一个RuntimeException,解析一个未经初始化的message将会抛出一个IOException。除此之外,required字段和optional字段完全相同。

  • optional:字段可能有,也可能没有被赋值。如果一个可选字段没有被赋值,默认值将会被使用。对简单类型而言,你可以指定你自己特有的默认值,就像我们在上例中为电话号码的类型指定默认值那样。如果不指定的话,系统默认值将会被使用:数字类型的默认值将是0,字符串类型的默认值是空字符串,bool类型的默认值是false。对于嵌入式的类型,默认值总是message的“默认实例”或者“原型”,它们的字段都未被赋值。调用存取器去获取一个optional(或required)字段的值,如果这个字段没有被显式的赋值,那么总是会返回一个该字段的默认值。

  • repeated:这个字段可能会被重复任意多次(包括0次)。重复值将会被保存在protocol buffer中。试着把重复字段理解成动态大小的数组。

注意 Required Is Forever 你需要谨慎的决定一个字段是否需要被当做required字段。如果在某一时刻,你希望停止写入或发送一个required字段,那么把这个字段变成一个optional字段将会变得棘手:老的读取者会认为缺失这个字段的messages是不完整的,因此可能在无意中就拒绝或是丢弃这样的messages。你应该考虑为buffers编写特定于应用程序的自定义验证例程。一些来自Google的工程师们得出这样一个结论:使用required字段弊大于利,他们偏向于只使用optional和repeated字段。当然了,这个观点并不普遍。

Protocol Buffer Language Guide中,你能获取到一个完整的编写.proto文件的指导,这里面包含了所有可能的字段类型。不要去寻找类似类继承的工具,protocol buffers并不这样做。

编译你的Protocol Buffers

现在你已经搞定了.proto文件,接下来要做的事情就是生成一个用来读写通讯录messages的类了。为了达成这个目的,你需要在.proto上运行protocol buffer的编译器protoc:

  1. 如果你还没有安装这个编译器, 下载安装包并且根据README中的指引完成安装

  2. 运行编译器,指定源代码所在的文件夹(即应用源代码的存放处,如果你不显式提供,那么就默认使用当前文件夹),目标文件夹(你希望存放生成的代码的地方,通常与$SRC_DIR(源文件夹)一致),.proto文件的路径。本例中:

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

因为我们预期的输出是Java类,所以我们使用了--java_out选项--protocol buffer支持的其他语言也提供了类似的选项。

上述代码执行后,会在你指定的目标文件夹里生成com/example/tutorial/AddressBookProtos.java

Protocol Buffer API

让我们来看看部分生成的代码,同时看看编译器为你生成了哪些类和方法。如果你打开AddressBookProtos.java这个文件,你能看到文件中定义了一个名为AddressBookProtos的类,你在addressbook.proto中定义的每一个message都会以嵌套类的形式存在这个类中。每一个类都有一个自己的Builder类用来生成该类的实例。下面的部分Builders vs. Messages会提供更多关于Builder的信息。

对message中的每一个字段,messages和builders都有自动生成的存取器方法,区别在于messages只有getter方法,而builders拥有getter和setter方法。下面是Person类的一些存取器方法(为了简明,省略了具体实现):

// 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);

Person.Builder拥有同样的getter同时还加入了setter。

// 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();

显而易见,每个字段的getter和setter都有着明显的JavaBeans风格。对于单数字段而言,它们还拥有has getter方法,用以返回该字段是否有被赋值过。最后,每一个字段还有一个clear方法用于将字段的值设置为空状态(应该指的是默认值状态)。

Repeated字段有一些额外的方法--Count方法(其实是列表大小的速记法);根据下标存取列表中元素的getter和setter方法;用于将新元素添加进列表的add方法;和一个将一整个容器中的所有元素都添加进列表的addAll方法。

你可能会注意到,这些存取器方法在命名上都使用了驼峰风格,而在.proto文件中使用的是小写加下划线的命名风格。这个转换是由protocol buffer编译器自动完成的,这样做的好处在于生成的类会符合标准的Java风格。在你的.proto文件中,你应该使用小写加下划线的命名风格,这样做是为了保证对所有支持的语言来说,都是一个好的命名实践。可以参考style guide来获得更多关于.proto文件风格的细节。

如果想获得更多有关protocol buffer编译器为任何特定字段定义生成的成员的详细信息,参考Java generated code reference

枚举和嵌套类

生成的代码中包含了一个Java 5中的枚举类PhoneType,嵌套在Person中:

public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
 ;
 ...
}

嵌套类型Person.PhoneNumber也如你期望的那般生成了一个嵌套在Person中的类

Builders vs. Messages

由protocol buffer编译器生成的message类都是不可变的,一旦某个message对象被构造,它就不能再被修改了,就像Java中的String那样。构造一个message之前,你必须首先构造一个builder,为你想要赋值的字段赋上你想要的值,然后调用builder的build方法。

你可能注意到,builder中涉及到修改message的方法最后都会返回一个builder,返回的这个对象其实就是当前的builder,返回当前builder是为了方法的链式调用。

下面是一个创建Person实例的示例:

Person john =
  Person.newBuilder()
   .setId(1234)
   .setName("John Doe")
   .setEmail("jdoe@example.com")
   .addPhones(
      Person.PhoneNumber.newBuilder()
       .setNumber("555-4321")
       .setType(Person.PhoneType.HOME))
   .build();

标准Message方法

每一个message和builder也包含了一些其他的方法,用来检查或者操作整个message,包括如下方法:

  • isInitialized():检查所有的required字段是否都已被赋值

  • toString():返回一个人类可读的message的表示,在debug的时候尤其有用

  • mergeFrom(Message other):(仅builder)合并另一个message的内容到当前message,单数形式的标量字段将会被覆盖;组合字段将会被合并;repeated字段将会被级联

  • clear():(仅builder)将所有字段都变成其空状态

    这些方法实现了Message和Message.Builder接口,更多信息,请参考complete API documentation for Message

解析与序列化

最后,每个protocol buffer类都有使用protocol buffer二进制格式编写的读取所选类型的消息的方法。包括:

  • byte[] toByteArray();:序列化一个message,返回一个包含这个message原始字节的字节数组

  • static Person parseFrom(byte[] data);:从给定的字节数组中解析出一个message

  • void writeTo(OutputStream output);:序列化一个message并把它写入到一个OutputStream

  • static Person parseFrom(InputStream input);:从一个InputStream读取并解析一个message

上述只是部分序列化和解析message的方法,还是那句话,想要获得完整的方法列表,请点击Message API reference

注意

Protocol Buffers and O-O Design

Protocol buffer类可以视为哑数据的持有者(类似C语言中的结构体),它们并不能很好的在对象模型中作为良好的一等公民。如果你想为生成类添加更丰富的行为,最好的方法是把生成的protocol buffer类封装到一个特定的应用类中。封装protocol buffers同样也是一个不错的想法,如果你不需要控制.proto文件的设计的话(比如说你正在从另一个项目中重用.proto文件)。在这种情况下,你可以使用封装好的类去制作一个更适合你的应用的接口:隐藏一些数据和方法,暴露便利的功能等。永远不要通过继承生成类的方式来为它们添加行为,这种做法会破坏内部机制,并且也不是一个好的面向对象的实践。

写一个Message

接下来让我们开始使用为你生成的protocol buffer类。第一件事就是在你的通讯录应用中写入一个个人的通讯详情,首先,我们要创建并填充一个protocol buffer类的实例,然后把它们写入到output stream中。

下面给出的代码完成了这样的功能:从文件中读取一个AddressBook,然后基于用户的输入在这个AddressBook中添加一个新的Person,最后把这个更新过的AddressBook再写会文件中。直接调用或是由编译器生成的引用代码将会被高亮。

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
  // This function fills in a Person message based on user input.
  static Person PromptForAddress(BufferedReader stdin,
                                 PrintStream stdout) throws IOException {
    Person.Builder person = Person.newBuilder();

    stdout.print("Enter person ID: ");
    person.setId(Integer.valueOf(stdin.readLine()));

    stdout.print("Enter name: ");
    person.setName(stdin.readLine());

    stdout.print("Enter email address (blank for none): ");
    String email = stdin.readLine();
    if (email.length() > 0) {
      person.setEmail(email);
   }

    while (true) {
      stdout.print("Enter a phone number (or leave blank to finish): ");
      String number = stdin.readLine();
      if (number.length() == 0) {
        break;
     }

      Person.PhoneNumber.Builder phoneNumber =
        Person.PhoneNumber.newBuilder().setNumber(number);

      stdout.print("Is this a mobile, home, or work phone? ");
      String type = stdin.readLine();
      if (type.equals("mobile")) {
        phoneNumber.setType(Person.PhoneType.MOBILE);
     } else if (type.equals("home")) {
        phoneNumber.setType(Person.PhoneType.HOME);
     } else if (type.equals("work")) {
        phoneNumber.setType(Person.PhoneType.WORK);
     } else {
        stdout.println("Unknown phone type. Using default.");
     }

      person.addPhones(phoneNumber);
   }

    return person.build();
 }

  // Main function: Reads the entire address book from a file,
  //   adds one person based on user input, then writes it back out to the same
  //   file.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
      System.exit(-1);
   }

    AddressBook.Builder addressBook = AddressBook.newBuilder();

    // Read the existing address book.
    try {
      addressBook.mergeFrom(new FileInputStream(args[0]));
   } catch (FileNotFoundException e) {
      System.out.println(args[0] + ": File not found. Creating a new file.");
   }

    // Add an address.
    addressBook.addPeople(
      PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
                       System.out));

    // Write the new address book back to disk.
    FileOutputStream output = new FileOutputStream(args[0]);
    addressBook.build().writeTo(output);
    output.close();
 }
}

读取一个Message

当然,如果你不能从中得到任何信息,通讯录就没有多大用处了!下面的代码读取了上文代码中所创建的文件,并将其中所有的信息打印出来。

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

class ListPeople {
  // Iterates though all people in the AddressBook and prints info about them.
  static void Print(AddressBook addressBook) {
    for (Person person: addressBook.getPeopleList()) {
      System.out.println("Person ID: " + person.getId());
      System.out.println(" Name: " + person.getName());
      if (person.hasEmail()) {
        System.out.println(" E-mail address: " + person.getEmail());
     }

      for (Person.PhoneNumber phoneNumber : person.getPhonesList()) {
        switch (phoneNumber.getType()) {
          case MOBILE:
            System.out.print(" Mobile phone #: ");
            break;
          case HOME:
            System.out.print(" Home phone #: ");
            break;
          case WORK:
            System.out.print(" Work phone #: ");
            break;
       }
        System.out.println(phoneNumber.getNumber());
     }
   }
 }

  // Main function: Reads the entire address book from a file and prints all
  //   the information inside.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
      System.exit(-1);
   }

    // Read the existing address book.
    AddressBook addressBook =
      AddressBook.parseFrom(new FileInputStream(args[0]));

    Print(addressBook);
 }
}

扩展Protocol Buffer

在你发布使用了protocol buffer的代码后,毫无疑问你迟早都会想要去改进protocol buffer的定义。如果你想要经过你改进的新的protocol buffer向后兼容,而旧的protocol buffer能向前兼容(你肯定会想要这样做的),那么接下来有几条规则你需要遵守。在新版本的protocol buffer中:

  • 不能更改任何现有字段的标记数字

  • 不能添加或删除任何required字段

  • 可以删除optional字段或repeated字段

  • 可以添加新的optional字段和repeated字段,但是记住一定要使用新的标记数字(即标记数字在当前的protocol buffer中从未被使用过,即使被已删除的字段使用过也不行)

也会有一些特殊情况,但是这些情况很少会遇到。

如果你遵循了这些规则,那么旧代码将愉快地读取新消息并简单地忽略任何新字段。对于旧代码而言,在新代码中被删除的optional字段会被处理成默认值,而repeated字段会被设置为空。新代码也将透明的读取旧消息。尽管如此,你仍然要记住,在新代码中添加的optional字段是不会在旧的messages中表示出来的,所以你需要显式的调用has_方法检查它们是否有被赋值,或者是在你的.proto文件中,是否有在标记数字后使用[default = value]为它们指定了一个合理的默认值。如果没有为optional字段指定一个默认值,那么类型相关的默认值就会被用来充当这个字段的默认值:对字符串类型而言,默认值就是空字符串;对bool类型而言,默认值就是false;对数字类型而言,默认值就是0。同时也要注意,如果你添加了一个新的repeated字段,你的新代码将没法分辨这个字段是空的(从新代码的角度)还是说从来没有被赋值过(从旧代码的角度),造成这种局面的原因是repeated字段没有has_方法。

高级用法

protocol buffer不仅仅是简单的存取器和序列化,你可以多看看 Java API reference,然后思考一下你还能做些什么。

protocol message类提供的一个关键特性是反射。你可以遍历message的字段并在不针对任何特定message类型编写代码的情况下处理它们的值。反射的一大用处是用于protocol messages和其他编码格式之间的相互转换,比如XML和Json。反射的一种更深入的用法可能是用于比对两个相同类型的message之间的差异,或者换一种说法,反射可以用于创建一种针对protocol messages的“正则表达式”,使用这种“正则表达式”,可以编写与某些消息内容匹配的表达式。发散你的思维,说不定有可能将Protocol Buffers应用到你原本没有想到的更大范围的领域!

反射是 Message 和Message.Builder接口中的一部分。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值