Google protobuf 与 Java POJO bean 相互转换

1 摘要

Google 的 protocol buffers 协议(简称: protobuf) ,凭借文本体积小、支持多语言、序列化与反序列化优秀等特点在一些场景中应用广泛。本文将介绍如何实现 protobuf 中定义的 message 与 Java 的 POJO bean 的相互转换。

准备工作:

Spring boot 2.5 集成 Google protocol buffer

Protubuf 定义复杂格式的 message

2 核心 Maven 依赖

./demo-mybatis-plus/pom.xml
        <!-- protocol buffer support -->
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-all</artifactId>
            <version>${grpc.version}</version>
        </dependency>

其中 ${protobuf.version} 的版本为 3.17.3${grpc.version} 的版本为 1.39.0

3 核心代码

3.1 protobuf 文件
./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/model/protobuf/Student.proto
syntax = "proto3";

option optimize_for = CODE_SIZE;
option java_package = "com.ljq.demo.springboot.mybatisplus.model.entity";
option java_outer_classname = "StudentPb";

// 学生信息
message Student {

  // id
  int64 id = 1;
  // 姓名
  string name = 2;
  // 出生日期(时间戳)
  int32 birth_date = 3;
}

// 班级信息
message ClassInfo {

  // id
  int64 id = 1;
  // 年级
  int32 grade = 2;
  // 班级编号
  int32 number = 3;

}

// 学生选修课
message ElectiveCourse {

  // 学生 id
  int64 stu_id = 1;
  // 课程名称
  repeated string course_names = 2;

}

// 学生班级信息
message StudentClass {

  // id
  int64 id = 1;
  // 学生信息
  Student student = 2;
  // 班级信息
  ClassInfo class_info = 3;

}

// 性别
message StudentSex {
  enum Sex{
    MAN = 0;
    WOMEN = 1;
  }
  Sex sex = 2;
}

// 教师信息
message Teacher {
  // id
  uint64 id = 1;
  // 姓名
  string name = 2;
}
./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/model/protobuf/Tree.proto
syntax = "proto3";

option optimize_for = CODE_SIZE;
option java_package = "com.ljq.demo.springboot.mybatisplus.model.entity";
option java_outer_classname = "TreePb";

import "com/ljq/demo/springboot/mybatisplus/model/protobuf/Student.proto";

// 学生植树
message Tree {
    // 树木品种
    string tree_type = 2;
    // 植树时间
    uint32 plant_date = 3;
    // 种树学生信息
    Student student = 4;
    // 学生选修课
    ElectiveCourse elective_course = 5;
    // 教师
    repeated Teacher teachers = 6;
}

其中 Tree.proto 文件引用了外部的 Student.proto 文件

3.2 Java PoJo 类

Tree 实体类中包含常用的复杂数据结构,其中包含了普通字段、对象属性、列表属性

./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/model/entity/TreeEntity.java
package com.ljq.demo.springboot.mybatisplus.model.entity;

import com.ljq.demo.springboot.mybatisplus.model.param.student.StudentReceiveParam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * @Description: 学生植树
 * @Author: junqiang.lu
 * @Date: 2021/8/17
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TreeEntity implements Serializable {

    private static final long serialVersionUID = 3727053267859322959L;

    /**
     * 数目品种
     */
    private String treeType;
    /**
     * 植树时间
     */
    private Integer plantDate;
    /**
     * 学生信息
     */
    private StudentReceiveParam student;
    /**
     * 选修课
     */
    private ElectiveCourseEntity electiveCourse;
    /**
     * 教师列表
     */
    private List<TeacherEntity> teachers;

}

学生信息 java bean

./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/model/param/student/StudentReceiveParam.java
package com.ljq.demo.springboot.mybatisplus.model.param.student;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @Description: 接收学生信息(pb)
 * @Author: junqiang.lu
 * @Date: 2021/8/7
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "接收学生信息(pb)", description = "查询学生信息")
public class StudentReceiveParam implements Serializable {

    private static final long serialVersionUID = -8856061586998092638L;

    /**
     * id
     */
    @ApiModelProperty(value = "id", required = true, example = "1")
    private Long id;
    /**
     * 姓名
     */
    @ApiModelProperty(value = "姓名", required = true)
    private String name;
    /**
     * 出生日期(时间戳,精确到秒)
     */
    @ApiModelProperty(value = "出生日期(时间戳,精确到秒)", required = true, example = "1")
    private Integer birthDate;


}

选修课实体类,选修课中包含列表

./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/model/entity/ElectiveCourseEntity.java
package com.ljq.demo.springboot.mybatisplus.model.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * @Description: 选修课
 * @Author: junqiang.lu
 * @Date: 2021/8/17
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ElectiveCourseEntity implements Serializable {

    private static final long serialVersionUID = -9214802138019634356L;

    /**
     * 学生 id
     */
    private Long stuId;
    /**
     * 课程名称
     */
    private List<String> courseNames;

}

教师信息

./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/model/entity/TeacherEntity.java
package com.ljq.demo.springboot.mybatisplus.model.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;

/**
 * 教师实体类
 *
 * @author junqiang.lu
 * @date 2020-10-09 17:27:32
 */
@Data
@TableName(value = "teacher", resultMap = "teacherMap")
@ApiModel(value = "教师", description = "教师")
public class TeacherEntity implements Serializable {

	private static final long serialVersionUID = 1L;

    /**
     * id
     **/
    @TableId(value = "ID", type = IdType.AUTO)
    @ApiModelProperty(value = "id", name = "id")
    private Long id;
    /**
     * 姓名
     **/
    @TableField(value = "NAME")
    @ApiModelProperty(value = "姓名", name = "name")
    private String name;

}
3.3 protobuf bean 转换工具类
./demo-mybatis-plus/src/main/java/com/ljq/demo/springboot/mybatisplus/common/util/ProtobufBeanUtil.java
package com.ljq.demo.springboot.mybatisplus.common.util;

import com.google.gson.Gson;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;

import javax.validation.constraints.NotNull;
import java.io.IOException;

/**
 * @Description: protobuf bean 转换工具类
 * @Author: junqiang.lu
 * @Date: 2021/8/9
 */
public class ProtobufBeanUtil {

    private ProtobufBeanUtil(){
    }

    /**
     * 将ProtoBean对象转化为POJO对象
     *
     * @param destPojoClass 目标POJO对象的类类型
     * @param sourceMessage 含有数据的ProtoBean对象实例
     * @param <PojoType> 目标POJO对象的类类型范型
     * @return
     * @throws IOException
     */
    public static <PojoType> PojoType toPojoBean(@NotNull Class<PojoType> destPojoClass, @NotNull Message sourceMessage)
            throws IOException {
        String json = JsonFormat.printer().print(sourceMessage);
        return new Gson().fromJson(json, destPojoClass);
    }

    /**
     * 将POJO对象转化为ProtoBean对象
     *
     * @param destBuilder 目标Message对象的Builder类
     * @param sourcePojoBean 含有数据的POJO对象
     * @return
     * @throws IOException
     */
    public static void toProtoBean(@NotNull Message.Builder destBuilder, @NotNull Object sourcePojoBean) throws IOException {
        String json = new Gson().toJson(sourcePojoBean);
        JsonFormat.parser().merge(json, destBuilder);
    }


}
3.4 测试示例

基本属性赋值与压力测试

./demo-mybatis-plus/src/test/java/com/ljq/demo/springboot/mybatisplus/common/util/ProtobufBeanUtilTest.java
package com.ljq.demo.springboot.mybatisplus.common.util;

import com.ljq.demo.springboot.mybatisplus.model.entity.StudentPb;
import com.ljq.demo.springboot.mybatisplus.model.param.student.StudentReceiveParam;
import org.junit.Test;

import java.io.IOException;


public class ProtobufBeanUtilTest {

    @Test
    public void toPojoBean() throws IOException {
        // 赋值
        StudentPb.Student student = StudentPb.Student.newBuilder()
                .setId(111L)
                .setName("张三")
                .setBirthDate(1628233076)
                .build();
        // 属性拷贝
        StudentReceiveParam receiveParam = ProtobufBeanUtil.toPojoBean(StudentReceiveParam.class, student);
        System.out.println(receiveParam);
    }

    @Test
    public void toProtoBean() throws IOException {
        StudentReceiveParam receiveParam = new StudentReceiveParam();
        receiveParam.setId(123L);
        receiveParam.setName("李四");
        receiveParam.setBirthDate(1628233076);
        // 属性拷贝
        StudentPb.Student.Builder studentBuilder = StudentPb.Student.newBuilder();
        ProtobufBeanUtil.toProtoBean(studentBuilder, receiveParam);
        // 打印
        System.out.println("id: " + studentBuilder.getId());
        System.out.println("name: " + studentBuilder.getName());
        System.out.println("birth_date: " + studentBuilder.getBirthDate());
    }

    /**
     * proto to pojo 压力测试
     *
     * @throws IOException
     */
    @Test
    public void toPojoBeanPressure() throws IOException {
        // 赋值
        StudentPb.Student student = StudentPb.Student.newBuilder()
                .setId(111L)
                .setName("张三")
                .setBirthDate(1628233076)
                .build();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            StudentReceiveParam receiveParam = ProtobufBeanUtil.toPojoBean(StudentReceiveParam.class, student);
        }
        long stop = System.currentTimeMillis();
        System.out.println("耗时: " + (stop-start));
    }

    /**
     * pojo to proto 压力测试
     */
    @Test
    public void toProtoBeanPressure() throws IOException {
        StudentReceiveParam receiveParam = new StudentReceiveParam();
        receiveParam.setId(123L);
        receiveParam.setName("李四");
        receiveParam.setBirthDate(1628233076);
        // 属性拷贝
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            StudentPb.Student.Builder studentBuilder = StudentPb.Student.newBuilder();
            ProtobufBeanUtil.toProtoBean(studentBuilder, receiveParam);
        }
        long stop = System.currentTimeMillis();
        System.out.println("耗时: " + (stop-start));

    }
}

复杂参数格式对象转换测试

./demo-mybatis-plus/src/test/java/com/ljq/demo/springboot/mybatisplus/model/entity/TreeVoTest.java
package com.ljq.demo.springboot.mybatisplus.model.entity;

import com.ljq.demo.springboot.mybatisplus.common.util.ProtobufBeanUtil;
import com.ljq.demo.springboot.mybatisplus.model.param.student.StudentReceiveParam;
import org.junit.Test;

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

public class TreeVoTest {

    /**
     * 测试-赋值
     */
    @Test
    public void value() {
        TreePb.Tree tree = TreePb.Tree.newBuilder()
                .setTreeType("杨树")
                .setPlantDate(1628233076)
                .setStudent(StudentPb.Student.newBuilder()
                        .setId(123L)
                        .setName("王五")
                        .setBirthDate(1628233076))
                .setElectiveCourse(StudentPb.ElectiveCourse.newBuilder()
                        .setStuId(123L)
                        .addCourseNames("语文")
                        .addCourseNames("数学")
                        .addCourseNames("物理"))
                .addTeachers(StudentPb.Teacher.newBuilder()
                        .setId(666L)
                        .setName("化学王子秦老师"))
                .addTeachers(StudentPb.Teacher.newBuilder()
                        .setId(888L)
                        .setName("马保国"))
                .build();
        // 输出值
        System.out.println("tree type: " + tree.getTreeType());
        System.out.println("plant date: " + tree.getPlantDate());
        System.out.println("tree protobuf: " + tree);

    }

    @Test
    public void convertToPojo() throws IOException {
        TreePb.Tree tree = TreePb.Tree.newBuilder()
                .setTreeType("杨树")
                .setPlantDate(1628233076)
                .setStudent(StudentPb.Student.newBuilder()
                        .setId(123L)
                        .setName("王五")
                        .setBirthDate(1628233076))
                .setElectiveCourse(StudentPb.ElectiveCourse.newBuilder()
                        .setStuId(123L)
                        .addCourseNames("语文")
                        .addCourseNames("数学")
                        .addCourseNames("物理"))
                .addTeachers(StudentPb.Teacher.newBuilder()
                        .setId(666L)
                        .setName("化学王子秦老师"))
                .addTeachers(StudentPb.Teacher.newBuilder()
                        .setId(888L)
                        .setName("马保国"))
                .build();
        TreeEntity treeEntity = ProtobufBeanUtil.toPojoBean(TreeEntity.class, tree);
        System.out.println("tree entity: " + treeEntity);

    }

    @Test
    public void convertToProtobuf() throws IOException {
        List<String> courseNameList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            courseNameList.add("科目" + (i+1));
        }
        List<TeacherEntity> teacherList = new ArrayList<>();
        TeacherEntity teacher;
        for (int i = 0; i < 3; i++) {
            teacher = new TeacherEntity();
            teacher.setId((i+1L));
            teacher.setName("老师" + (i+1));
            teacherList.add(teacher);
        }
        TreeEntity treeEntity = TreeEntity.builder()
                .treeType("梧桐树")
                .plantDate(1628233076)
                .student(StudentReceiveParam.builder()
                        .id(1234L)
                        .name("张三")
                        .birthDate(1628233076)
                        .build())
                .electiveCourse(ElectiveCourseEntity.builder()
                        .stuId(1234L)
                        .courseNames(courseNameList)
                        .build())
                .teachers(teacherList)
                .build();
        // 树木信息
        System.out.println("TreeEntity: " + treeEntity);
        // pojoBean -> protobuf
        TreePb.Tree.Builder tree = TreePb.Tree.newBuilder();
        ProtobufBeanUtil.toProtoBean(tree, treeEntity);
        System.out.println("tree protobuf:" + tree.build());


    }

}

测试结果:

复杂结构的对象也能够转换,使用工具类转换极大地提升了属性复制的效率。

4 推荐参考资料

protobuf 与 Java 对象互转

Protobuf与POJO的相互转化 - 通过Json

5 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值