网络

Protocol Buffer 基础教程: Java

2015-10-13  本文已影响4718人  JohnShen

原文地址:
https://developers.google.com/protocol-buffers/docs/javatutorial

这个教程为Java开发者使用protocol buffers工作提供一个基本的介绍。通过创建一个简单的示例程序,向你展示如何:

这不是一个深入介绍如何通过Java使用protocol buffers的教程。你可以通过Protocol Buffer Language Guide, the Java API Reference, the Java Generated Code Guide, and the Encoding Reference.来获取更多的参考信息。

为什么要使用Protocol Buffers?

我们将要使用的例子是一个非常简单的“地址簿”应用,这个应用可以从一个文件里读写人们的联系方式。每个在地址簿中的人都有一个名字, 一个ID, 一个email地址,和一个联系电话号码。

你是如何序列化并寻回像这样的结构化数据的呢?有这样一些方法可以解决这个问题:

Protocol buffers是灵活,高效,自动化的解决方案,来解决这个问题。通过protocol buffers,你写一个.proto的描述来描述你想要存储的数据结构。proto buffer编译器会通过这个.proto创建一个类,这个类实现了自动化的编码和将proto buffer数据的转化为高效的二进制格式。生成的类为域属性提供了getters和setters,并实现将proto buffer作为一个单元来进行读写的功能。更重要的是,proto buffer格式支持扩展格式,因此,使用这种方式的代码依旧能够读取通过旧格式编码的数据。

哪里可以找到示例代码?

示例代码是被包含在源码包中的,在名为“example”目录下面。点击下载

定义你的Protocol格式

为了创建你的地址簿应用,你将需要从一个.proto文件开始。在.proto文件中定义很简单:你为每一个想要序列化的数据结构添加一个message,然后在message中为每一个属性域指定一个name和一个type。下面是定义了你的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 name被用来作为Java的package,除非你详细指定了一个java_package,正如我们在这里是这样做的。即使你却是提供了一个java_package,你也应该仍旧定义一个正常的package以便在Protocol Buffers命名空间和非Java语言中避免命名冲突。

在包声明以后,你会看到两个Java特有的配置项:java_packagejava_outer_classnamejava_package指定你生成的类要存在于什么包下。如果你没有具体的指定,它会简单的与package给出的package name相匹配,但是这些名字通常不适合做为Java包的名字(因为它们通常不以域名开头)。java_outer_classname配置项定义了那些在这个文件中包含所有类的名称。如果你没有具体指定java_outer_classname,它将会转换文件名为驼峰式来产生。例如,“my_proto.proto”将默认使用“MyProto”做为outer class name(类的文件名)。

接下来是你的message定义。一个message只是一系列具有类型的域的集合。许多标准的简单类型可以做为可用的域类型,包括bool, int32, float, double, 和string。你同样可以向你的message中添加更多的结构,通过把其他的message类型当做域类型使用--在上面的例子中,Person message包含PhoneNumber message,而AddressBook message包含Person message。你甚至可以通过内部嵌套其它message的方式定义message,你可以看到,PhoneNumber类型被定义在Person里面。你同样可以定义enum类型,如果你希望其中的一个你的域拥有预定义列表中的一个值--在这里,你希望指定一个phone number的值是MOBILE, HOME, 或者WORK中的一个。

这里在每个元素上面的“=1”,“=2”标记,辨识唯一的“tag”,这些“tag”是域在二进制编码的时候使用的。Tag编号从1到15,需要比更大的数字少一个字节,因此,出于优化考虑你可以决定使用常用的或者重复的元素。而从16到更高的编号留给不常用的可选元素。在重复域中的每一个元素都需要重新编码tag number,所以,使用重复域对优化来说是特别好的选择。

每一个域都必须被下面之一的modifer注解:

Required是永久的。你在将域标记成required的时候需要很小心。如果在某些时候你希望停止写入或发送一个required域,它将会易出问题地将域转为一个optional域--以前的读取器将认为没有这个域的message是不完整的,并且可能无意中将它们拒绝或者丢弃。取而代之地,你应该考虑为你的buffer写应用特有的自定义校验规则。在Google的一些工程师得出了结论就是:与带来的益处相比,使用required带来的坏处会更多。他们倾向于只使用optionalrepeated。然而,这种情景并不是普遍的。

你将找到一个完整的教程关于写.proto文件--包括所有可能的域类型--在Protocol Buffer Language Guide。不要试图寻找类似于类继承机制的组件,因为protocol buffer不做这些。

编译你的Protocol Buffers

现在你有一个.proto,接下来你需要生成类,这些类是你需要读写AddressBook(当然也包括PersonPhoneNumber) message用的。为了做到这点,你需要运行proto buffer的编译器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在你指定的目标目录中。

Protocol Buffer API

让我们来看一些生成的代码,并看看编译器都为你生成了什么类和方法。如果你看看AddressBookProtos.java文件,你能看到它定义了一个叫做AddressBookProtos的类,嵌套在其中的是你在addressbook.proto中指定的每一个类。每一个类都有自己的Builder类,通过这个类你可以创建那个类的实例。你可以在下面的Builders vs. Messages找到关于builder的更多信息。

message和builder都有自动生成的为message中每个域准备的访问器方法。message只有getter方法,而builder同时有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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(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 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();

你可以看到,每一个域都有简单的JavaBeans风格的getters和setters方法。如果一个域被设置了值,同样会有getters为每一个单独的域。最后,每一个域都有一个clear方法,用来将域设置回原来的空状态。

Repeated域有一些额外的方法--一个Count方法(用来速记列表的大小),getters和setters用来通过下标,get或者set一个具体的元素。一个add方法用来向列表中追加一个新元素。一个addAll方法用来追加整个容器中的元素到列表中。

请注意这些访问器方法是如何使用驼峰式的命名,即使.proto文件使用了小写字母+下划线的方式。这种格式的转换是由protocol buffer的编译器自动完成的,以便于生成的类可以符合标准的Java风格规范。你应该总是为.proto文件中的域名称使用“小写字母+下划线”的方式;这确保了好的命名实践在所有的生成的语言里。参考style guide以了解更多好的.proto风格。
了解更多关于编译器对于任何具体的域定义会生成什么样的成员,请参见Java generated code reference

枚举和内嵌类

生成的代码中包含一个PhoneTypeJava 5 enum, 内嵌于Person

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

内嵌的类型Person.PhoneNumber被生成,正如你所期望的那样,作为一个内嵌类在Person中。

Builders vs. Messages

protocol buffer生成的所有message类都是不可变的(immutable)。一旦一个message对象被构建,它就不能被修改,就像Java的String类型一样。为了构建一个message,你必须首先构建一个builder,给任何域设置你想要设置的值,然后调用builder的build()方法。

你或许已经注意到builder的每一个修改message的方法都会返回一个新的builder。返回的对象和你调用方法时使用的其实是同一个builder。它被返回是为了方便,使你能够将若干setters组成一串,在代码中书写为一行(译者注:链式编程)。

这里是一个你如何创建一个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();

标准的Message方法

每一个message和builder类也包含一些其它的方法,这些方法使你可以检查或操作整个message,其中包括:

这些方法实现了MessageMessage.Builder接口,这些接口被所有的Java message和builder共享。更多的信息,请参见complete API documentation for Message

转化和序列化

最后,每一个protocol buffer类都有一些方法用来读写你使用protocol buffer二进制格式选择的message。这包括:

这些只是一些提供的选项来转化和序列化。再次参见Message API reference的完整列表。

Protocol Buffers和面向对象Protocol buffer类是基本的不发挥作用的数据持有者(像是C++中的结构体);它们不创建第一个类成员在一个对象模型中。如果你想要向一个生成的类中添加丰富的行为,最好的方式是用一个应用特有的类包含生成的protocol buffer类。这样做同样是一个好的方式,如果你对.proto文件的设计没有控制权的时候(这是说,如果你在从另一个项目中重用一个.proto文件)。在这种情况下,你可以用包含的类去精巧地设计一个接口使它更适合你应用特有的环境:隐藏一些数据和方法,暴露便于使用的功能,等等。你绝不应该去通过继承它们来向生成的类中添加行为。这将会破坏内部机制,并且毕竟不是面向对象的做法。

写一个Message

现在,让我们试着使用你的protocol buffer类。第一件你想让你的地址簿应用能够做的事情是向你的地址簿文件写入个人详情。为了做这个,你需要创建并安置你protocol buffer类的实例,并且接下来将它们写入到一个输出流中。

下面是一个能从一个文件中读取一个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.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();
  }
}

读取一个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.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中:

(关于这些约定有一些例外的情况,但它们极少被用到。)

如果你遵守这些规则,旧的代码将会很好地读取新的message,并且轻易地忽略任何新的域。对于旧的代码来说,被删除的optional域将有它们的默认值,并且被删除的repeated域将会为空。新的代码将会透明地读取旧的message。然而,需要牢记的是新的optional域将不会在旧的message中出现,所以你将需要具体检查它们是否被has_设置,或者提供一个可靠的默认值在你的.proto文件中,即在tag number后面写[default = value]。如果一个optional元素未被具体指定,那么取而代之地,一个具体类型的默认值将被使用:对于string,默认值是空串。对于boolean,默认值是false。对于numeric类型,默认值是0。同样记得,如果你添加了一个新的repeated域,你的新代码将不能识别它是被置空(通过新的代码)还是从来没有被设置过(通过旧的代码)。因为,没有为它提供has_标记。

高级用法

Protocol buffers有一些超过访问器和序列化的用法。请确保查看过Java API reference来看看你还能用它做些什么。

Protocol message类提供的一个关键特性是反射。你可以迭代一个message中的域,并且不用写任何与message中类型抵触的代码,来操作它们的值它们的值。一个非常有用的方法是使用反射来将protocol message转为或转自其它的编码方式,比如:XML或JSON。一个更高级的反射用法是查找相同类型的两个message的不同之处,或者开发一套“protocol message的正则表达式”,使得你可以通过写表达式来匹配确定的message内容。如果发挥你的想象力,通过应用Protocol Buffer去解决一些的问题会远超出你的预期!

反射被作为MessageMessage.Builder接口的一部分提供出来。

上一篇下一篇

猜你喜欢

热点阅读