1 摘要
Google 的 protocol buffers 协议(简称: protobuf) ,凭借文本体积小、支持多语言、序列化与反序列化优秀等特点在一些场景中应用广泛。本文将介绍如何实现 protobuf 中定义的 message 与 Java 的 POJO bean 的相互转换。
准备工作:
Spring boot 2.5 集成 Google protocol buffer
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 推荐参考资料
5 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.