protoBean与javaBean的转换,与Java反射效率测试

358 阅读6分钟

目录

 

源码:

protobuf序列化简介:

protobuf3缺失值Null处理:

beanTransfer解释:

使用示例:

beanTransfer测试:

对比评测:

测试用例:

耗时对比:

结果分析:

参考资料:


以下内容摘自雪球,在公司内部的docs上的内容总结,部分隐私信息已经处理改动


xueqiu.com/

最近在网上调研了很多方法,发现几乎没有protoBean与javaBean之间转换的工具

  • 大家基本上的做法是protobuf与mybatis之间直接使用mybatis的orm映射,不能通过Mybatis的内置转换器转换的时候就自己实现转换器
  • 如果使用转换的,是采用protoBean→json→javaBean,采用json作为中间的载体,没有采用二进制流的
  • 还有就是采用代码生成器的方式,生成get set

源码:

github.com/singgel/RPC…

github的开源项目,我修改了源码,增加wrapper的支持,但是效率还是底下

set转换:580ms
预加载转换:172167ms

github.com/singgel/pro…

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解释:

public static <T> T protoToJava(Object userBean, Object protoBean) {     ``TransferBean transferBean = ``new TransferBean(userBean, protoBean);     ``ProtoBeanToJavaBean protoBeanToJavaBean = ``new ProtoBeanToJavaBean();     ``return (T) protoBeanToJavaBean.proto2java(transferBean); } /**  ``* 不使用 public static <T> T protoToService(Object protoBean, Class<T> userBean){     ``TransferBean transferBean = new TransferBean(userBean,protoBean);     ``ProtoBeanToService toService = new ProtoBeanToService();     ``return (T) toService.proto2service(transferBean); } */

不采用注释掉的class传递的原因是因为,protobuf和java的反射在进行前需要将class通过invoke获取到实例,然后再进行操作,在bean转换过程中会因编程原因造成效率降低,代码的鲁棒性降低

public class TransferException  ``extends RuntimeException {     ``public TransferException(String msg) {         ``super``(msg);     ``}       ``public TransferException(String msg, Throwable cause) {         ``super``(msg, cause);     ``}       ``@Override     ``public boolean equals(Object other) {         ``if (``this == other) {             ``return true``;         ``} ``else if (!(other ``instanceof TransferException)) {             ``return false``;         ``} ``else {             ``TransferException otherBe = (TransferException)other;             ``return this``.getMessage().equals(otherBe.getMessage()) && ObjectUtils.equals(``this``.getCause(), otherBe.getCause());         ``}     ``}       ``@Override     ``public int hashCode() {         ``return this``.getMessage().hashCode();     ``} }

transfer的过程中的异常采用了自定的实现,将运行时异常,如method的反射不可取,filed的access权限,详情见深入理解Java虚拟机

使用示例:

//CarKeyPackage javaBean类 //CarKeyPackageProto.CarKeyPackage protobuf生成类 public static void main(String args[]) ``throws NoSuchFieldException {         ``CarKeyPackage carKeyPackage = ``new CarKeyPackage();         ``carKeyPackage.setOwener(``"张三"``);           ``Map<String, String> map = ``new HashMap<>();         ``map.put(``"xueqiu"``, ``"雪球"``);         ``map.put(``"xueying"``, ``"雪盈"``);         ``carKeyPackage.setCarkeyMap(map);           ``Map<String, People> peopleMap = ``new HashMap<>();         ``peopleMap.put(``"1"``, createServicePeople(``1``));         ``peopleMap.put(``"2"``, createServicePeople(``2``));         ``carKeyPackage.setPeopleMap(peopleMap);           ``CarKeyPackageProto.CarKeyPackage protobean = TransferBeanUtil.javaToProto(carKeyPackage, CarKeyPackageProto.CarKeyPackage.newBuilder());         ``System.out.println(protobean);           ``CarKeyPackage serviceBean = TransferBeanUtil.protoToJava(``new CarKeyPackage(), protobean);         ``System.out.println(serviceBean);   }

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

syntax = ``"proto3"``;   package netty;   import "google/protobuf/wrappers.proto"``; option java_package = ``"com.xueqiu.infra.grpc.lib.bean"``; option java_outer_classname = ``"PeopleProto"``;   message People {     ``int32 personId = ``1``;     ``string personName = ``2``;     ``google.protobuf.StringValue  sex = ``3``;     ``repeated string address = ``4``; }

People.java

package com.xueqiu.infra.grpc.lib.bean;   import java.util.List;   public class People {     ``private int personId;       ``private String personName;       ``private String sex;       ``List<String> address;         ``@Override     ``public String toString() {         ``return "People{" +                 ``"personId=" + personId +                 ``", personName='" + personName + '\``'' +                 ``", sex='" + sex + '\``'' +                 ``", address=" + address +                 ``'}'``;     ``}       ``public int getPersonId() {         ``return personId;     ``}       ``public void setPersonId(``int personId) {         ``this``.personId = personId;     ``}       ``public String getPersonName() {         ``return personName;     ``}       ``public void setPersonName(String personName) {         ``this``.personName = personName;     ``}       ``public String getSex() {         ``return sex;     ``}       ``public void setSex(String sex) {         ``this``.sex = sex;     ``}       ``public List<String> getAddress() {         ``return address;     ``}       ``public void setAddress(List<String> address) {         ``this``.address = address;     ``} }

耗时对比:

测试场景:

Mac 4核,16G

JVM运行4G堆内存

进程启动后Thread.sleep(1000)后执行

spring的beanUtils只是作为映射的指标参考,因为对于protobuf的bean映射,springBeanUtils基本上都是映射不到,默认置为null

loop(次数)使用set直接赋值/ms使用spring的beanUtils转换/ms项目中的beanTransfer转换ms
100152135
1k181433
1w21151916
100w3080989842

结果分析:

由上表中数据可知,项目中的反射代码在请求上升过程中耗时随之呈现出线性增长,系数α明显大于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
方法直接调用235190
JDK Method调用291884633
JDK Method调用(稍作优化)56724262
Cglib FastMethod调用53902787

得出一个性感的结论:

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的高性能.

参考资料:

zhuanlan.zhihu.com/p/46603988

developers.google.com/protocol-bu…

docs.spring.io/spring/docs…

zhuanlan.zhihu.com/p/21423208

zhuanlan.zhihu.com/p/55075493

www.zhihu.com/question/34…

www.jianshu.com/p/4e2b49fa8…