在Java中使用protobuf序列化对象


 

 

什么是protobuf

它是一个对象序列化/反序列化的工具,什么是对象的序列化/反序列化?就是把一个Java堆中存活的对象转换成一串二进制编码,然后该编码可以用于本地存储和网络传输。反序列化就是根据一串二进制编码还原出原来的那个对象,protobuf能够将一个对象以特定的格式转换为一个二进制串(序列化),然后将二进制串还原成对象(反序列化)。这里涉及到两个指标:

对同一个目标对象:

1)序列化和反序列化的时间开销,

2)序列化之后串的长度

protobuf在这两个方面都有非常出色的表现(网传)

 

在Windows下使用protobuf的步骤如下:

第一步:

下载protoc-2.5.0-win32.zip,得到其中的protoc.exe.然后将该protoc.exe的存放路径加入Path环境变量,便于访问。比如,我的protoc.exe存放于D:/protobuf,环境变量中添加如下配置:

D:/protobuf

第二步:

编写.proto文件,它是序列化一个对象的“模板”,protobuf就是根据它来决定如何序列化和反序列化。

编写的person-entity.proto配置文件如下:

option java_outer_classname = "PersonEntity";//生成的数据访问类的类名  
message Person {  
  required int32 id = 1;//同上  
  required string name = 2;//必须字段,在后面的使用中必须为该段设置值  
  optional string email = 3;//可选字段,在后面的使用中可以自由决定是否为该字段设置值
}  

message字段代表了一个对象,所以,可以使用message实现对象的嵌套序列化

required表示是强制字段,在后面的使用中必须为该字段设置值;

optional表示是可选字段,在后面的使用中可选地为该字段设置值;

repeated表示集合类型,可以填充多个数据

后面的1,2,3是字段的编号,字段名是让用户使用的,字段编号则是让系统识别的,从1开始。

protobuf自定义的数据类型和Java的数据类型的对应关系见这篇博客https://blog.csdn.net/chuhui1765/article/details/100670318

 

可以指定字段类型为其他的Message字段类型:

message SearchResponse{
 repeated Result result = 1;
}

message Result{
 required string url = 1;
 optional string title = 2;
 repeated string snippets = 3;
}

可以在message内部嵌套另一个message:

message SearchResponse{

 
 message Result{
  required string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
 }

 repeated Result result = 1;
}

有关protobuf更详细的用法,可以参考这篇优秀的博文https://worktile.com/tech/share/prototol-buffers

第三步:

用protoc.exe生成PersonEntity.class。打开DOS窗口,输入如下的编译命令:

protoc.exe -I=D:/protobuf --java_out=D:/protobuf D:/protobuf/person-entity.proto

编译命令的格式如下:

protoc.exe -I=.protoc文件的存放路径 --java_out=.class文件的输出路径 .protoc文件的具体存放路径

其中第三个路径是必须的,而且要写明.protoc文件

最后会生成PersonEntity.java文件,可以把它理解为一个工具类,帮助我们执行对象的序列化。

第四步:

新建maven项目,将PersonEntity.java复制到项目中,引入maven依赖

        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>2.5.0</version>
        </dependency>

 

利用工具类序列化和反序列化对象。

/*
author:chxy
data:2020/4/1
description:
*/

import java.io.IOException;

public class Test {
    public static void main(String[] args) throws IOException {
        //模拟将对象转成byte[],方便传输
        PersonEntity.Person.Builder builder = PersonEntity.Person.newBuilder();
        builder.setId(1);
        builder.setName("abc");
        builder.setEmail("def");
        PersonEntity.Person person = builder.build();
        System.out.println("after :" +person.toString());
        final byte[] bytes = person.toByteArray();

        System.out.println("===========Person Byte==========");
        for(byte b : person.toByteArray()){
            System.out.print(b);
        }
        System.out.println();

        //反序列化成Person类
        byte[] byteArray =person.toByteArray();
        PersonEntity.Person person2 = PersonEntity.Person.parseFrom(byteArray);
        System.out.println("after :" +person2.toString());
    }
}

这里还犯过一个错:另外自定义一个Person类与PersonEntity.Person相对应。实际上在我们的项目中可以直接利用这个PersonEntity.Person来实现功能需求。

 

下面同样以这个Person类序列化为例,来实际地比较通过各种不同的序列化方式得到的的字节数组长度:

1.protobuf

序列化长度为:

System.out.println(person.getSerializedSize());

结果为:12

2.Java原生序列化

/*
author:chxy
data:2020/4/2
description:
*/

import java.io.*;

public class Test2 {

    public static void main(String[] args) throws IOException {

        Person p = new Person(1,"abc","def");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(
                new FileOutputStream("E:/object.txt"));
        objectOutputStream.writeObject(p);

        FileInputStream fileInputStream = new FileInputStream("E:/object.txt");
        final int length = fileInputStream.read();
        System.out.println(length);
    }
}

结果为:172

3.通过对象的toString()

序列化:重写对象的toString()方法,按照特定的格式,将对象的各个字段封装进一个String字符串,再将字符串按照特定的编码方式,转换成byte数组,实现本地存储和网络传输。

反序列化:对byte数组,重新转换成字符串,再将字符串按照约定的格式,还原成对象的属性,从而构造出原来的对象。

这里重点探讨的是:String对象转换成byte数组之后的长度

public static void main(String[] args) {

        String s = "abc";
        System.out.println(s.getBytes().length);
        String s2 = "嘤嘤嘤";
        System.out.println(s2.getBytes().length);
        System.out.println(Charset.defaultCharset());
}

输出结果为:

3

9

UTF-8

可见,如果是英文字母每个字符占一个字节,如果是汉字,每个字符占3个字节。如果通过这种方式序列化,它的空间效率也是非常高的。

这里有必要搞清楚Java默认的字符编码集:

 /**
     * Encodes this {@code String} into a sequence of bytes using the * platform's default charset, storing the result into a new byte array.
     *
     * <p> The behavior of this method when this string cannot be encoded in
     * the default charset is unspecified.  The {@link
     * java.nio.charset.CharsetEncoder} class should be used when more control
     * over the encoding process is required.
     *
     * @return  The resultant byte array
     *
     * @since      JDK1.1
     */
    public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }

Java采用平台的默认字符编码集来进行字符与二进制byte序列的转换。当然也可以在序列化的时候显示指定字符编码集。

 

Unicode 与 UTF-8

为什么会出现unicode,这首先要从ASCII码说起,它起源最早,用7位来表示英文字母、数字等字符。但是后来又出现了很多其他语言的字符,比如汉字,ASCII码最多只能表示256个字符,无法支持这些”异域“字符,因此出现了unicode码,将各种语言的字符用一种统一的编码方式转换成01序列。但是新的问题又出现了,unicode编码效率不高,原来一个字符a用ASCII表示只需要一个字符,现在用unicode可能用3个字符,存储效率和网络传输效率大打折扣。因此出现了中间转换编码集,utf-8就是其中之一。”UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,又称万国码“,注意,它的编码长度是可变的。所以,unicode是utf-8的基础,utf-8是对unicode的一种解释。 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM