ProtoBuf-java使用

1、简介

1.1、什么是 protobuf

Protocal Buffers(简称protobuf)是谷歌的一项技术,用于结构化的数据序列化、反序列化。

官方解释:Protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法。可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

1.2、 为什么要使用protobuf

使用protobuf的原因肯定是为了解决开发中的一些问题,那使用其他的序列化机制会出现什么问题呢?

(1)java默认序列化机制:效率极低,而且还能不能跨语言之间共享数据。

(2)XML常用于与其他项目之间数据传输或者是共享数据,但是编码和解码会造成很大的性能损失。

(3)gson格式也是常见的一种,但是gson在解析的时候非常耗时,而且gson结构非常占内存。

但是我们protobuf是一种灵活的、高效的、自动化的序列化机制,可以有效的解决上面的问题。由于 protobuf是跨语言的,所以用不同的语言序列化对象后,生成一段字节码,之后可以其他任何语言反序列化并自用,大大方便了跨语言的通讯,同时也提高了效率

需要注意: protobuf生成的是字节码,可读性相比略差一点。

2、protobuf数据类型

创建 FileName.proto文件,后缀名称必须是.proto。一般一个文件就代表一个 proto对象。在文件中定义 proto 对象的属性。通过 .proto文件可以生成不同语言的类,用于结构化的数据序列化、反序列化。

protobuf官方文档:https://protobuf.dev/programming-guides/proto3/

定义一个 proto 对象的属性,基本格式如下:

字段标签(可选) 字段类型 字段名称 字段标识符 字段默认值(可选)

关于字段编号(标识符),是字段中唯一且必须的,以 1开始,不能重复,不能跳值,这个是和编译有关系的。

2.1、基本数据类型

常见基本数据类型:

在这里插入图片描述

系统默认值:

  • string:默认为空字符串
  • byte:默认值为空字节
  • bool:默认为false
  • 数值:默认为0
  • enum:默认为第一个元素
2.2、集合List字段

Java String、Integer List 在 protobuf 的定义。

message User{
  //list Int
  repeated int32 intList = 1;
  //list String
  repeated string strList = 2;
}
2.3、Map字段

Java String、Integer Map 在 protobuf 的定义。

message User{
  // 定义简单的 Map string
  map<string, int32> intMap = 7;
  // 定义复杂的 Map 对象
  map<string, string> stringMap = 8;
}
2.4、对象字段

Java 对象 List 在 protobuf 的定义。

message User{
  //list 对象
  repeated Role roleList = 6;
}
2.5、Map对象值字段

Java 对象 Map 在 protobuf 的定义。

message User{
  // 定义复杂的 Map 对象
  map<string, MapVauleObject> mapObject = 8;
}


// 定义 Map 的 value 对象
message MapVauleObject {
  string code = 1;
  string name = 2;
}
2.36、嵌套对象字段

Java 实体类中使用另一个实体类作为字段在 protobuf 的定义。

message User{
  // 对象
  NickName nickName = 4;
}

// 定义一个新的Name对象
message NickName {
  string nickName = 1;
}
2.7、关键字
protobuf关键字
•syntax:声明版本。例如上面syntax=”proto3”,如果没有声明,则默认是proto2。
•package:声明包名.
•import:导入包。类似于java,例如上面导入了timestamp.proto包。
•java_package:指定生成的类应该放在什么Java包名下。如果你没有显式地指定这个值,则它简单地匹配由package 声明给出的Java包名,但这些名字通常都不是合适的Java包名 (由于它们通常不以一个域名打头)。
•java_outer_classname:定义应该包含这个文件中所有类的类名。如果你没有显式地给定java_outer_classname ,则将通过把文件名转换为首字母大写来生成。例如上面例子编译生成的文件名和类名是AddressBookProtos。
•message:类似于java中的class关键字。
•repeated:用于修饰属性,表示对应的属性是个array。
2.8、案例

MarsComponent.proto

syntax = "proto3";
import public "google/protobuf/timestamp.proto";

import "MarsPhraseRelate.proto";
import "MarsGeneralComponentType.proto";
import "MarsComponentLocation.proto";

//生成的类所处的层级
option java_package = "com.windhill.rmt.mars.protobuf";

//是否需要將生成的类拆分为多个
option java_multiple_files = false;
//生成 proto 文件名
option java_outer_classname = "MarsComponentModule";

message MarsComponent {
  string id = 1;
  string displayId = 2;
  string name = 3;
  string nameSv = 4;
  int32 active = 5;
  string signalTypeDesignation = 6;
  int32 releaseStatus = 7;
  int32 highestRelease = 8;
  string ecu = 9;
  string pins = 10;
  string cableHarness = 11;
  string brandCode = 12;

  string isDeleted = 13;
  string tenantId = 14;
  int32 revision = 15;
  string createdBy = 16;
  //  google.protobuf.Timestamp createdTime = 17;
  string createdTime = 17;

  repeated MarsPhraseRelate marsPhraseRelateList = 18;
}

MarsPhrase.proto

syntax = "proto3";
import public "google/protobuf/timestamp.proto";
import "MarsPhrase.proto";

//生成的类所处的层级
option java_package = "com.windhill.rmt.mars.protobuf";

//是否需要將生成的类拆分为多个
option java_multiple_files = false;
//生成 proto 文件名
option java_outer_classname = "MarsPhraseRelateModule";

message MarsPhraseRelate {
  string id = 1;
  string mountId = 2;
  string phraseId = 3;
  string signalType = 4;

}

3、 ProtoBuf入门使用

3.1、下载安装并配置环境变量

下载地址:https://github.com/protocolbuffers/protobuf/releases 选择你喜欢的版本

配置Path环境变量,指定protoc.exe执行程序路径:

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2h1eGlhbmcxOTg1MTExNA==,size_16,color_FFFFFF,t_70

打开CMD,输入protoc --version,显示版本号即可:

3.2、手动命令实现

语法:protoc -I= S R C D I R − − j a v a o u t = SRC_DIR --java_out= SRCDIRjavaout=DST_DIR $SRC_DIR/addressbook.proto PS:也可以生成C++文件,区别在于java_out改为cpp_out

protoc -I="D://workspaces//workspace//serializable-test" --java_out="D://workspaces//java" "D://workspaces//workspace//serializable-test//src//main//java//com//ydt//protobuf//person.proto"

protoc ./a.proto --java_out=./

或者进入目录执行命令
protoc.exe --java_out=. MarsComponent.proto 
3.3、使用Idea插件

image-20240424191610352

右键点击,生成文件

image-20240424191657307

3、java使用

3.1、导入依赖坐标:
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.7.1</version>
    </dependency>

先根据proto2或者proto3的语法创建一个.proto文件,下面是.proto数据类型和Java类型的对照表

// 如果使用此注释,则使用proto3; 否则使用proto2

syntax = "proto3";
// 生成类的包名
option java_package = "com.ydt.template";
//生成的数据访问类的类名,如果没有指定此值,则生成的类名为proto文件名的驼峰命名方法
option java_outer_classname = "UserSerializable";
message User {
   int32 age = 1;
   string name = 2;
   string sex = 3;
}

定义一个命令类,用来根据.proto文件生成对应的序列化类:

package com.ydt.cmd;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Cmd {
// protoc的目录
private static final String PROTOC_FILE = System.getProperty("user.dir")+ "\\src\\main\\resources\\protoc.exe";
// .proto文件所在项目根目录
private static final String IMPOR_TPROTO = System.getProperty("user.dir");
// 生成java类输出目录
private static final String JAVA_OUT = System.getProperty("user.dir")+ "\\src\\main\\java";
// 指定proto文件
private static final String PROTOS = System.getProperty("user.dir")+ "\\src\\main\\java\\com\\ydt\\protobuf\\user.proto";

/**
 * 使用java process执行shell命令
 */
public void execute() {
    List<String> lCommand = new ArrayList<String>();
    lCommand.add(PROTOC_FILE);
    lCommand.add("-I=" + IMPOR_TPROTO );
    lCommand.add("--java_out=" + JAVA_OUT);
    lCommand.add(PROTOS);
    ProcessBuilder pb = new ProcessBuilder(lCommand);
    pb.redirectErrorStream(true);
    Process p;
    int i = 1;
    try {
        p = pb.start();
        try {
            //jdk实现process时,调用外部命令不是同步的调用,而是异步执行,需要等待执行完成
            i = p.waitFor();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    int iResult = p.exitValue();
    if (iResult == 0 && i == 0) {
        System.out.println(" result = " + p.exitValue() + ", execute command success! Command = " + lCommand);
    } else {
        System.out.println(" result = " + p.exitValue() + ", execute command failure! Command = " + lCommand);
    }
}
}

生成序列化Java实体类测试:

@Test
public void test3(){
    Cmd cmd = new Cmd();
    cmd.execute();
}

生成成功:

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2h1eGlhbmcxOTg1MTExNA==,size_16,color_FFFFFF,t_70

序列化和反序列化测试:

@Test
public void test4() throws IOException {
    //序列化
    UserSerializable.User.Builder builder = UserSerializable.User.newBuilder();
    builder.setAge(18)
            .setName("laohu")
            .setSex("men");
    UserSerializable.User user = builder.build();
    byte[] bytes = user.toByteArray();
    System.out.println(Arrays.toString(bytes));

    //反序列化
    UserSerializable.User decodeUser = UserSerializable.User.parseFrom(bytes);
    System.out.println(decodeUser);

    //生成序列化文件
    OutputStream os = new FileOutputStream("object3.txt");
    os.write(bytes);
    os.flush();
    os.close();

}

//protobuf打印的结果
/*[8, 1, 18, 5, 108, 97, 111, 104, 117, 26, 6, 49, 50, 51, 52, 53, 54]*/

//Gson打印的结果
/*[123, 34, 105, 100, 34, 58, 49, 44, 34, 117, 115, 101, 114, 110, 97, 109, 101, 34, 58, 34, 108, 97, 111, 104, 117, 34, 44, 34, 112, 97, 115, 115, 119, 111, 114, 100, 34, 58, 34, 49, 50, 51, 52, 53, 54, 34, 125]*/

//jdk打印的结果
/*[-84, -19, 0, 5, 115, 114, 0, 17, 99, 111, 109, 46, 121, 100, 116, 46, 112, 111, 106, 111, 46, 85, 115, 101, 114, 108, 124, -59, -89, -28, 34, 123, -76, 2, 0, 3, 73, 0, 2, 105, 100, 76, 0, 8, 112, 97, 115, 115, 119, 111, 114, 100, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 0, 0, 0, 1, 116, 0, 6, 49, 50, 51, 52, 53, 54, 116, 0, 5, 108, 97, 111, 104, 117]*/

起码我们现在可以看到,同样的一个User对象,protobuf序列化的字节码数组明显小太多,那么如果大量这样的对象在网络中传输的话,对于带宽的消耗是否要低得多,是不是速度会更快!

当然,Gson序列化的方式比较合理,所以大家注意了,在对性能没有极限要求的情况下,用Gson进行序列化即可,毕竟使用protobuf需要的学习成本比较大

4、 ProtoBuf的实现机制

从上面我们看到,ProtoBuf在网络传输中的字节流数组非常小,这是为什么呢?

我们先来看看三种情况下生成的序列化文件:

JDK:

Gson:

ProtoBuf:

发现什么了没?

JDK的最复杂,0000000h-000000c0h表示行号;0-f表示列;行后面的文字表示对这行16进制的解释(PS:notepad打开需要安装hex插件)

Gson次之,包括属性,括号等

ProtoBuf只剩下顺序的属性值了!

@java.lang.Override
public void writeTo(com.google.protobuf.CodedOutputStream output)
                    throws java.io.IOException {
  if (id_ != 0) {
    output.writeInt32(1, id_);//指定位置1
  }
  if (!getUsernameBytes().isEmpty()) {
    com.google.protobuf.GeneratedMessageV3.writeString(output, 2, username_);//指定位置2
  }
  for (int i = 0; i < orders_.size(); i++) {
    com.google.protobuf.GeneratedMessageV3.writeString(output, 3, orders_.getRaw(i));//指定位置3
  }
  if (!getPasswordBytes().isEmpty()) {
    com.google.protobuf.GeneratedMessageV3.writeString(output, 4, password_);//指定位置4
  }
  unknownFields.writeTo(output);
}

另外,ProtoBuf还有最大的特性:数据动态伸缩

打个比方,对于int age = 35这个成员变量来说,如果是JSON或者JDK的情况下,不管怎么样,都是占四个字节,而ProtoBuf采用可伸缩的机制,根据你实际的值来确定所占字节(int(1-5)最多五个字节),那么对于问题很明朗了,世界上超过100岁的人不多吧,超过1000岁的那是神仙了,所以ProtoBuf在age这个成员变量上最多消耗两个字节位!空间就这么省出来了!

	public final void writeUInt32NoTag(int value) throws IOException {
        if (CodedOutputStream.HAS_UNSAFE_ARRAY_OPERATIONS && this.spaceLeft() >= 10) {
            while((value & -128) != 0) {
                //010000000(128)
                //110000000(-128)   ---->010000000 != 0
                UnsafeUtil.putByte(this.buffer, (long)(this.position++), (byte)(value & 127 | 128));
                value >>>= 7; //010000000右移七位 --->01
            }
 
            UnsafeUtil.putByte(this.buffer, (long)(this.position++), (byte)value);
        } else {
            try {
                while((value & -128) != 0) {
                    this.buffer[this.position++] = (byte)(value & 127 | 128);
                    value >>>= 7;
                }
 
                this.buffer[this.position++] = (byte)value;
            } catch (IndexOutOfBoundsException var3) {
                throw new CodedOutputStream.OutOfSpaceException(String.format("Pos: %d, limit: %d, len: %d", this.position, this.limit, 1), var3);
            }
        }
    }

5、JSON和protobuf 互转

5.1、导包、

除了之前的 protobuf-java依赖之外,还需要引入 protobuf-java-uti 依赖

<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.19.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java-util</artifactId>
    <version>3.19.1</version>
</dependency>

如果不使用protobuf提供的JSON API,而使用fastJson等,直接序列化 com.google.protobuf.Message proto对象,会报错。如果希望使用第三方的JSON API,可以重新定义一个实体类,抽取需要的字段。

5.2、注意默认值

官网文档:https://protobuf.dev/programming-guides/proto3/#default

在这里插入图片描述

官方字段默认值,使用时需要注意:
  1. 对于标量消息字段,一旦解析了消息,就无法判断字段是显式设置为默认值还是根本没有设置(例如布尔值是否设置为false):所以,在定义消息类型时应该记住这一点。例如,如果你不希望某些行为在默认情况下也发生,不要使用布尔值在设置为false时打开某些行为。
  2. 如果将标量消息字段设置为其默认值(显式设置),则该值将不会在网络上序列化。
  3. Json 转 proto 对象时,如果Json字符串中的设置为了默认值(显式设置),则该值将不会在网络上序列化。
5.3、protobuf对象 转 JSON串
// 接收数据反序列化:将字节数据转化为对象数据。
UserProtoBuf.User user = UserProtoBuf.User.parseFrom(byteData);

// 1、proto 对象 转 Json
//获取 Printer对象用于生成JSON字符串
JsonFormat.Printer printer = JsonFormat.printer();
String userJsonStr = printer.print(user);

Printer对象生成 JSON字符串时,支持设置一些功能方法。比如:

  • includingDefaultValueFields():表示 Json输出包含默认值(显示和隐式赋默认值)的字段。
  • preservingProtoFieldNames():表示使用.proto文件中定义的原始proto字段名而不是将其转换为lowerCamelCase输出。默认 lowerCamelCase的输出。
5.4、JSON串 转 protobuf对象
// 创建 proto 对象
UserProtoBuf.User.Builder userBuilder = UserProtoBuf.User.newBuilder();

// 2、Json 转 proto 对象
//获取 Parser对象用于解析JSON字符串
JsonFormat.Parser parser = JsonFormat.parser();
parser.merge(userJsonStr2, userBuilder);

Parser对象解析 JSON字符串时,支持设置一些功能方法。比如:

  • ignoringUnknownFields():表示如果 json 串中存在的属性,proto 对象中不存在,则进行忽略,否则会抛出 InvalidProtocolBufferException异常。

  • 注意,json序列化的时候使用 ,空置也要序列化

    String string = JSON.toJSONString(marsComponentListDto, JSONWriter.Feature.WriteNullStringAsEmpty);
    
  • LocalDateTime 在proto映射为String, 按照时间戳进行保存,反序列化的时候会自动改为LocalDateTime

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值