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

目录

 

源码:

protobuf序列化简介:

protobuf3缺失值Null处理:

beanTransfer解释:

使用示例:

beanTransfer测试:

对比评测:

测试用例:

耗时对比:

扫描二维码关注公众号,回复: 6781788 查看本文章

结果分析:

参考资料:


最近在网上调研了很多方法,发现几乎没有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解释:

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

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

https://www.zhihu.com/question/34846173/answer/60302017

https://www.jianshu.com/p/4e2b49fa8ba1

猜你喜欢

转载自blog.csdn.net/singgel/article/details/91869490