Java的ProtoBuf

安装

protocolbuffer是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了三种语言的实现:javac++ 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域

本篇博客主要教大家如何在windows7下安装Java的protocol buffer(具体使用及注意事项将会在下一篇博客当中进行详细介绍) 

首先,要使用protocol buffer得保证maven安装成功,maven的下载地址:http://maven.apache.org/download.cgi 。

1.解压完之后请将maven的bin目录配置到你的环境变量当中。

2.请确保你的JAVA_HOME的变量是指向你的JDK的主目录,如果你的系统变量中没有JAVA_HOME这一项,请点击新建添加。

3.打开命令行,输入“mvn --version”如果输出正确则表示安装成功

安装完maven之后就要进行protocol buffer的安装了,下载地址: http://code.google.com/p/protobuf/downloads/list 。下载protobuf-2.4.1.zip 和 protoc-2.4.1-win32.zip 两个包。

1. 解压完成之后有两种选择,第一:将protoc-2.4.1-win32中的protoc.exe所在的目录配置到环境变量当中,第二:将protoc.exe拷贝到c:\windows\system32目录下,这里推荐第二种做法。

2. 将proto.exe文件拷贝到解压后的protobuf-2.4.1\src目录中.

3. 进入protobuf-2.4.1\java 目录  执行mvn package命令编辑该包,系统将会在target目录中生成protobuf-java-2.4.1.jar文件(注意运行时需要联网,首次安装可能需要一定的时间)。

4. 假设你的数据文件目录在XXX\data目录,把上一步生成的jar拷贝到该目录中即可。

5. 进入XXX\protobuf-2.4.1\examples目录,可以看到addressbook.proto文件,在命令行中执行 protoc --java_out=. addressbook.proto 命令(特别注意. Addressbook.proto中间的空格,我第一次安装就因为没注意而反复失败),如果生成com文件夹并且最终生成AddressBookProtos类则说明安装成功。

6. 打开eclipse,选择windows-->preferences-->java-->Installed JREs编辑你默认的java源码包,并将上面所提到的protobuf-java-2.4.1.jar文件添加进去。

使用

创建一个.proto文件并在里面定义消息格式把你需要序列化的数据在里面声明类型和名称,并将这个文件命名为addressbook.proto。具体内容如下:

Proto代码   收藏代码
  1. package tutorial;  
  2.   
  3. option java_package = "com.example.tutorial";  
  4. option java_outer_classname = "AddressBookProtos";  
  5.   
  6. message Person{  
  7.     required string name = 1;  
  8.     required int32  id =2;  
  9.     optional string email = 3;  
  10.       
  11.     enum PhoneType{  
  12.         MOBILE = 0;  
  13.         HOME = 1;  
  14.         WORK = 2;  
  15.     }  
  16.       
  17.     message PhoneNumber{  
  18.         required string number = 1;  
  19.         optional PhoneType type  = 2 [default = HOME];  
  20.     }  
  21.       
  22.     repeated PhoneNumber phone = 4;  
  23. }  
  24.   
  25. message AddressBook{  
  26.     repeated Person person = 1;  
  27. }  

正如你所看到的,它的语法类似C++或者Java,让我们通过该文件的每一个部分来分析一下吧。

首先,这个文件以一个package的定义开头,以防在不同工程中的命名冲突,在Java里面,package就是用来当做Java package(除非你有明确的定义一个java package)。 不过,在这里,即使你已经提供了一个java_package,你仍然需要定义一个package以防Protocol Buffer使用在其他没有Java语言的环境中。

在package的定义之后,你可以看到两个option(选项):java_package以及java_outer_classname。java_package是用来定义java类应该在哪个包里面生成,如果你没有写这一项的话,那么他默认的会以你的package的定义来生成。Java_outer_classname这一项是用来定义在这个文件中的哪一个类需要包含所有的这些类的信息,如果你不定义这一项的话,那么它会以你的文件名来作为刻板,比如说如果你写的是“my_proto.proto”的话,那么它就会默认的使用“MyProto”来作为类名。

接下来就是你消息的类型定义了。一个message就是一系列类型领域的集合,许多基础的数据类型在这里面都是可用的,包括bool,int32,float,double,以及string等。你同样可以添加更多的结构化的数据在里面,比如上面的Person message就包含了PhoneNumber message,而AddressBook message包含了Person message。你同样在一中message类型里面定义另外一种类型,例如上面所举到的,Person里面所定义的enum(枚举)类型,以区分PhoneNumber的不同类型。

在每一个元素之后的“=1”,“=2”是用来区分它们的独特的“标签”。标签数字1-15编码所需的字节数比更高的数字所需的字节数要少一个,所以为了使程序达到最佳的状态,你可以使用这些标签进行反复标记(在同一个域中不能重复)。每一个元素在重复的领域都需要重新编码这些“标签”,所以重复的领域应该考虑到更多可能的方案来达到最佳状态。

每一个域的前面都必须使用下面这些修饰符来修饰:

·required(必需的): 这说明这个域的值不能为空,否则这条message将会被当做“不知情的”。如果你尝试的创建一条“不知情的”message,那么系统将会抛出一个RuntimeException(运行时异常)。而分析一条“不知情的”message则会抛出IOException。除此之外,确切的来说一个被required修饰的域其行为则更接近optional修饰的域。 

·optional(可选择的): 被这个修饰符修饰的域可以为空。如果一个被optional修饰的域没有设值的话,那么系统将会使用默认值。对于一些基本类型来说,你可以定义你自己的默认值(就像我前面在定义PhoneNumber的PhoneType时一样)。 否则,对于数值类型来说,系统的默认值是0,string的默认值是empty string,bool的默认值是false。对于植入的message来说(比如AddressBook里面的Person),默认值则经常是该消息默认的实例或者标准。调用存取器去获取那些被optional(或者required)修饰的但还没有被初始化的域将会返回它的默认值。

·repeated(反复的): 某一个域可能会被使用多次,而那些反复使用的值将会被保留在protocol buffer里面。 你可以把用repeated修饰的域想象成动态数组。

 

Required是“永久”的。

当你使用required来修饰域的时候你必须非常的小心。如果某些时候你想要停止发送一个用required修饰的域并将它修改为optional修饰时,之前的readers会把你的message考虑为不完整的并且无意识的丢弃它。事实上,人们已经开始注意到了使用required的所带来的危害,所以我们应该更多的使用optional和repeated。


使用Protocol buffer的编译器


    现在,你已经有一个.proto文件了,接下来就需要生成class来发送和读取AddressBook消息(包含Person以及PhoneNumber),为了完成这件工作,你需要调用protocol buffer的编译器的proto指令来编译你的.proto文件。语法如下:
                         <span style="white-space:pre">		</span>proto -I=$SRC --java_out=$DIR File
$SRC表示资源文件夹,$DIR表示目标文件夹,File是你要编译的.proto文件,当然,你也可以定位到这个资源目录中然后只调用proto --java_out=$DIR File即可。(需要注意的是生成的class文件会存在于你所填的目标文件夹下的java_package所指向的目录中,如果你想把目标文件夹指向当前目录,你可以使用“.”来代替,例如对于该例,我们先定位到这个目录下,然后运行:
<span style="white-space:pre">					</span>proto --java_out=. addressbook.proto)


使用protocol buffer的API


首先,让我们看看编译器给我们生成了那些代码。首先你可以发现它的类名与我们定义的java_outer_classname的名字相同,同样里面还包含了你在addressbook.proto里面定义的各种message的类,每一个类都有它自身的Builder用来实例化这个类对象。所有的messagesbuilders都自动生成了存取器,但是messages只有getterbuilders既有getters又有setters。以下就是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 也有这样的存取器:

// 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-stylegetterssetters,但是用repeated修饰的域有一些特殊的方法,Count方法(用来统计这个消息list(列表)的长度)。通过add 方法可以在这个list中追加一个元素,而addAll 方法可以把一个Container(容器)里面的所有元素都添加在list当中。

注意到这些方法都是使用的驼峰式命名法,尽管在.proto文件里面我们都是写的小写,这也恰恰展示了protocol buffer的强大之处。

另外,还有一个需要注意的就是enum(枚举)类型所生成的类,它自动生成了如下代码:

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

 

Builders vs. Messages

Message类里面由protocol buffer编译器自动生成的代码都是不可变的,一旦一个message对象被实例化之后,它就不能再被修改了,就像Java中的String一样。而如果想要实例化一个message类,你必须首先实例化一个builder类,然后设置好所有你想要设置的属性,然后再调用builder类的build()方法。你或许已经注意到了builder的每一个用来改变message的属性的方法都返回了另外一个builder。不要怀疑,这个 builder就是为了让你更加方便的定义其他属性而存在的。下面就展示了一段用来创建一个新的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,这些方法包括:

·isInitialized()检测所有用required修饰的域是否都设置了初始值。

·toString()返回一个可读的message,特别是用来测试的时候 

·mergeFrom(Message other): (builder独有的把另一个message合并到这个message当中,重写单独域并且连接反复的域。

·clear(): (builder独有的)清空所有的域并且返回空的状态

这些方法都实现了MessageMessage.Builder 接口并且接口被所有的Java messages以及builders共享。

 

解析和序列化

最后,所有的protocol buffer类都有writingreading你所选择的protocol buffer (二进制)格式数据的方法。他们包括:

·byte[] toByteArray();序列化这个 message 并且返回一个字节数组。

·static Person parseFrom(byte[] data);从给定的字节数组中解析一条message

·void writeTo(OutputStream output);序列化这个message并且将它写入 OutputStream.

·static Person parseFrom(InputStream input);读取并且解析一条InputStream中的message

这里只是其中的几个解析和序列化的方法而已,如果想要知道其中所有的方法可以将该类生成doc文档然后查看。

 

Protocol BuffersO-O Design

 Protocol buffer的类基本上是无声的数据持有者(就像 C++里面的结构体一样);他们最开始在一个对象模型中表现的并不友好,如果你想要为生成的类添加一个更加友好的方法的话,最好的方式是将protocol buffer生成的类包装在一个特定的应用程序里面。对于不太懂.proto文件设计的人来说包装protocol buffer也是一个不错的主意。你也可以使用包装类生成接口以便更好地适应特定的程序环境:隐藏一些数据和方法,并且显示一些便捷的功能,等等。特别需要注意的是,你最好不要写一些行为去继承这些生成的类。那会打破它内部的机制况且它对于你来说也不是一次很好的面向对象的练习机会。

 

Writing A Message

OK,说了这么多,那就来使用一下protocol buffer生成的类吧。首先,你肯定希望你的这个“addressbook”的应用程序可以write一个你定义的message。为了完成这项工作,你需要创建一个新的类来调用protocol buffer类里面的方法并将message写入OutputStream.

下面就是一个将用户在控制台输入的AddressBook 的相关信息写入文件的一个类,当然,你首先得创建一个文件(当然你也可以在文件不存在的情况下使用File类的createNewFile()方法来创建一个新的文件),为了个性化你的程序,不妨以.book作为你的后缀名,具体代码如下:

 

Java代码   收藏代码
  1. import java.io.BufferedReader;  
  2. import java.io.FileInputStream;  
  3. import java.io.FileNotFoundException;  
  4. import java.io.FileOutputStream;  
  5. import java.io.IOException;  
  6. import java.io.InputStreamReader;  
  7. import java.io.PrintStream;  
  8.   
  9. import com.example.tutorial.AddressBookProtos.AddressBook;  
  10. import com.example.tutorial.AddressBookProtos.Person;  
  11. class AddPerson {  
  12.     /** 
  13.      * 将用户输入的Person message写入输出流中   
  14.      * @param stdin 输入流 
  15.      * @param stdout 打印输出流 
  16.      * @return Person类 
  17.      * @throws IOException 
  18.      */  
  19.     static Person PromptForAddress(BufferedReader stdin,PrintStream stdout)  
  20.             throws IOException {  
  21.           
  22.         Person.Builder person = Person.newBuilder();  
  23.         stdout.print("Enter person ID: ");  
  24.         person.setId(Integer.valueOf(stdin.readLine()));  
  25.   
  26.         stdout.print("Enter name: ");  
  27.         person.setName(stdin.readLine());  
  28.   
  29.         //空白表示没有  
  30.         stdout.print("Enter email address (blank for none): ");  
  31.         String email = stdin.readLine();  
  32.         if (email.length() > 0){  
  33.             person.setEmail(email);  
  34.         }  
  35.         while (true) {  
  36.             //按下Enter键结束输入  
  37.             stdout.print("Enter a phone number (or leave blank to finish): ");  
  38.             String number = stdin.readLine();  
  39.             if (number.length() == 0) {  
  40.                 break;  
  41.             }  
  42.               
  43.             Person.PhoneNumber.Builder phoneNumber = Person.PhoneNumber.newBuilder().setNumber(number);  
  44.               
  45.             //输入完成之后需要确定你输入的是手机号、家庭电话还是工作电话  
  46.             stdout.print("Is this a mobile, home, or work phone? ");  
  47.             String type = stdin.readLine();  
  48.             if (type.equals("mobile")) {  
  49.                 phoneNumber.setType(Person.PhoneType.MOBILE);  
  50.             } else if(type.equals("home")){  
  51.                     phoneNumber.setType(Person.PhoneType.HOME);  
  52.             } else if (type.equals("work")) {  
  53.                 phoneNumber.setType(Person.PhoneType.WORK);  
  54.             } else {  
  55.                 stdout.println("Unknown phone type.Using default.");  
  56.             }  
  57.             person.addPhone(phoneNumber);  
  58.             }  
  59.             return person.build();  
  60.         }  
  61.       
  62.     //Main function:  Reads the entire address book from a file,  
  63.     //adds one person based on user input, then writes it back out to the same  
  64.     //file.    
  65.     public static void main(String[] args)  
  66.             throws Exception {  
  67.           
  68. //      if (args.length != 1) {  
  69. //          System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");  
  70. //          System.exit(-1);  
  71. //          }  
  72.         AddressBook.Builder addressBook = AddressBook.newBuilder();  
  73.           
  74.         // 检验是否存在这个文件  
  75.         try {  
  76.             addressBook.mergeFrom(new FileInputStream("src/Book/TestPerson.book"));  
  77.             } catch (FileNotFoundException e) {  
  78.                 System.out.println("src/Book/TestPerson.book" + ": File not found.Creating a new file.");  
  79.             }      
  80.           
  81.         //将这条Person message添加到AddressBook中  
  82.         addressBook.addPerson(PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),System.out));  
  83.           
  84.         //将新建的AddressBook写入文件当中  
  85.         FileOutputStream output = new FileOutputStream("src/Book/TestPerson.book");  
  86.         addressBook.build().writeTo(output);  
  87.         output.close();    
  88.     }  
  89. }  

Reading A Message

当然,这个程序肯定不止一个写入消息的类,还要能把存在文件中的数据读出来。如下:

 

Java代码   收藏代码
  1. import com.example.tutorial.AddressBookProtos.AddressBook;  
  2. import com.example.tutorial.AddressBookProtos.Person;  
  3. import java.io.FileInputStream;  
  4. class ListPeople {  
  5.     /** 
  6.      * 迭代遍历并且打印文件中所包含的信息 
  7.      * @param addressBook AddressBook对象 
  8.      */  
  9.     static void Print(AddressBook addressBook) {  
  10.         for (Person person: addressBook.getPersonList()){  
  11.             System.out.println("Person ID: " + person.getId());  
  12.             System.out.println("Name: " + person.getName());  
  13.             if (person.hasEmail()) {  
  14.                 System.out.println("E-mail address:"+ person.getEmail());  
  15.                 }  
  16.             for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {  
  17.                 switch (phoneNumber.getType()) {  
  18.                 case MOBILE:  
  19.                     System.out.print("Mobile phone #: ");  
  20.                     break;      
  21.                 case HOME:  
  22.                     System.out.print("Home phone #: ");  
  23.                     break;  
  24.                 case WORK:  
  25.                     System.out.print("Work phone #: ");  
  26.                     break;  
  27.                     }        
  28.                     System.out.println(phoneNumber.getNumber());  
  29.                 }  
  30.             }  
  31.         }  
  32.       
  33.     public static void main(String[] args) throws Exception {  
  34. //      if (args.length != 1) {  
  35. //          System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");   
  36. //          System.exit(-1);  
  37. //      }   
  38.         // 读取已经存在.book文件  
  39.         AddressBook addressBook = AddressBook.parseFrom(new FileInputStream("src/Book/TestPerson.book"));  
  40.         Print(addressBook);  
  41.     }  
  42.  }  

 

扩展一个Protocol Buffer

当你发布了一段由你的protocol buffer编写的代码之后,你或许迫不及待的想要扩展它的功能。如果你想要使你的新buffers是反向兼容的或者你的旧buffers是正向兼容的话,那么下面有几条规则是你需要遵守的,在新的protocol buffer的版本中:· 你最好不要改变已经存在的域的标签(Tag)

   ·你最好不要添加或者删除任何用required修饰的域

      ·你可以删除optional或者repeated修饰的域

   ·你可以添加新的用optional或者repeated修饰的域但你必须使用Tag数字(从未被这个protocol所使用过的tag,即使是被删除了的也不行).

如果你遵循这些规则,旧的代码也会非常“高兴”的读取新的消息。对于旧的代码来说,那些被删除了的用optional修饰的域会有他们的默认值并且被删除了用repeated修饰的域会为空,新的代码读取旧的消息也会很轻松。然而,请记住,新的optional域不会存在于旧的message当中,所以你应该明确的知道它们是否被设置为has_或者在你的.proto 文件提供了一个合理的默认值[default = value]。如果默认值没有明确是一个optional元素,而是按默认类型定义的话对于string来说默认值就是empty string,其他类型也类型,在本文的上面已经提到过了,这里就不在累赘。特别声明,如果你添加了一个新的用repeated修饰的域而没有has_标志的话,你的新代码将无法识别它是否为空,而你的旧代码则完全无法设置它。

 

高级用法

Protocol buffers还有一些用法是一般的存取器和序列化所无法办到的,如果你需要了解更多的信息可以上Google的官方文档上面去查看。

Protocolmessage类提供的一个关键特征就是映射 在一个message里面你可以反复声明不同的域并且操作他们的值而不需要在任何类型的message之前写代码。使用映射来从其他的编码中转换一条message无疑是一个非常有效的方法,即使面对XML 或者JSON也一样。关于映射的一个更加高级的用法恐怕就是找出两条相同类型的message类之间的不同点了,或者是为Protocol buffer生成一系列的“规则映像”,使用这些映像你可以匹配确切的消息内容。发挥你的想象,Protocol Buffers可以应用到更多的领域当中去。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值