protobuf系列-protobuf基础教程:Java

这篇文章主要对protocol buffer做基本介绍(针对java开发人员),我门通过一个简单的例子来学习,其中主要包含三块内容:

  1. 在.proto文件中定义一个消息格式
  2. 使用protobuf编译器
  3. 使用java protocol buffer API读写消息

为什么使用Protocol Buffers

在本文中我们要使用的例子是一个"address book" 应用,主要是从文件中读写联系人信息,每条联系人信息包含:名称、ID、邮箱、手机号。
如何序列化和检索这些结构化数据呢?我们一般有下面几种方式来处理:

  1. 使用Java Serialization。这是java语言中已经集成的默认方法。但是这种处理方式有很多问题( 参考Josh Bloch写的Effective Java pp. 213),并且如果是c++或者python等其他语言写的应用无法很好的共享这种方式序列化的数据,
  2. 自定义数据格式,例如:“12:3:-23:67”。这是一种简单灵活的方式,但是需要写编解码的代码,并且在运行时解析会有一定的消耗。这种方式适合非常简单数据格式。
  3. 使用XML,这种方式非常适合阅读,并且很多语言都有对xml支持库,这是一种很好的在不同语言之间分享数据的一种方式。但是,xml会占用大量的空间,并且在编解码时会带来很大的性能损耗。XML DOM 树查找方式也比普通类简单字段查找更加复杂。

Protocol buffers是一种灵活、高效、自动化的解决方案,可以解决上述方式中存在的问题。使用protocol buffers,需要将想要存储的数据结构写入到.proto文件中。protocol buffer编译器会根据.proto文件生成一个类,此类已经通过高效的二进制格式实现编码解析数据格式,还包含.proto中定义的字段的getter和setter方法,其中的读写细节会在类中自动处理。更重要的是,它还支持以某种方式扩展协议之后仍然可以读写旧格式的数据。

定义Protocol格式

下面开始我们的demo,首先我们先创建一个.proto格式的文件,文件中定义的内容也很简单:为每一个数据结构定义一个message,然后在message中指定属性的名称和类型。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;
}

让我们来分析下上面的代码,从上面代码中可以看到proto的语法格式跟java或c++很像。

首先,我们自定package声明,用来防止在不同的工程下名称冲突问题。默认情况下,此处package会用作java的package,当然也可以配置java_package来指定生成java类的包名,上述代码中是通过java_package来指定包名。其中要注意的是,即使我们指定了java_package,我们也要指定package,这样来避免在pb或者生成其他语言时导致的名称冲突问题。

接下来是两个java特别指定:java_package和java_outer_classname,其中java_package刚才已经说过,不过要注意的是,一般情况下java包名是按照域名来定义的,跟pacakge定义的不太匹配。java_outer_classname是用来指定生成的java类的名称,如果没有指定java_outer_classname,编译器会根据文件名称按照驼峰命名法生成java类名,例如,“my_proto.proto” 生成的类名为"MyProto"。

然后是message的定义,message是包含一组类型字段的汇总。一些常见的简单字段类型可以直接使用,包括:bool, int32, float, double, string ,当然也可以指定自定义类型的数据类型。在上面的demo中Person包含PhoneNumber的message类型,AddressBook包含Person的消息类型。也可以在message内嵌套定义message,例如PhoneNumber定义在Person内部。你也可以定义enum类型,例如上例中的PhoneType。

字段修饰符说明:

  1. required: 用此作为修饰符的字段必须指定值,否则会被认为未初始化,在编译未初始化的字段时会抛出RuntimeException异常,解析为初始化的字段会抛出IOException异常。其他的跟optional修饰的字段相同。
  2. optional: 这个修饰符指定的字段可以指定值也可以不用指定,如果没有指定的场景下,会使用一个默认值。对于普通简单字段来说,你可以自定指定一个默认值,例如上述demo中的PhoneType。如果没有指定,则会使用系统默认值,数值型变量默认为0,字符串类型为空字符串,布尔类型为false。对于嵌入的message类型,默认值为这个message的"default instance" 或者 “prototype” ,其没有设置任何字段。
  3. repeated: 这个字段可以被重复多次,重复的顺序会被记录下来。可以将其理解为动态分配的数组。

注意: 当使用required修饰时一定要小心,因为当你从required修改为optional时会产生一些问题,修改之后老版本的程序解析数据时认为此处是必须的,而新版本的没有指定,就会造成老版本的异常,出现兼容问题。对于这种必须指定的尽量在程序代码中做check。所以在定义proto时尽量少用required来修饰。

说明: proto格式不支持继承。

编译proto文件

现在已经写好来.proto文件,接下来我们可以使用编译器来生成Java类了。

1.下载编译器

下载地址:https://developers.google.com/protocol-buffers/docs/downloads

因为新版本的编译器是向后兼容的,所以下载最新版本的protoc编译器即可。

2.编译生成java代码

编译使用:

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

demo编译如下:

$ ls
addressbook.proto	dist
$ protoc -I=./ --java_out=./dist/ ./addressbook.proto

查看dist目录

B000000141481OS:tutorial$ cd dist
B000000141481OS:dist$ ls
com
B000000141481OS:dist$ cd com/example/tutorial/
B000000141481OS:tutorial$ ls
AddressBookProtos.java

可以看到已经生成AddressBookProtos的Java类

Protocol Buffer API

接下来让我们分析一下编译器生成的部分Java代码,看下编译器为我们生成类哪些类和方法。
首先,打开生成的java文件,会看到定义的类AddressBookProtos,此类里面包含类proto文件中指定的消息类。每个类都包含一个Builder的类,用来创建类的实例。每个类及对应的builder中会生成字段访问接口,类仅生成getter接口,builder生成getter和setter接口。下面是Person类中的一些简化java代码:

// 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中的部分代码如下:

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

从上述代码中可以看到,编译器会为每个字段生成简单的JavaBean风格代码,也会提供has方法来判断字段是否已经被配置,clear方法用来恢复字段空值状态。
repeated字段会提供一些额外的针对list的处理方法,如Cout用来获取list的大小,通过index来get或者set 值,通过add添加元素,addAll添加所有元素到list中。

注意: 编译器生成的java代码是遵从java约束规范按照驼峰命名法生成的,但是在proto文件中均统一使用小写+下划线方式命名,这样保证了生成其他语言的规范。

枚举和内嵌类

在Person类下包含PhoneType枚举:

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

Person.PhoneNumber类也会内嵌在Person类中

Builders Vs Messages

所有的更具message生成的java类都是固定不变的,如果要构造一个类,首先需要创建一个builder,把想要设置的值通过builder设置,然后调用build()方法。
下面是一个创建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标准方法

每个builder和message类包含一些额外的方法用来检查或者操纵整个消息,包含:

  1. isInitialized() : 检查所有的required字段是否已经初始化
  2. toString(): 返回一个可读的消息内容,主要用来方便调试
  3. mergeFrom(Message other): 合并其他消息,覆盖但单个相同类型字段,合并复合类型字段,整合repeated字段。
  4. clear():设置所有的字段为空。
解析和序列化

最后,每个类中都包含读写protocol buf二进制类型的接口方法。

  1. byte[] toByteArray(): 序列化消息并返回一个包含其二进制数据的byte数组
  2. static Person parseFrom(byte[] data): 从给定的字节数组中解析消息内容
  3. void writeTo(OutputStream output);: 序列化消息并将其写入到OutputStream
  4. static Person parseFrom(InputStream input);: 从InputStream读取并解析消息

写消息

让我来用protoc生成的类来写消息到文件中,代码:

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.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.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);
  }
}

协议扩展

当我们定义好协议,我们后期肯定会因为各种各样的需求来改善协议,如果更改原来的协议那么我们要保证新协议向后兼容,旧协议向前兼容,为来保证兼容性,我们需要遵循下面几条原则:

  1. 不能更改任何存在的字段的标记数字
  2. 不能添加或者删除任何required字段
  3. 可以删除optional或者repeated字段
  4. 添加optional或者repeated字段必须使用一个新的标记数字,这个标记数字不能在之前定义的字段中使用过,包括之前已经删除的标记数字字段

遵从上面的原则,老版本的代码可以使用新协议的内容:
1)忽略新增加的字段
2)删除的optional字段将会使用其默认值
3)删除的repeated字段会使用一个空的list
对与新版本的代码完全可以直接读取解析旧版本的协议消息。

一定要注意的是旧版本的消息中不会包含新增加的optional字段,所以我们需要在代码中使用has_方法去做检查,或者在定义.proto文件是在标记数字后面添加[default = value]默认值,如果没有指定则会使用系统默认值。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值