在java程序中使用protobuf
在java程序中使用protobuf
1、為什么使用protobuf
我們知道數(shù)據(jù)在網(wǎng)絡(luò)傳輸中是以二進制進行的,一般我們使用字節(jié)byte來表示, 一個byte是8bits,如果要在網(wǎng)絡(luò)上中傳輸對象,一般需要將對象序列化,序列化的目的就是將對象轉(zhuǎn)換成byte數(shù)組在網(wǎng)絡(luò)中傳輸,當(dāng)接收方接收到byte數(shù)組之后,再對byte數(shù)組進行反序列化,最終轉(zhuǎn)換成java中的對象。
那么將java對象序列化可能會有如下幾種方法:
(1)使用JDK自帶的對象序列化,但是JDK自帶的序列化本身存在一些問題,并且這種序列化手段只適合在java程序之間進行傳輸,如果是非java程序,比如PHP或者GO,那么序列化就不通用了。
(2)你還可以自定義序列化協(xié)使用JDK自帶的對象序列化,但是JDK自帶的序列化本身存在一些問題,并且這種序列化手段只適合在java程序之間進行傳輸,如果是非java程序,比如PHP或者GO,那么序列化就不通用了。
你還可以自定義序列化協(xié)議,這種方式的靈活程度比較高,但是不夠通用,并且實現(xiàn)起來也比較復(fù)雜,很可能出現(xiàn)意想不到的問題。
(3)將數(shù)據(jù)轉(zhuǎn)換成為XML或者JSON進行傳輸。XML和JSON的好處在于他們都有可以區(qū)分對象的起始符號,通過判斷這些符號的位置就可以讀取到完整的對象。但是不管是XML還是JSON的缺點都是轉(zhuǎn)換成的數(shù)據(jù)比較大。在反序列化的時候?qū)Y源的消耗也比較多。
所以我們需要一種新的序列化的方法,這就是protobuf
,它是一種靈活、高效、自動化的解決方案。
通過編寫一個.proto的數(shù)據(jù)結(jié)構(gòu)定義文件,然后調(diào)用protobuf
的編譯器,就會生成對應(yīng)的類,該類以高效的二進制格式實現(xiàn)protobuf數(shù)據(jù)的自動編碼和解析。 生成的類為定義文件中的數(shù)據(jù)字段提供了getter
和setter
方法,并提供了讀寫的處理細(xì)節(jié)。 重要的是,protobuf可以向前兼容,也就是說老的二進制代碼也可以使用最新的協(xié)議進行讀取。
2、定義.proto文件
.proto
文件中定義的是你將要序列化的消息對象。我們來一個最基本的student.proto文件,這個文件定義了student這個對象中最基本的屬性。
先看一個比較簡單的.proto文件:
syntax = "proto3"; package com.flydean; option java_multiple_files = true; option java_package = "com.flydean.tutorial.protos"; option java_outer_classname = "StudentListProtos"; message Student { optional string name = 1; optional int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; } message PhoneNumber { optional string number = 1; optional PhoneType type = 2; } repeated PhoneNumber phones = 4; } message StudentList { repeated Student student = 1; }
第一行定義的是protobuf
中使用的syntax協(xié)議,默認(rèn)情況下是proto2
,因為目前最新的協(xié)議是proto3
,所以這里我們使用proto3作為例子。
然后我們定義了所在的package,這個package是指編譯的時候生成文件的包。這是一個命名空間,雖然我們在后面定義了java_package
,但是為了和非java語言中的協(xié)議相沖突,所以定義package還是非常有必要的。
然后是三個專門給java程序使用的option。java_multiple_files
, java_package
, 和 java_outer_classname
.
其中java_multiple_files指編譯過后java文件的個數(shù),如果是true,那么將會一個java對象一個類,如果是false,那么定義的java對象將會被包含在同一個文件中。
java_package
指定生成的類應(yīng)該使用的Java包名稱。 如果沒有明確的指定,則會使用之前定義的package的值。
java_outer_classname
選項定義將表示此文件的包裝類的類名。 如果沒有給java_outer_classname賦值,它將通過將文件名轉(zhuǎn)換為大寫駝峰來生成。 例如,默認(rèn)情況下,“student.proto
”將使用"Student"作為包裝類名稱。
接下來的部分是消息的定義,對于簡單類型來說可以使用bool, int32, float, double, 和 string來定義字段的類型。
上例中我們還使用了復(fù)雜的組合屬性,和嵌套類型。還定義了一個枚舉類。
上面我們?yōu)槊總€屬性值分配了ID,這個ID是二進制編碼中使用的唯一“標(biāo)簽”。因為在protobuf中標(biāo)記數(shù)字1-15比16以上的標(biāo)記數(shù)字占用的字節(jié)空間要更少,因此作為一種優(yōu)化,通常將1-15這些標(biāo)記用于常用或重復(fù)的元素,而將標(biāo)記16和更高的標(biāo)記用于不太常用的可選元素。
然后再來看看字段的修飾符,有三個修飾符分別是optional,repeated和required。
optional表示該字段是可選的,可以設(shè)置也可以不設(shè)置,如果沒有設(shè)置,則會使使用默認(rèn)值,對于簡單類型來說,我們可以自定義默認(rèn)值,如果不自定義,就會使用系統(tǒng)的默認(rèn)值。對于系統(tǒng)的默認(rèn)值來說,數(shù)字為0,字符串為空字符串,布爾值為false。
repeated表示該字段是可以重復(fù)的,這種重復(fù)實際上就是一種數(shù)組的結(jié)構(gòu)。
required表示該字段是必須的,如果該字段沒有值,那么該字段將會被認(rèn)為是沒有初始化,嘗試構(gòu)建未初始化的消息將拋出 RuntimeException,解析未初始化的消息將拋出 IOException。
注意:在Proto3中不支持required字段。
3、編譯協(xié)議文件
定義好proto文件之后,就可以使用protoc命令對其進行編譯了。
protoc是protobuf提供的編譯器,一般情況下,可以從github的release庫中直接下載即可。如果你不想直接下載,或者官方提供的庫中并沒有你需要的版本,則可以使用源代碼直接進行編譯。
protoc的使用的命令如下:
protoc --experimental_allow_proto3_optional -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/student.proto
如果編譯proto3
,則需要添加--experimental_allow_proto3_optional
選項。
我們運行一下上面的代碼。會發(fā)現(xiàn)在com.flydean.tutorial.protos包里面生成了5個文件。分別是:
Student.java StudentList.java StudentListOrBuilder.java StudentListProtos.java StudentOrBuilder.java
其中StudentListOrBuilder
和StudentOrBuilder
是兩個接口,Student
和StudentLis
t是這兩個類的實現(xiàn)。
4、詳解生成的文件
在proto文件中,我們主要定義了兩個類Student和StudentList, 他們中定義了一個內(nèi)部類Builder,以Student為例,看下這個兩個類的定義:
public final class Student extends com.google.protobuf.GeneratedMessageV3 implements StudentOrBuilder public static final class Builder extends com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements com.flydean.tutorial.protos.StudentOrBuilder
可以看到他們實現(xiàn)的接口都是一樣的,表示他們可能提供了相同的功能。實際上Builder是對消息的一個封裝器,所有對Student的操作都可以由Builder來完成。
對于Student中的字段來說,Student類只有這些字段的get方法,而Builder中同時有g(shù)et和set方法。
對于Student來說,對于字段的方法有:
// 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);
對于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();
多出的兩個方法是set和clear方法。clear是清空字段的內(nèi)容,讓其變回初始狀態(tài)。
我們還定義了一個枚舉類PhoneType
:
public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum
這個類的實現(xiàn)和普通的枚舉類沒太大區(qū)別。
5、Builders 和 Messages
如上一節(jié)所示,Message對應(yīng)的類只有g(shù)et和has方法,所以它是不可以變的,消息對象一旦被構(gòu)造,就不能被修改。要構(gòu)建消息,必須首先構(gòu)建一個構(gòu)建器,將要設(shè)置的任何字段設(shè)置為你選擇的值,然后調(diào)用構(gòu)建器的 build()方法。
每次調(diào)用Builder的方法都會返回一個新的Builder,當(dāng)然這個返回的Builder和原來的Builder是同一個,返回Builder只是為了方便進行代碼的連寫。
下面的代碼是如何創(chuàng)建一個Student實例:
Student xiaoming = Student.newBuilder() .setId(1234) .setName("小明") .setEmail("flydean@163.com") .addPhones( Student.PhoneNumber.newBuilder() .setNumber("010-1234567") .setType(Student.PhoneType.HOME)) .build();
Student中提供了一些常用的方法,如isInitialized()檢測是否所有必須的字段都設(shè)置完畢。toString()將對象轉(zhuǎn)換成為字符串。使用它的Builder還可以調(diào)用clear()用來清除已設(shè)置的狀態(tài),mergeFrom(Message other)用來對對象進行合并。
6、序列化和反序列化
生成的對象中提供了序列化和反序列化方法,我們只需要在需要的時候?qū)ζ溥M行調(diào)用即可:
- byte[] toByteArray();: 序列化消息并返回一個包含其原始字節(jié)的字節(jié)數(shù)組。
- static Person parseFrom(byte[] data);: 從給定的字節(jié)數(shù)組中解析一條消息。
- void writeTo(OutputStream output);: 序列化消息并將其寫入 OutputStream.
- static Person parseFrom(InputStream input);: 從一個消息中讀取并解析消息 InputStream.
通過使用上面的方法,可以很方便的將對象進行序列化和反序列化。
7、協(xié)議擴展
我們在定義好proto之后,假如后續(xù)還希望對其進行修改,那么我們希望新的協(xié)議對歷史數(shù)據(jù)是兼容的。那么我們需要考慮下面幾點:
- 不能更改現(xiàn)有字段的ID編號。
- 不能添加和刪除任何必填字段。
- 可以 刪除可選或重復(fù)的字段。
- 可以 添加新的可選字段或重復(fù)字段,但您必須使用新的ID編號。
到此這篇關(guān)于在java程序中使用protobuf的文章就介紹到這了,更多相關(guān)在java使用protobuf內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java注解處理器學(xué)習(xí)在編譯期修改語法樹教程
這篇文章主要為大家介紹了java注解處理器學(xué)習(xí)在編譯期修改語法樹教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09Springboot @Configuration @bean注解作用解析
這篇文章主要介紹了springboot @Configuration @bean注解作用解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02Java多線程生產(chǎn)者消費者模式實現(xiàn)過程解析
這篇文章主要介紹了Java多線程生產(chǎn)者消費者模式實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03Java實現(xiàn)過濾掉map集合中key或value為空的值示例
這篇文章主要介紹了Java實現(xiàn)過濾掉map集合中key或value為空的值,涉及java針對map的簡單遍歷、判斷、移除等相關(guān)操作技巧,需要的朋友可以參考下2018-06-06基于mybatis-plus QueryWrapper 排序的坑
這篇文章主要介紹了mybatis-plus QueryWrapper 排序的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01解決SpringBoot運行報錯:找不到或無法加載主類的問題
這篇文章主要介紹了解決SpringBoot運行報錯:找不到或無法加載主類的問題,具有很好的參考價值,對大家的學(xué)習(xí)或工作有一定的參考價值,需要的朋友可以參考下2023-09-09java關(guān)于并發(fā)模型中的兩種鎖知識點詳解
在本篇文章了小編給大家整理的是一篇關(guān)于java關(guān)于并發(fā)模型中的兩種鎖知識點詳解內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2021-04-04SpringBoot2.x 整合 AntiSamy防御XSS攻擊的簡單總結(jié)
本文主要對SpringBoot2.x集成AntiSamy防御XSS攻擊進行簡單總結(jié),其中SpringBoot使用的2.4.5版本,通過示例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-08-08