目录
最近在网上调研了很多方法,发现几乎没有protoBean与javaBean之间转换的工具
- 大家基本上的做法是protobuf与mybatis之间直接使用mybatis的orm映射,不能通过Mybatis的内置转换器转换的时候就自己实现转换器
- 如果使用转换的,是采用protoBean→json→javaBean,采用json作为中间的载体,没有采用二进制流的
- 还有就是采用代码生成器的方式,生成get set
源码:
https://github.com/singgel/RPC-SkillTree/tree/master/grpc-api
github的开源项目,我修改了源码,增加wrapper的支持,但是效率还是底下
set转换:580ms
预加载转换:172167ms
https://github.com/singgel/protobuf-converter
protobuf序列化简介:
规则:
protobuf把消息结果message也是通过 key-value对来表示。只是其中的key是采取一定的算法计算出来的即通过每个message中每个字段(field index)和字段的数据类型进行运算得来的key = (index<<3)|type
Varints算法描述: 每一个字节的最高位都是有特殊含义的,如果是1,则表示后续的字节也是该数字的一部分;如果是0,则结束
效率:
通过protobuf序列化/反序列化的过程可以得出:protobuf是通过算法生成二进制流,序列化与反序列化不需要解析相应的节点属性和多余的描述信息,所以序列化和反序列化时间效率较高
protobuf是由字段索引(fieldIndex)与数据类型(type)计算(fieldIndex<<3|type)得出的key维护字段之间的映射且只占一个字节,相比json与xml文件,protobuf的序列化字节没有过多的key与描述符信息,所以占用空间要小很多
消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对,如下图
采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。
protobuf3缺失值Null处理:
ˇ引自知乎:
- 在 Protobuf 2 中,消息的字段可以加 required 和 optional 修饰符,也支持 default 修饰符指定默认值。默认配置下,一个 optional 字段如果没有设置,或者显式设置成了默认值,在序列化成二进制格式时,这个字段会被去掉,导致反序列化后,无法区分是当初没有设置还是设置成了默认值但序列化时被去掉了,即使 Protobuf 2 对于原始数据类型字段都有 hasXxx() 方法,在反序列化后,对于这个“缺失”字段,hasXxx() 总是 false——失去了其判定意义。
- 在 Protobuf 3 中,更进一步,直接去掉了 required 和 optional 修饰符,所有字段都是 optional 的, 而且对于原始数据类型字段,压根不提供 hasXxx() 方法。来自 Google 的 GRPC 核心成员Eric Anderson 在 StackOverflow 网站很好的解释了这个设计决策的原因:Why required and optional is removed in Protocol Buffers 3
知乎上面有四种方案:
-
- 使用特殊值,例如Double.MAX_VALUE表示null
- 加一个字段,用true false来表示null
- 使用oneof,oneof当做基础类型的封装类可以为null
- 使用wrapper类型,相当于Java里面的装箱类,例如Integer可以为null
雪球采用的最后一种,使用wrapper类型
beanTransfer解释:
|
不采用注释掉的class传递的原因是因为,protobuf和java的反射在进行前需要将class通过invoke获取到实例,然后再进行操作,在bean转换过程中会因编程原因造成效率降低,代码的鲁棒性降低
|
transfer的过程中的异常采用了自定的实现,将运行时异常,如method的反射不可取,filed的access权限,详情见深入理解Java虚拟机
使用示例:
|
beanTransfer测试:
test case:
protoBean(A/B/C)→ javaBean(A/B/C)= javaBean(A/B/C)正常
protoBean(A/B)→ javaBean(A/B/C)= javaBean(A/B/C)c的属性值为default默认值
protoBean(A/B/C)→ javaBean(A/B)= javaBean(A/B)proto的c的属性值忽略
其中:
A、B、C的属性测试了8个基础类型,也测试了collection集合类
protoBean和javaBean测试了继承关系,也就是深度clone
效率:
单机Mac测试,JVM运行内存4G,只做示例参考
单bean有三个属性:
1000双向转换:4365ms
10000双向转换:16860ms
1000000双向转换:631362ms
单bean:
1000map属性filed:488ms
10000map属性filed:1372ms
1000000map属性filed:26785ms
对比评测:
测试用例:
people.proto
|
People.java
|
耗时对比:
测试场景:
Mac 4核,16G
JVM运行4G堆内存
进程启动后Thread.sleep(1000)后执行
spring的beanUtils只是作为映射的指标参考,因为对于protobuf的bean映射,springBeanUtils基本上都是映射不到,默认置为null
loop(次数) |
使用set直接赋值/ms |
使用spring的beanUtils转换/ms |
项目中的beanTransfer转换ms |
---|---|---|---|
100 | 1 | 52 | 135 |
1k | 1 | 81 | 433 |
1w | 2 | 115 | 1916 |
100w | 30 | 809 | 89842 |
结果分析:
由上表中数据可知,项目中的反射代码在请求上升过程中耗时随之呈现出线性增长,系数α明显大于set和SpringBeanUtils,不适用于生产环境,下表给出反射的
测试场景:
1. 测试简单Bean(int,Integer,String)的set方法
2. loop 1亿次
3. 测试代码尽可能避免对象的创建,复发方法的调用,仅仅测试set方法的耗时
(*以下数据来自在探索中前行*,梯子被撤了,官方些的数据都不方便找,网上各种自测数据满天飞)
场景 |
单机测试结果(XP,双核,2G)/ms |
服务器测试结果(Linux,XEN虚拟机,8核,5.5G)/ms |
---|---|---|
场景 |
单机测试结果(XP,双核,2G)/ms |
服务器测试结果(Linux,XEN虚拟机,8核,5.5G)/ms |
方法直接调用 | 235 | 190 |
JDK Method调用 | 29188 | 4633 |
JDK Method调用(稍作优化) | 5672 | 4262 |
Cglib FastMethod调用 | 5390 | 2787 |
得出一个性感的结论:
1.JDK反射效率是直接调用的一个数量级,差不多20倍
2.一个set方法的反射调用时间=4633ms/1亿/3次=0.0154us
3.Cglib的fastmethod具有优势
当然并不是说反射一定比直接调用慢,只是在当前解决grpc bean映射问题的情况下,反射不是一个好的解决方案。
因为java对反射做了很多优化:
1. Method的invoke调用在JDK内部是通过MethodAccessor来调用的,而这个接口有一些不同的实现;
2. 如果某个Method的invoke调用次数较多, 会通过MethodAccessorGenerator的generate方法为Method的目标方法动态字节码生成一个MethodAccessor的实现类, 针对该Method的特征做了代码级的优化,用最少的字节码实现特殊的间接调用;
3. 这个实现类再通过JIT的编译优化, 就能使Method的invoke性能达到最大化.
JDK11中ConcurrentLinkedQueue的高性能原理讲了MethodHandles的应用, 这在JDK8中是用Unsafe类实现的, 既然现在不用Unsafe了, 说明MethodHandles有类似Unsafe的高性能.
参考资料:
https://zhuanlan.zhihu.com/p/46603988
https://developers.google.com/protocol-buffers/docs/javatutorial
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html
https://zhuanlan.zhihu.com/p/21423208
https://zhuanlan.zhihu.com/p/55075493