现在的位置: 首页 > 综合 > 正文

Protocol Buffer介绍(Java)

2013年08月26日 ⁄ 综合 ⁄ 共 12090字 ⁄ 字号 评论关闭

本文译自:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN

ProtocolBuffer基础:Java

本指南提供了使用ProtocolBuffer工作的Java编程方法。全文通过一个简单的示例,向你介绍在Java中使用ProtocolBuffer的方法:

1.如何在.proto文件中定义消息格式;

2.如何使用ProtocolBuffercreates编译器;

3.如何使用JavaProtocol BufferAPI来读写消息。

本文不是在Java中使用ProtocolBuffer的完整指南,更详细的信息请参照以下资料:

Protocol-buffers语言

JavaAPI参考

生成Java代码指南

编码参考

为什么使用ProtocolBuffer

我们使用了一个非常简单的“地址本”应用的例子,这个应用能够从一个文件中读写个人的联系方式信息。在地址本中每个人都有以下信息:姓名、ID、邮件地址、电话号码。

像这样的结构化数据应该如何系列化和恢复呢?以下几种方法能够解决这个问题:

1.使用Java系列化。因为它是内置在编程语言中的,所以是默认的方法,但是由于众所周知的主机问题,并且如果需要在使用不同编程语言(如C++Python)编写应用程序之间共享数据,这种方式也不会很好的工作。

2.使用特殊的方式把数据项编码到一个单独的字符串中,如把4个整数编码成“123-2367”。尽管它需要编写一次性的编码和解码代码,但是这种方法简单而灵活,而且运行时解析成本很小。这种方法对于简单数据是最好的。

3.把数据系列化到XML。因为XML是可人类可读的,并且很多编程语言都有对应的功能类库,所以这种方法非常受欢迎。如果你想要跟其他应用程序/项目共享数据,那么这种方法是一个非常好的选择。但是,众所周知,XML是空间密集性的,并且编解码会严重影响应用程序的性能。此外,XMLDOM树导航也比一般的类中的字段导航要复杂的多。

ProtocolBuffer是完全解决这个问题的灵活、高效的自动化解决方案。使用ProtocolBuffer,要先编写一个.proto文件,用这个文件来描述你希望保存的数据结构。然后用ProtocolBuffer编译器创建一个类,这个类用高效的二进制的格式实现了ProtocolBuffer数据的自动编解码。生成的类提供了组成ProtocolBuffer字段的gettersetter方法,以及提供了负责读写一个ProtocolBuffer单位的方法。重要的是,ProtocolBuffer格式支持向后的兼容性,新的代码依然可以读取用旧格式编码的数据。

什么地方可以找到示例代码

示例代码的源代码包,可以直接从这儿下载。

定义协议格式

要创建你的地址本应用程序,需要从编写.proto文件开始。.proto文件的定义很简单:你要在每个想要系列化的数据结构前添加一个message关键字,然后指定消息中每个字段的名称和类型。以下就是你要定义的.proto文件,addressbook.proto:

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 phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

就像你看到的,语法与C++Java非常类似,接下来让我们检查一下文件的每个部分,并看一下它们都做了些什么。

.proto文件开始是包声明,它有助于防止不同项目间的命名冲突。除非你明确的指定了java_package

关键字,否则,该包名会被用于生成的Java类文件的包名。即使提供了java_package,依然应该定义一个普通的package,以避免跟ProtocolBuffer命名空间以及非Java语言中的命名冲突。

在包声明之后,有两个可选的Java规范:java_packagejava_outer_classnamejava_package指定要生成的Java类的包名。如果没有明确的指定这个关键字,它会简单的用package关键字的声明来作为包名,但是这些名称通常不适合做Java的包名(因为它们通常不是用域名开头的)。java_outer_classname可选项定义了这个文件中所包含的所有类的类名。如果没有明确的给出java_outer_classname定义,它会把文件名转换成驼峰样式的类名。如,“my_proto.proto”文件,默认的情况下会使用MyProto作为外部的类名。

接下来是消息定义,一个消息包含了一组类型字段。很多标准的简单数据类型都可以作为有效的字段类型,包括:boolint32floatdoublestring。还可以是其他消息类型作为字段类型---在上面的示例中,Person消息包含了PhoneNumber消息,而AddressBook消息又包含了Person消息。甚至还可以定嵌套在其他消息内的消息类型---如,PhoneNumber类型就被定义在Person内。如果想要字段有一个预定义的值列表,也可以定enum类型---上例中电话号码能够指定MOBILEHOMEWORK三种类型之一。

每个字段后标记的“=1”、“=2”,是在二进制编码时使用的每个字段的唯一标识。在编码时,数字115要比大于它们的数字少一个字节,因此,作为一个优化选项,可以把115的数字用于常用的或重复性的元素。大于等于16的数字尽可能的用于那些不常用的可选元素。在重复字段中的每个元素都需要预定义一个标记数字,因此在重复性字段中使用这种优化是良好的选择。

每个字段必须用以下修饰符之一来进行标注:

1.required:用这个修饰符来标注的字段必须给该字段提供一个值,否则该消息会被认为未被初始化。尝试构建一个未被初始化的消息会抛出一个RuntimeException异常。解析未被初始化的消息时,会抛出一个IOException异常。其他方面,该类型字段的行为与可选类型字段完全一样;

2.optional:用这个修饰符来标注的字段可以设定值,也可以不设定值。如果可选字段的。值没有设定,那么就会使用一个默认的值。对于简单类型,能够像上例中指定电话号码的type那样,指定一个默认值。否则,系统使用的默认值如下:数字类型是0、字符串类型是空字符串、布尔值是false。对于内嵌的消息,默认值始终是“默认的实例“或”消息的“原型”,其中没有字段设置。调用没有明确设置值的字段的获取值的访问器的时候,会始终返回字段的默认值。

3.repeated:用这个修饰符来标注的字段可以被重复指定的数字的次数(包括0)。重复值的顺序会被保留在ProtocolBuffer中。重复字段跟动态数组很像。

对于标记为required的字段要始终小心。如果在某些时候,你希望终止写入或发送一个required类型的字段,那么在把该字段改变成可选字段时,就会发生问题---旧的版本会认为没有这个字段的消息是不完整的,并且会因此而拒绝或删除它们。因此应该考虑使用编写应用程序规范来定制Buffer的验证规则来代替。Google的一些工程师认为使用required,弊大于利,他们建议只使用optionalrepeqted。但实际上是行不通的。

ProtocolBuffer语言指南中,你会找到完成.proto文件编写指南---包括所有可能的字段类型。不要寻求类的继承性,ProtocolBuffer是不支持的。

编译ProtocolBuffer

现在有一个.proto文件了,接下来要做的就是生成一个读写AddressBook(包括PersonPhoneNumber)消息的类。运行ProtocolBuffer编译器protoc来生成与.proto文件相关的类。

1.如果你没有安装编译器,需要下载编译器包,并按着README文件中的指示来做。

2.运行编译器,指定源目录(你的应用程序的源代码所在的目录---如果没有提供这个值,则使用当前目录)、目的目录(生成代码存放的目录,经常使用与环境变量$SRC_DIR相同的目录),以及.proto文件所在的路径,如:

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

因为你想要Java类,所以要使用—java_out选项,其他支持的语言也提供了类似的选项。

在指定的目标目录中生成com/example/tutorial/AddressBookProtos.java文件。

ProtocolBuffer API

让我们来看一下生成的代码,并看一下编译器都为你创建了那些类和方法。如果你在看AddressBookProtos.java文件,你能够看到它定义了一个叫做AddressBookProtos的类,在addressbook.proto文件中指定的每个消息都嵌套在这个类中。每个类都有它们自己的Builder类,你能够使用这个类来创建对应的类的实例。在下文的Buildersvs.
Messages
章节中,你会找到更多的有关Builder的信息。

MessageBuilder会给消息的每个字段都生成访问方法。Message仅有get方法,而Builder同时拥有getset方法。以下是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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
同时,Person.Buildergetset方法:
// 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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();
正如你看到的,每个字段都有简单的JavaBean样式的的getset方法。对于每个有get方法的字段,如果该字段被设置,那么对应的has方法会返回ture。最后,每个字段还有一个clear方法,它会清除对应字段的设置,让它们回退到空的状态。
重复性字段会有一些额外的方法---Count方法(它会返回列表的尺寸)、通过索引指定获取或设定列表元素的getset方法、往列表中添加新元素的add方法、以及把装有完整元素的容器放入列表中。
注意,这些访问方法都使用驼峰式命名,即使是使用小写字母和下划线的.proto文件名。这些变换都是由Protocol Buffer编译器自动完成的,因此生成的类也符合标准的Java样式协议。在你的.proto文件中,应该始终使用小写字母和下划线的字段命名,这样就会在所有的生成的编程语言中具有良好的命名实践。更多的良好的.proto样式,请看样式指南
对于那些特殊的字段定义,Protocol编译器生成的成员相关的更多更准确的信息,请看“Java生成代码参照”。
枚举和嵌套类
在嵌套的Person类的生成代码中包含了Java5中的枚举类型PhoneType
public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
  ;
  ...
}
正如你所期待的,作为Person的嵌套类,生成了Person.PhoneNumber类型。
Builders vs. Messages
这些有Protocol Buffer编译器生成的消息类都是不可变的。一旦消息对象被构建了,它就不能被编辑了,就像JavaString。要构建一个消息对象,首先必须构建一个Builder,把你选择的值设置给对应的字段,然后调用build()方法。
你可能已经注意到,每个编辑消息的builder方法都会返回另外一个Builder对象,返回的Builder对象实际上与你调用的那个方法的Builder对象相同。这主要是为了能够在一行中编写set方法提供方便。
以下是创建Person实例的例子:
Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();
标准的消息方法
每个消息和构建器类还包含了一些其他的方法,这些方法会帮助你检查或维护整个消息,这些方法包括:
1.isInitialized():检查所有的required字段是否都被设置了。
2.toString():返回一个可读的消息描述,对于调试特别有用。
3.mergeFrom(Message other):(只有构建器有这个方法),它会把other参数中的内容,用重写和串联的方式合并到本消息中。
Clear():(只有构建器才有这个方法),清除所有字段的值,让它们返回到空的状态。
这些方法实现的MessageMessage.Builder接口,会被所有的Java消息和构建器共享。更多信息,请看Message的完成API文档
解析和系列化
最后,每个Protocol Buffer类都有一些使用二进制来读写你选择的类型的方法,包括:
1.byte[] toByteArray():系列化消息,并返回包含原始字节的字节数组。
2.static Person parseFrom(byte[] data):从给定的字节数组中解析消息。
3.void writeTo(OutputStream output):系列化一个消息,并把该消息写入一个OutputStream对象中。
4.static Person parseFrom(InputStream input):InputStream对象中读取和解析一个消息。
对于解析和系列化,这些方法是成对使用的。完整的API列表请看“Message API参考
Protocol Buffer和面向对象的设计:Protocol Buffer类是基本的数据持有者(有点类似C++中的结构体);在对象模型中,它们不是良好的一等类公民。如果你想要给生成的类添加丰富的行为,最好的做法是在特定的应用程序类中封装生成的Protocol Buffer类。如果在.proto文件的设计上没有控制,那么封装Protocol Buffer类也是个不错的主意(比方说,你要重用另一个项目中一个Protocol Buffer类)。在这种情况下,你能够包装类来构建一个适应你的应用程序环境的更好的接口:如隐藏一些数据和方法、暴露一些方便的功能,等等。你不应该通过继承给这些生成的类添加行为方法,这样做会终端内部机制,而且也不是良好的面向对象的实践。
编写一个消息
现在,让我们来尝试使用这些Protocol Buffer类。首先,你希望你的地址本应用程序能够把个人详细信息写入地址本文件。要完成这件事情,你需要创建并初始化Protocol Buffer类的实例,然后把它们写入一个输出流中。
以下是一段从文件中读取AddressBook的程序,它会基于用户的输入把一个新的Person对象添加到AddressBook对象中,并这个新的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.addPhone(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.addPerson(
      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();
  }
}
读取一个消息
当然,如果不能够从输出的文件中获取任何信息,那么这个地址本就毫无用处。下面的例子演示了如何从上例创建的文件中读取信息,并把所有的信息都打印出来:
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.getPersonList()) {
      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.getPhoneList()) {
        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的定义。如果想要新的Buffer类保持向后的兼容性,旧的Buffer保持向前的兼容性---几乎可以确定你是希望这样的。以下是你的新的Protocol Buffer版本要遵循的一些规则:
1.一定不要改变既存的标记数字;
2.不要添加或删除任何required类型的字段;
3.可以删除可选的或重复类型的字段;
4.可以添加新的可选的或重复类型的字段,必须使用新的标记数字(即,在该Protocol Buffer中没有被使用过的(即使是被删除的字段也不曾使用过)标记数字)。
(除了这些规则之外,还有一些其他的规则,但是它们很少使用)
如果你遵循了这些规则,旧的代码将会很好的读取新的消息,并且只是简单忽略了新的字段。对于旧代码,被删除的可选字段会简单的使用它们的默认值,被删除的重复性字段会被设置为空。新的代码也会透明的读取旧的消息。但是,要记住,新的可选字段不会出现在旧的消息里,因此你既可以明确的使用has_方法来检查它们是否被设置,也可以在.proto文件中在标记数字之后,用[default = value]来提供一个合理的默认值。对于没有指定默认值的可选元素,以下是特定类型使用的默认值:字符串类型,默认值是空字符串;布尔类型,默认值是false;数字类型,默认值是0。还要注意的是,如果你添加了一个新的重复性字段,因为没有给它has_标记,所以你的新代码不能被告知该字段是否是空的还是没有被设置。
高级用法
Protocol Buffer消息提供的一个关键特征就是反射。你能够迭代消息的字段,不用编写任何代码就可以维护任何指定的消息类型的值。使用反射的一个非常有用的方法就是把其他的编码格式转换成Protocol Buffer消息,如XML消息或JSON消息。反射的更高级的用途是查找两个相同类型消息直接的差异,或者是开发一种针对Protocol Buffer消息的正则表达式,在这个表达式中,你能够编写跟确定消息内容匹配的表达式。如果发挥你的想象力,Protocol Buffer的应用范围会比你的初始期望值还要高。
反射是作为MessageMessage.Builder的接口部分来提供的。

抱歉!评论已关闭.