基于字节码技术的对象映射工具框架

摘要

  本文详细阐述一种基于字节码技术的对象映射工具框架及其实现方法。并与现有开源工具框架详细对比测试。自研BeanTransform工具支持任意原始类型互转、任意包装类型互转、自定义嵌套实体类型的深拷贝或者映射互转等情况。对于复杂场景,比如N层数组类、N层Map类、N层Collection 类、N层Collection转N维数组或者以上类型的相互嵌套情况均可适配兼容,优于现有开源框架。映射转换效率方面,不同场景略有差异。在绝大部分场景下,实体类字段基本为基础类型或者常规自定义类型,少量Collection或者Map字段类型。这种场景下,自研工具框架吞吐量均值与Manual get/set 方式的理论极限相差约3.88%,基本接近理论极限,排在第一位,略高于第二名的Selma 框架 1.52%。与其他开源工具框架相比有显著性能优势。在少数特殊场景下,比如所有实体类字段类型均为复杂类型,自研工具框架也大大优于反射框架。不过,该场景Selma 框架效率更高,基本接近manual get/set 理论极限,高出自研工具框架约65%。性能方面综合而言,自研工具框架和Selma框架的转换效率均非常优秀,不同场景下效率各有优劣。此外,自研工具框用户接口设计简洁,使用方便,运行稳定。自研工具框架支持动态生成转换类,满足各类动态转换需,因此在动态性方面,自研工具框架相比于Selma 和MapStruct 这类编译型框架有显著优势。总体而言,自研BeanTransform工具功能强大,是bean 对象深度映射转换或者深拷贝方面的优选工具框架。

1 背景

  在面向对象的编程语言中经常会碰到bean 对象映射转换或者拷贝的应用场景。比如,java 软件体系中无论是MVC式的三层架构,还是DDD领域驱动式的架构。总会有各种DTO、DO、PO、VO之间的转换需求,又或者在软件平台对外接口实体向内层算法库(和平台基于同一语言开发)传递时,两层之间的对象实体参数由于各种原因无法完全一致,需要做对象协议转换等场景。最常用的是Manual的方式对象字段重新转换赋值,这种方式效率最高,占用的内存开销最小,但是这种方法也有致命弱点,不具有抽象性、通用性,换一种场景则需要重新硬编码,耗费时间、人力,如果是字段数很少的实体赋值转换建议采用这种方式,如果字段很多或者需要完成任意类型转换,这种弊端就凸显出来了。因此,需要实现一种多模式Bean转换通用工具可以让编程人员从这类繁琐、枯燥、容易出错且无创造性的工作中解放出来,提高自动化程度和编程效率。

2 现状

 这类工具应用需求旺盛,复用性强。开源世界里有spring,oracle,Apache等社群、公司或者个人贡献的bean copy框架,功能及性能各异。 运行模式大体分为三类:一类是运行期 反射调用set/get 或者是直接对成员变量赋值 。 该方式通过invoke执行赋值,实现时一般会采用beanutil, Javassist等开源库。这类的代表:Dozer,ModelMaper;第二类是通过代码动态生成转换程序源码,然后调用编译器动态生成set/get代码的class文件 ,在运行时直接调用该class文件。该方式实际上扔会存在set/get代码,只是不需要自己写了。 这类的代表:MapStruct、Selma、Orika;第三种是运行时动态生成get/set 字节码类,通过自定义类加载器运行。代表工具是cglib 的BeanCopier。

这些通用工具在功能、性能、使用便捷性功能拓展性等维度上各有千秋。下图是常用转换框架思维导图。

java bean 转成json 在线工具类_java

github 上有人对比了不同bean转换工具之间的性能,链接如下
https://github.com/arey/java-object-mapper-benchmark

java bean 转成json 在线工具类_java_02

 重点讨论MapStruct和Selma ,从上面可以看出MapStruct和Selma 性能和Manual 方式调用get set 效率大体相当。MapStruct和Selma都是基于JSR 269的Java注解处理器实现。MapStruct和Selma 用户文档完善,官网链接如下:
mapstruct.orgselma-java.org   MapStruct需要预先定义一个映射器接口,声明任何需要映射的方法,拓展性很强。在编译过程中,MapStruct将生成该接口的实现。此实现使用纯Java的方法调用源对象和目标对象之间进行映射,并非Java反射机制。换言之,他根据接口定义生成我们原本需要手动实现的bean 转换的java 源代码。使用时需要重新编译java 文件到字节码文件(Java源码编译过程:分析和输入到符号表-> 解处->语义分析和生成class文件)这也就意味着无法在运行时动态生成转换类,灵活性降低。对于集合类或者Map类,MapStruct框架也可自动生成转换代码,深拷贝还是浅拷贝取决于用户注解配置。使用方法示例如下:mappingControl = DeepClone.class 是指定深拷贝模式,不指定则默认浅拷贝,浅拷贝时集合类是底层是调用Array 的copy 方法。如果是深拷贝模式,MapStruct框架会生成集合遍历代码,集合中元素如果是引用类型会生成引用类型转换代码,层层转换,深度拷贝。

@Mapper(mappingControl = DeepClone.class)
public interface MapperStructConvert {
     MapperStructConvert INSTANCE = Mappers.getMapper(MapperStructConvert.class);

    CopyTo transform(CopyFrom from);
}

不过从使用情况来看,深拷贝模式下,对集合类拷贝的限制比较多,不支持多层嵌套集合类深拷贝,而且要求源字段和目标字段集合类型严格一致,比如List 到Set 或者List 到Dquene 情况下无法自动转换,编译会报错,需要用户手动实现,场景兼容性略有不足。比如以下两个字段
目标类字段

private Set<Inner> innerSet;

源类字段

private List<Inner> innerSet;

这两个字段mapStruct 自动忽略,不做转换。
或者以下字段,编译会报错。
源类字段:

private Set<List<List<Integer>>> threeNestList;

目标类字段:

private List<List<List<Double>>> threeNestList=new ArrayList<>();
Error:(20, 12) java: Can't map property "List<List<List<Double>>> threeNestList" to "Set<List<List<Integer>>> threeNestList". Consider to declare/implement a mapping method: "Set<List<List<Integer>>> map(List<List<List<Double>>> value)".

mapstruct 也无法自动转换多层嵌套集合类,无法编译通过。总之,对于单层异类集合、Map或者是有多层集合、Map等特殊场景,MapSturt 框架都无法自动转换。
  Selma 和MapStruct 类似,也需要预先定义一个映射器接口。使用方法:

@Mapper
public interface SelmaMapper {
    // This will build a fresh new OrderDto
    CopyTo  asCopyTo(CopyFrom  in);

}

不过如果基础类型类型不一致,或者两个实体之间字段无法一一对应时会抛出异常,需要通过方法mapper注解配置withIgnoreFields属性,忽略对应字段,不做转换。Selma 功能比MapStruct 更加强大,支持多层集合、多层Map、多维数组深度转换。但不支持Collection 与数组转换
  这篇报告中未对比cglib 的BeanCopier, 这款基于cglib 动态代理(内部使用字节码生成技术)的工具性能也非常好,使用非常便捷,效率上和Manual 方式的效率相当。不过这个工具功能上有不足,对引用类型,BeanCopier直接赋值,意味着对自定义嵌套类、数组、集合类或者Map类等引用类型共享数据单元,会导致数据相互影响,而且对数组、集合类或者Map 类的内部元素一概不作转换处理,这点是相当粗暴的。MapStruct、Selma在这一功能点上比BeanCopier更加实用的。而且原始类型、包装类型 之间不能自动化转换,还有不同名称字段也无法自动转换。由于它内部也是基于字节码生成方式来处理的,字节码生成过程对以上问题的处理比较难,在代码框架结构设计是个挑战。好在可以继承它的Convert 接口自定义实现深拷贝。当然,前面提到的原始类型、包装类型不能自动互转问题如果也通过自定义Convert 实现那就麻烦了。也就是说大部分场景下使用BeanCopier都无法自动实现深拷贝,都需要用户定制实现。自动化程度略低。所以如果使用cglib 的BeanCopier ,建议还是同名同类型且字段是是基础类型的场景(ps:场景兼容性不是很强)。
  spring 和apache的 BeansUtils 基于java反射实现,可以实现深拷贝,由于反射效率比较低,在大数据量场景且对性能有较高要求选用这两款工具会导致应用的整体性能下降,因此使用时需要慎重考虑。

3 自研工具

  综上分析,各类开源工具各有差异,用户可根据场景自行选用。为了在功能、性能、使用便捷性、拓展性四个维度都能够有较为满意的效果,决定自研一款bean 转换工具。

3.1 需求分析

工具需求分析如下:

  1. 尽量降低对应用代码侵入性低,使用方便,开箱即用。
  2. 支持深拷贝和浅拷贝两种模式,运行效率高,与Manual 方式性能接近。
  3. 支持不同基础类型,包装类型之间的转换
  4. 支持用户自定义转换方式,通过注解方式配置。
  5. 如果目标类字段和源类字段是Collection 、Map类型、数组类型或者三者嵌套(可以多层嵌套),对应的内部元素类类型一致或者类型不一致情况均可实现自动化转换,且是深拷贝模式,前提是内部元素类的字段可以对应,对应规格可自定义。比如 Set<List<...List<SetElement>...>> 类型 到 List<List...<List<ListElement>...>> 转换
  6. 在自动转换模式下可实现N维常规数组类型互转,或者N常规数组类与N层Collection 集合类互转,且能实现深拷贝
  7. 复杂类型,比如泛型类型变量、通配泛型、泛型数组、参数化泛型(Collection 或者Map 也是参数化泛型中的一类特例)自动换转换比较复杂(非自动换转换比如用户自定义转换方式可解决,但需要人工实现部分编码,使用便携度降低),工具框架可先实现部分复杂类型转换,框架需具备拓展接口,可集成第三方开发者实现类。
  8. 转换工具类可动态加载,最好无需编译即可运行
    第五、第六点目前开源的工具框架基本不支持或者支持程度不高,无法适配大多数应用场景。

3.1技术选型

  实现方式和现有开源框架中三种方式类似,java Reflection 是最容易想到也最便于实现的方式。但Reflection 是重量级类,执行效率一般,在性能要求很高的场景不适用。其次是像MapStruct 工具框架那样,基于需要转换的类meta 信息 自动生成java 源代码文件,并调用JavaCompiler 编译后生成字节码文件并加载运行。这种方式生成的转换类执行效率很高,实现难度上比java Reflection 要难一些,毕竟要通过代码编写代码需要精心编码文本文件,对符号和关键字的处理比较棘手,不过这种方式还需要每次编译,无法满足上述第七点描述的“动态加载、无需编译”需求,当然这个需求优先级不高,属于锦上添花功能,可弱化。第三种方式,也是基于JVM 体系的编程语言所能做到的最快速同时也最难处理的终极方式,即,编写字节码文件方式,直接操作虚拟机指令完成功能。生成的文件就是可执行的class 文件。为什么说这种是JVM 体系的编程语言所能做到的终极方式?实际上直接写汇编语言文件才算是终极方式(这年头也不会再有人玩机器语言了吧,如果还有的话那可以用不疯魔不成活来形容这类人),但是对于JVM 体系的语言来说,虚拟机运行的基本文件就是class 字节码文件,上层用户无法使用更底层的汇编代码放在虚拟机上执行。
本工具实现采用第三种方式,优点不必多说,缺点就是实现难度比较大,需要熟悉JVM 标准规范和指令集,而且代码结构设计很复杂,尤其是深度映射拷贝场景下,编写字节码指令复杂度很高。此外,代码是运行时动态生成、动态加载,调试不方便。
确立这种方式后还需要选择一款可编写字节码的框架,目前主流的有两种Javassist和ASM,二者对比如下:

  1. Javassist源代码级API比ASM中实际的字节码操作更容易使用
  2. Javassist在复杂的字节码级操作上提供了更高级别的抽象层。Javassist源代码级API只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易。
  3. Javassist使用反射机制,这使得它比运行时使用Classworking技术的ASM慢。
    总的来说ASM比Javassist快得多,并且提供了更好的性能。Javassist使用Java源代码的简化版本,然后将其编译成字节码。这使得Javassist非常容易使用,但是它也将字节码的使用限制在Javassist源代码的限制之内。
    要追求性能最高,灵活度最高,优先选择ASM 来实现转换工具。关于ASM,最早可追溯到在2002年的时候,Eric Bruneton、Romain Lenglet和Thierry Coupaye发表了一篇论文,名为:
    《 ASM: a code manipulation tool to implement adaptable systems》。在这篇文章当中,他们提出了ASM的设计思路。论文摘要如下,详情可点击链接查看论文PDF文件。

java bean 转成json 在线工具类_java_03

3.4 设计与实现

3.4.1 基础知识

 工具实现采用ASM 实现字节码文件编写,涉及java虚拟机字节码规范,虚拟机指令以及ASM 框架API。关于java虚拟机字节码规范,可参考oracle JVM官方描述:
https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html#jvms-2.1 工具要创建类文件需要按照这个规范来实现。以下是class 文件格式(采用类似于C 结构体的描述形式)。具体解释请查看官方说明。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

 另外一块就是方法体所需要的字节码指令集合,逻辑的实现需要通过字节码指令来实现。官方描述:
A Java Virtual Machine instruction consists of a one-byte opcode specifying the operation to be performed, followed by zero or more operands supplying arguments or data that are used by the operation. Many instructions have no operands and consist only of an opcode
java 指令集是基于栈结构的(Instruction Set Architecture, ISA)。和汇编(基于寄存器)的指令有些差异。栈结构指令主要的优点就是可移植高,代码相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数),编译器实现更加简单(不需要考虑空间分配的问题,所需空间都在栈上操作)。缺点是完成相同功能的指令数量一般会比寄存器架构多,略微比较慢。但就目前硬件的性能而言,这种性能差异几乎可忽略。
JVM 指令系统官方说明文档链接如下:
https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html#jvms-2.11.1-220 比如常用的Type support in the Java Virtual Machine instruction set 如下,其他的比如方法调用指令、异常处理指令,同步指令,分支跳转指令、对象创建指令、栈管理指令等官方文档均由说明。

java bean 转成json 在线工具类_java_04

 最后一块本工具涉及的知识是ASM 框架。ASM 框架有Core API和Tree API,本工具使用Core API,二者差异类似于xml SAX 和DOM API。需要学习研究的可查看看官网文档链接:
https://asm.ow2.io/asm4-guide.pdf
Core API 主要类图结构如下:

java bean 转成json 在线工具类_字段_05

以上简要描述了本工具所涉及的理论知识体系和相应的操作框架。限于篇幅,本文不做展开描述。有兴趣者可自行查阅资料学习研究。

3.4.1 多模式Bean 转换工具框架设计

  本工具设计上分为三层。工具框架设计图如下:

java bean 转成json 在线工具类_字节码_06

  底层模块实现解析以及类加载功能。类元解析子模块完成Bean 实体类字段、注解的反射处理,获取字段信息以及其他的用户注解配置信息。配置处理子模块负责解析工具包中指定的环境变量值,包括生成类是否写入到本地文件,生成类缓存空间阈值等信息。自定义类加载器子模块也是工具包的底层模块,负责加载动态生成的类信息,加载器可缓存生成类Class byte数组,避免重复生成类元信息,提高效率。
  中间层是本工具核心层。通过ClassWriter 依次生成转换类的类名信息、继承接口,方法体申明、字段申明等,转换方法的字节码通过MethodVisitor根据解析的Bean 类信息生成对应的转换指令。java 字段的Type包含五大类: 常规Class类型,参数化泛型、类型变量、泛型数组、通配泛型等。在自动转换模式下,其两两排序组合共有25种。实体类中参数化泛型、类型变量、泛型数组、通配泛型使用情况很少,因此这几类建议用户引入工具包后在工程代码中继承工具包中的拓展类接口实现转换,第三方开发者也可以自行开发转换策略注册到上下文环境中,在github 中提交。源类字段以及目标类字段均为常规Class类型的情况框架可自动化处理转换。常规Class类型也包括多种情况。包括数组类、N层Map类、N层Collection 类、自定义实体类、原始类型、包装类型、其他第三方定义类型(比如jdk 内部定义的Class类型)。,**排序组合后任意两种之间的转换逻辑代码均有差异。抽象共性逻辑,创建几种通用的转换方法字节码生成策略,任意两类之间的转换代码便可通过几种抽象转换策略的组合来实现。**Bean转换类及其方法体代码生成后通过自定义类加载器加载,反射生成转换类对象,并完成对象字段赋值。这里所说的转换类的字段值实际上也是转换类对象,取决于Bean 实体中字段类型,如果是原始类型、包装类型,转换类方法体内部自行处理,如果是数组类、N层Map类、N层Collection 类、自定义实体类、其他第三方定义类型等,递归生成相应字段的转换类存储于字段中,在转换类方法体中调用内部递归生成的转换类方法实现以上复杂类型的转换。这种设计可提高复用性,递归生成的转换类在其他同类场景中可直接使用。
  最上层时用户接口层,有多个重载静态方法可调用,用户自行选择,浅拷贝或者深拷贝模式也可通过参数设置,使用便捷。

3.4.1 技术难点

  数组类、N层Map类、N层Collection 类 的深拷贝模式下需要层层创建并遍历原始数据依次赋值,Map 和Colection 实际上属于参数化类型,泛型实参还可以是其他任意类型或者嵌套,比如:Map<String, Map<String,List Inner>>> ,这些场景的转换代码本身也比较复杂,而且需要通过字节码的方式实现,其难度又增加不少。字段类型是Map 或者Collection 等接口或者子接口,子抽象类时,需要找到对应的实现类才可通过new 指令实例化。实例化时需要考虑扩容带来的效率影响,初始容量要依据需要转换的对象元素数量来设置。
  其次,以上类型的排序组合后转换逻辑代码虽有差异还是有共性逻辑。如果每种都去实现转换,势必会有大量的分支判断和重复要逻辑。如何抽象几类共性逻辑并创建几种通用的转换方法字节码生成策略,使任意两类之间的转换代码可通过几种抽象转换策略的组合来实现?需要制定基础策略模式,同时策略模式的选择要和具体转换逻辑解耦分离,提高扩展性,这也是一个难点。
  九种原始类型: int long double float boolean short char String byte和8种包装类型 Integer Long Double Float Boolean Short Byte 共17种。 两两之间的排序组合有A217 种。原始类型之间需要强转,包装类型需要拆箱或装箱。这些通过java 硬编码处理其实不算复杂,但基础类型转换的虚拟机指令并不简单,我们编程后的代码通过编译器处理后内部的字节码指令自动生成了,可以说编译器给我们承担了大部分工作,简化了我们编程难度。但是写字节码指令方式却需要精细处理各种转换指令。比如,原始类字段double d=5.0 ,转换成目标类字段byte b。编写源代码时只需要强转语句即可:b=(byte)d, 但是基础类型转换只有特定指令,有些可以一步到位,有些需要两步。如下图所示。

java bean 转成json 在线工具类_java_07

因此以上转换需要两步转换d2i, i2b 。这就意味着需要判别转换类型生成对应转换指令序列。此外,包装类的装箱和拆箱指令也需要依次处理。最繁琐的就是 包装类到到非对应原始类的转换,比如:Double d=5.0 byte b, 先执行拆箱函数指令生成 double 型操作数,再调用原始类的转换指令。总之以上每类转换情况如果通过字节码指令实现都需要精细化处理,判断分支必然非常庞大。因此也需要抽象提炼以上各类情况,生成几种基础转换语句,然后通过基础转换的动态组合来实现。

3.4.2 代码实现

  代码类图结构如下所示。最底下类对应基础层,TransformUtilGenerate对应用户接口层,其他为中间层实现类。源码地址https://github.com/meditator-wen/bean-transform-tool/tree/master/bean-transform-tool

java bean 转成json 在线工具类_java_08

上图中最底下一层是框架基础模块实现类,包括工具定义的注解类、Class解析类、自定义类加载器、系统配置解析等。这里重点说下注解类,用户引入工具包后可在对应的实体类字段中添加BeanFieldInfo设置注解属性,工具框架自动解析用户注解配置,直接影响转换类的方法体字节码生成策略。具体注解项注释如下。有些属性之间有逻辑优先级。使用方法参见使用示例。

/**
 * @Classname SourceBeanField
 * @Description
 * 
 * userExtend 标注是否用户自定义转换,默认为false
 * autoTransform  标注是否自动转换
 * extensionObjectTransformImplClass 自定义转换实现类名称,继承ExtensionObjectTransform接口
 * getFunctionName  字段对应的get 方法名,如果不设置,
 * 默认值是 前缀get+字段名称首字母大写+字段名称除首字母外其他字符,
 * 如果是boolean 型,默认前缀是is, 不是get(idea 生成boolean get方法的默认规则)
 *  setFunctionName  字段对应的set 方法名,如果不设置,
 *  默认值是 set+字段名称首字母大写+字段名称除首字母外其他字符
 *  sourceFieldName  本类字段对应的源类字段名称,在字段名不一致的情况下需要制定,默认源类字段和本类字段同名
 *  description   字段描述信息
 * @Date 2021/9/13 22:54
 * @Created by wen wang
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanFieldInfo {
    boolean userExtend() default false;
    boolean autoTransform() default true;
    String extensionObjectTransformImplClass() default "";
    String getFunctionName() default "";
    String setFunctionName() default "";
    String sourceFieldName() default "";
    String description() default "";

}

  最复杂的是复杂类型处理策略,参数化类型中Map或者Collection 类型,常规数组类型,自定义实体类型、第三方实体类型的嵌套组合空间无穷,只能抽象基础策略。多层嵌套内部递归转换时通过解耦的策略选择层从基础策略中选择某种组合生成对应转换类。以下是基础策略的类层次结构以及核心函数列表

java bean 转成json 在线工具类_字节码_09

4 使用方法及示例

 该工具框架使用便捷,可通过maven pom 依赖引入组件(也可以手动下载https://search.maven.org/ 查询bean-transform-tool 下载最新版本jar 包 ) ,先修改maven 用户配置文件setting.xml,增加 profile 标签,profiles 标签下可以有多个,如果用户配置中有其他profile 配置项,无需覆盖,保留原有的配置。但需要选择ossrh 的 profile 为激活项。

<profiles>
        <profile>
            <id>ossrh</id>
            <repositories>

                <repository>
                    <id>ossrh</id>
                    <url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
                <repository>
                    <id>ossrh</id>
                    <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
            </repositories>
        </profile>
    </profiles>

激活profile 配置:

<activeProfiles>
        <activeProfile>ossrh</activeProfile>
    </activeProfiles>

开发时引入 pom 坐标如下:

<dependency>
            <groupId>io.github.meditator-wen</groupId>
            <artifactId>bean-transform-tool</artifactId>
            <version>1.0.1</version>
        </dependency>

注意,本工具内部依赖SLF4J日志框架,内部引用了以下依赖,如果用户原有工程中引入的是其他SLF4J 日志框架的实现,比如log4j,不需要logback,则需要排除logback依赖,避免SLF4J 接口实现类冲突。如果原有工程也是使用logback 则不需排除,maven 自动去除重复依赖,当然,如果版本不对,用户也可以排除工具里面的logback依赖,因为根据最短依赖原则,maven 可能会选择工具里面的logback 版本,导致与用户期望版本不一致。

<dependency>
            <groupId>io.github.meditator-wen</groupId>
            <artifactId>bean-transform-tool</artifactId>
            <version>1.0.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>logback-classic</artifactId>
                    <groupId>ch.qos.logback</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>logback-core</artifactId>
                    <groupId>ch.qos.logback</groupId>
                </exclusion>
            </exclusions>
        </dependency>
<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.0-alpha5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-core -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.3.0-alpha10</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.3.0-alpha10</version>
        </dependency>

所转换的实体类字段需要有默认get/set 方法,框架内部通过invokevirtual 字节码指令调用get/set方法。。com.shzz.common.tool.bean.transform.asm.TransformUtilGenerate 是用户接口类,重载的接口函数形式如下:

/**
     * 静态generate方法1,重载方法,根据输入参数生成转换类对象
     * @see {@link TransformUtilGenerate#generate(Class, Class, boolean, boolean, List, java.lang.reflect.Type[])} }
     */
  public static <S, T> BeanTransform generate(Class<S> sourceBeanClass, Class<T> targetClass, List<ExtensionObjectTransform> extendsTransformList) throws Exception {
        return generate(sourceBeanClass, targetClass, true, true, extendsTransformList, null);
    }

    /**
     * 静态generate方法2,重载方法,根据输入参数生成转换类对象
     * @see {@link TransformUtilGenerate#generate(Class, Class, boolean, boolean, List, java.lang.reflect.Type[])} }
     */
    public static <S, T> BeanTransform generate(Class<S> sourceBeanClass, Class<T> targetClass, boolean isDeepCopy) throws Exception {
        return generate(sourceBeanClass, targetClass, isDeepCopy, true, null, null);
    }

    /**
     * 静态generate方法3,重载方法,根据输入参数生成转换类对象
     * @see {@link TransformUtilGenerate#generate(Class, Class, boolean, boolean, List, java.lang.reflect.Type[])} }
     */
    public static <S, T> BeanTransform generate(Class<S> sourceBeanClass, Class<T> targetClass, boolean isDeepCopy, List<ExtensionObjectTransform> extendsTransformList) throws Exception {
        return generate(sourceBeanClass, targetClass, isDeepCopy, true, extendsTransformList, null);
    }


    /**
     * 静态generate方法4,重载方法,根据输入参数生成转换类对象
     * @see {@link TransformUtilGenerate#generate(Class, Class, boolean, boolean, List, java.lang.reflect.Type[])} }
     */ 
    public static <S, T> BeanTransform generate(Class<S> sourceBeanClass, Class<T> targetClass, boolean isDeepCopy, boolean permitBaseTypeInterconvert, List<ExtensionObjectTransform> extendsTransformList) throws Exception {
        return generate(sourceBeanClass, targetClass, isDeepCopy, permitBaseTypeInterconvert, extendsTransformList, null);
    }
    /**
     * 静态generate方法5,重载方法,以上所有重载方法内部均调用该方法。根据输入参数生成转换类对象
     * 参数 {@code boolean isDeepCopy} 如果设置为false,如果目标类和源类类型不一致时无法转换,如果一致则引用类型直接赋值.
     * 参数 {@code java.lang.reflect.Type[] actualGenericType}, 如果要转换方法体内部定义的局部变量(非匿名内部类方式定义)
     * 且变量类型是参数化类型、泛型数组、通配泛型时,内部泛型实参可通过该参数传入
     * 参数 {@code List<ExtensionObjectTransform> extendsTransformList}, 如果用户在对应目标类的字段中通过工具包中的注解BeanFieldInfo设置了 extensionObjectTransformImplClass 属性,表示该字段的转换使用用户自定义实现类来完成。
     * 用户创建实现类的对象通过extendsTransformList 参数传入,工具框架内部调用对应方法进行转换
     *
     * @param sourceBeanClass            源类类型
     * @param targetClass                转换的目标类类型
     * @param isDeepCopy                 是否深拷贝,默认是true
     * @param permitBaseTypeInterconvert 是否支持不同的原始类型或者包装类型互相转换,比如 double 到 byte  或者Integer 到 short
     * @param extendsTransformList       用户自定义的转换类对象的集合
     * @param actualGenericType          实际泛型类型,该参数预留,可传入null.
     * @return {@link BeanTransform}     字节码生成的转换类对象
     * @throws Exception Bean 转换异常
     */
   public static <S, T> BeanTransform generate(Class<S> sourceBeanClass, Class<T> targetClass, boolean isDeepCopy, boolean permitBaseTypeInterconvert, List<ExtensionObjectTransform> extendsTransformList, java.lang.reflect.Type[] actualGenericType) throws Exception {
    
        UniversalClassTypeStrategy universalClassTypeStrategy = new UniversalClassTypeStrategy();
        BeanTransform beanTransform = null;
        try {
            beanTransform = universalClassTypeStrategy.generate(sourceBeanClass, targetClass, isDeepCopy, permitBaseTypeInterconvert, extendsTransformList, actualGenericType);
        }finally {
            afterGenerate();
        }
        
        return beanTransform;
    }

 通过TransformUtilGenerate 函数创建对应类型的转换类字节码,并加载新创建的类,反射生成转换类的对象然后返回给调用者。调用方式如下,多个重载方法及其参数设置可根据场景自行选择,使用非常便捷。

// generate 方法会抛出异常,建议捕获或者抛给上层函数处理
        try {
          BeanTransform  beanTransFormsHandler =TransformUtilGenerate.generate(CopyFrom.class, CopyTo.class, true, true, null);
        BeanTo beanTo =  beanTransFormsHandler.beanTransform(BeanFrom.class,
                        from,
                        BeanTo.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

  实体类字段注解配置参见 上一节 BeanFieldInfo 注解说明**(注意,该注解非强制添加,取决于用户场景和需求,不添加时工具框架采用默认策略处理该字段的映射关系)**。该注解添加于需要转换的目标类字段。比如:

// BeanTo 中 nestList 字段对应 BeanFrom  nestArray 
    @BeanFieldInfo(userExtend = false, sourceFieldName = "nestArray",autoTransform = true)
    private List<List<ListElement>>  nestList=new ArrayList<>();

框架内部解析实体类时同步解析字段注解配置,每个字段和源类字段的对应关系以注解配置为准,若未配置。则以同名字段对应。字段的get/set 方法名称也可以通过注解getFunctionName、setFunctionName 属性配置。默认规则:set/get+字段名称首字母大写+字段名称除首字母外其他字符。其他注解配置参见3.4.2节中BeanFieldInfo注解配置说明。
如果某些字段用户想要按照自定义方式转换,比如枚举类、特殊时间类或者其他任何类型,要传入用户自定义类,需要在字段注解中添加以下配置:

@BeanFieldInfo(userExtend = true, extensionObjectTransformImplClass = "com.akfd.methodhandle.compare.EnumTransform")
    private CommonCode enumCode ;

userExtend 设置为true,如果是false,则默认自动转换,用户配置自定义实现类不生效。 extensionObjectTransformImplClass 配置自定义实现类的全路径类名,该类需要实现工具框架ExtensionObjectTransform接口类方法完成类型转换。

public interface ExtensionObjectTransform extends Transform {
    public Object extensionObjectTransform(Object sourceObject, boolean deepCopy) throws Exception;

}

配置完后还需要创建自定义类对象,以上述配置为例,com.akfd.methodhandle.compareEnumTransform 类是CommonCode enumCode 字段的转换类。代码如下:

public class CommonCodeEnumTransform implements ExtensionObjectTransform {
    @Override
    public Object extensionObjectTransform(Object sourceObject, boolean deepCopy) throws Exception {
        String code=(String)sourceObject;
        CommonCode commonCodeReturn=null;
        CommonCode[] commonCodes= CommonCode.values();
       for(CommonCode commonCode:commonCodes){
           if(commonCode.getErrorCode().equals(code)){
               commonCodeReturn=commonCode;
           }
       }

       return commonCodeReturn;
    }
}

生成的转换类对象添加到List 对象中,并在参数中传入,调用的接口方法可以选择带有List<ExtensionObjectTransform> extendsTransformList :参数的方法,比如:

public static <S, T> BeanTransform generate(Class<S> sourceBeanClass, Class<T> targetClass,    List<ExtensionObjectTransform> extendsTransformList) throws Exception {
        return generate(sourceBeanClass, targetClass, true, true, extendsTransformList, null);
    }

传参和调用用户接口函数方式如下:

// 如果用户在目标类多个字段配置了对应字段的自定义转换类,均需创建对应转换类对象。
 EnumTransform enumTransform=new EnumTransform(); 
 List<ExtensionObjectTransform>   extensions=new ArrayList<>();
 // 创建对应对象然后依次添加到List 中统一传入generate 方法。
 extensions.add(enumTransform);
 // 框架内部会根据用户自定义的注解配置将相关转换对象存储于字段中,内部调用相应对象的的方法,是字节码层面调用,非反射调用。
 BeanTransform  beanTransFormsHandler =TransformUtilGenerate.generate(BeanFrom.class, BeanTo.class, extensions);
 // 执行转换代码
 BeanTo copyTo =  beanTransFormsHandler.beanTransform(BeanFrom.class,
                        from,
                        BeanTo.class);

注意,TransformUtilGenerate.generate 方法内部会有通过反射解析实体类信息,也会通过反射创建BeanTransform 类对象,建议创建一次后缓存生成的beanTransFormsHandler,避免重复创建增加性能开销。BeanTransform 类对象 内部完全通过字节码调用,无反射,效率极高。

5 开源框架对比测试

5.1 功能测试

5.1.1 用户自定义转换

  用户自定义转换原则上可以转换任何类型。用户继承工具包中com.shzz.common.tool.bean.transform.ExtensionObjectTransform 接口,实现某个复杂字段的映射转换,并将实现类全路径类名通过注解方式配置到对应字段。调用工具接口时需要将实现类的实例化对象以列表形式传入。工具内部生成转换类时会将对应自定义对象写入字段中保存,并在转换代码中调用自定义对象的方法转换。比如,某上一节提到的枚举字段自定义转换类使用示例。原则上自定义模式可以实现任何对象的转换,但由于有人工继承接口的编码量,因此只在少数场景下才使用自定义接口,其他情况直接使用自动转换的模式即可。

5.1.2 自动化换转换

  通过框架自动转换,无需传入用户自定义实现转换接口。这种模式对于实体类中少数复杂情况,比如通配泛型字段<? extends A>、泛型数组T[][]、自定义参数化泛型A<T>、类型变量T 暂不支持外,其他的场景基本支持,包括多层Map、Collection、常规数据等复杂数据类型。浅拷贝和深拷贝均支持。对于上面提到的实体类中少数复杂情况,框架本身可支持开发者拓展,继承对应的开发者接口(这里的开发者接口是实现转换类字节码的策略接口com.shzz.common.tool.bean.transform.asm.strategy.ComplexTypeStrategy,不是用户接口com.shzz.common.tool.bean.transform.ExtensionObjectTransform)实现复杂类型通用处理策略并集成到工具中,可以完善自动换换的场景适配性。

5.1.3 测试用例

  实体类字段尽量覆盖各类数据转换情况。实体类结构图如下所示。其中CopyFrom 是映射转换的源类,CopyTo 是映射转换的目标类。
有几个字段名称不对应,在CopyTo 实体中增加映射注解如下,标注源类和目标类字段对应关系的@BeanFieldInfo注解只需要加载于目标类的字段上即可。

@BeanFieldInfo(userExtend = true, sourceFieldName = "commonCode", extensionObjectTransformImplClass = "bean.transform.CommonCodeEnumTransform")
 private CommonCode commonCodeEnum;

 @BeanFieldInfo(sourceFieldName = "innerDoubleList")
 Inner[][] innerarray;
   
 @BeanFieldInfo(userExtend = false, sourceFieldName = "nestArray", autoTransform = true)
 private List<List<ListElement>> nestList = new ArrayList<>();

 @BeanFieldInfo(sourceFieldName = "threeNestStringList")
 private char[][][] intThreeDems;

 @BeanFieldInfo(sourceFieldName = "threeNestList")
 private Double[][][] doubleThreeDems;

java bean 转成json 在线工具类_java_10


  以上实体覆盖了大多数数据场景,如有其它特殊场景,用户可自行测试。对CopyFrom 对象赋值后,用FastJson 转换后打印前后拷贝前后的数据json。

List<ExtensionObjectTransform> extensionObjectTransforms=new ArrayList<>();
  ExtensionObjectTransform extensionObjectTransform=new CommonCodeEnumTransform();
  extensionObjectTransforms.add(extensionObjectTransform);
  beanTransFormsHandler = TransformUtilGenerate.generate(CopyFrom.class, CopyTo.class, true, true,extensionObjectTransforms);
  CopyTo copyTo2 =  beanTransFormsHandler.beanTransform(CopyFrom.class,
                        from,
                        CopyTo.class);

TransformUtilGenerate.generate 方法执行时会通过字节码动态生成class 文件。可以配置系统环境变量将生成的文件输出到工程根目录。代码中修改系统变量如下,默认时false

System.setProperty("class.output.flag","true");

生成的转换类如下:

java bean 转成json 在线工具类_List_11


原始对象JSON:

{"carDirection":2,"characterValue":"u","commonCode":"0xff06","dateField":1646045959529,"district":"xxx","divider":3,"douVar":8.9,"ffs":30.0,"gridId":4,"inner":{"greenRatio":30,"phaseId":"1","phaseName":"phase1","phaseSeqNo":"3","red":3,"yellow":3},"innerDoubleList":[[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3},{"$ref":"$.innerDoubleList[0][0]"}]],"innerarray":[[{"$ref":"$.innerDoubleList[0][0]"}],[{"$ref":"$.innerDoubleList[0][0]"}]],"intersectionFrom":"A INTERSECTION","intersectionTo":"B INTERSECTION","laneNum":300,"laneWidth":3.5,"listContainArray":[[[{"$ref":"$.innerDoubleList[0][0]"}],[{"$ref":"$.innerDoubleList[0][0]"}]]],"listContainMap":[{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}],"listContainTwoLayerMap":[{"layer1":{"$ref":"$.listContainMap[0]"}}],"listElementList":[{"listElementField1":"filed1","listElementField2":1001,"listElementField3":1002},{"listElementField1":"filed1","listElementField2":2001,"listElementField3":2002}],"mapContainList":{"fieldMapwithList":[{"greenRatio":30,"phaseId":"1","phaseName":"phase2 in mapContainList field","phaseSeqNo":"3","red":3,"yellow":3}]},"nestArray":[[{"listElementField1":"filed1_for_array","listElementField2":5001,"listElementField3":5002}]],"nestList":[[{"$ref":"$.listElementList[0]"},{"$ref":"$.listElementList[1]"}]],"nodeFrom":"A","nodeTo":"B","otherDistrict":12,"roadData":"32.555,106.789","roadDirection":2,"roadId":"301","roadIn":"","roadLength":450.0,"roadName":"road A","roadNameFrom":"road from","roadNameTo":"road to","roadOut":"road out","roadType":5,"streetId":"","threeNestCharacterList":[[["2"]]],"threeNestList":[[[520.0]]],"threeNestStringList":[[["20.3","213","214"],["125.9"]]],"thresholdId":1200,"twoLayerMap":{"$ref":"$.listContainTwoLayerMap[0]"}}

转换后CopyTo 对象:

{"carDirection":2,"characterValue":"u","commonCodeEnum":"CLASS_NAME_NULL_EXCEPTION","dateFiled":1646045959529,"district":"xxx","divider":3.0,"douVar":8.9,"doubleThreeDems":[[[520.0]]],"ffs":30.0,"gridId":4,"inner":{"greenRatio":30,"phaseId":"1","phaseName":"phase1","phaseSeqNo":"3","red":3,"yellow":3},"innerarray":[[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3},{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3}]],"intThreeDems":[["222","1"]],"intersectionFrom":"A INTERSECTION","intersectionTo":"B INTERSECTION","laneNum":300,"laneWidth":3,"listContainArray":[[[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3}],[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3}]]],"listContainMap":[{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}],"listContainTwoLayerMap":[{"layer1":{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}}],"listElementList":[{"listElementField1":"filed1","listElementField2":1001,"listElementField3":1002},{"listElementField1":"filed1","listElementField2":2001,"listElementField3":2002}],"mapContainList":{"fieldMapwithList":[{"greenRatio":30,"phaseId":"1","phaseName":"phase2 in mapContainList field","phaseSeqNo":"3","red":3,"yellow":3}]},"nestList":[[{"listElementField1":"filed1_for_array","listElementField2":5001,"listElementField3":5002}]],"nodeFrom":"A","nodeTo":"B","otherDistrict":12,"roadData":"32.555,106.789","roadDirection":2,"roadId":301,"roadIn":"","roadLength":450,"roadName":"road A","roadNameFrom":"road from","roadNameTo":"road to","roadOut":"road out","roadType":5,"streetId":"","threeNestList":[[[520]]],"thresholdId":1200,"twoLayerMap":{"layer1":{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}}}

修改原始对象中某元素的内部字段

from.getTwoLayerMap().get("layer1").get("layer2").setPhaseName(" 修改twoLayerMap PhaseName ");
 from.getNestList().get(0).get(0).setListElementField1("修改 ListElementField1");

转换后CopyTo 对象内部元素保持不变,是深拷贝模式。

{"carDirection":2,"characterValue":"u","commonCodeEnum":"CLASS_NAME_NULL_EXCEPTION","dateFiled":1646045959529,"district":"xxx","divider":3.0,"douVar":8.9,"doubleThreeDems":[[[520.0]]],"ffs":30.0,"gridId":4,"inner":{"greenRatio":30,"phaseId":"1","phaseName":"phase1","phaseSeqNo":"3","red":3,"yellow":3},"innerarray":[[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3},{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3}]],"intThreeDems":[["222","1"]],"intersectionFrom":"A INTERSECTION","intersectionTo":"B INTERSECTION","laneNum":300,"laneWidth":3,"listContainArray":[[[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3}],[{"greenRatio":20,"phaseId":"1","phaseName":"phase2","phaseSeqNo":"3","red":3,"yellow":3}]]],"listContainMap":[{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}],"listContainTwoLayerMap":[{"layer1":{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}}],"listElementList":[{"listElementField1":"filed1","listElementField2":1001,"listElementField3":1002},{"listElementField1":"filed1","listElementField2":2001,"listElementField3":2002}],"mapContainList":{"fieldMapwithList":[{"greenRatio":30,"phaseId":"1","phaseName":"phase2 in mapContainList field","phaseSeqNo":"3","red":3,"yellow":3}]},"nestList":[[{"listElementField1":"filed1_for_array","listElementField2":5001,"listElementField3":5002}]],"nodeFrom":"A","nodeTo":"B","otherDistrict":12,"roadData":"32.555,106.789","roadDirection":2,"roadId":301,"roadIn":"","roadLength":450,"roadName":"road A","roadNameFrom":"road from","roadNameTo":"road to","roadOut":"road out","roadType":5,"streetId":"","threeNestList":[[[520]]],"thresholdId":1200,"twoLayerMap":{"layer1":{"layer2":{"greenRatio":20,"phaseId":"1","phaseName":"phase2 in map field","phaseSeqNo":"3","red":3,"yellow":3}}}}

BeanTransform 自动忽略Object 类型,继承Map、Collection接口的实体类,比如fastjson JSONObject。

private JSONObject jsonObject ;
    private Object object ;

解析实体类遇到以上情况会自动忽略,其他第三方实体,如果定义类字段的get/set 方法,可实现字段转换,否则赋值为空。

20:19:27.729 [main] WARN BeanTransformsMethodAdapter - target class CopyTo field: JSONObject jsonObject,userExtend=false, 不满足自动转换条件,不予转换,请实现拓展类转换
20:19:27.729 [main] WARN BeanTransformsMethodAdapter - target class CopyTo field: Object object,userExtend=false, 不满足自动转换条件,不予转换,请实现拓展类转换

  自动换转换模式下,开源工具和自研工具功能对比如下:

java bean 转成json 在线工具类_List_12

5.2 性能对比测试

  选取三种主流Bean 转换工具,包括 org.springframework.beans.BeanUtils、fr.xebia.extras.selma、org.mapstruct,实现一种get/set 硬编码转换方式,以及自研的BeanTransform工具,一共五种方式。apache 的BeanUtils 性能太差,不具有对比性。net.sf.cglib.beans.BeanCopier 虽然效率也很高,但是只能实现浅拷贝,也不具有对比性。get/set 硬编码转换方式,即,Manual 方式,是理论上性能最高的方式。可作为对照组,与之越接近,性能越好。
  分两种场景测试,第一种,实体类字段以基础类型和常规自定义类为主,包含少量Collection或者Map类型;第二种,测试实体类字段均为 多层Map 、多层Collection 、多维数组等复杂类型。
  第一种场景,需要转换的实体类BeanTo BeanFrom 以及内部嵌套类关系图如下。字段数32个,基础类型字段29个,集合类型2 个,自定义类型1个。

java bean 转成json 在线工具类_java_13

 实体类中源类字段和目标类字段名称和类型可以不一致,字段名称不一致时以上工具框架均可通过各自的注解指定字段名映射关系。类型不一致时,各自框架可以自动转换能力略有不同,这个在功能测试对比中有详细说明。两个实体类之间字段数目不同,slemaMapper 框架需要增加注解配置,忽略不一致的字段。其他框架会自动忽略不一致字段。
测试机器,华硕,处理器 i7 第7代系列,内存8G.
JMH 测试类如下:

@BenchmarkMode({Mode.Throughput}) 
@OutputTimeUnit(TimeUnit.SECONDS) // 指定输出的耗时时长的单位
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 50, time = 5, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@Threads(1)
@State(Scope.Benchmark)
public class Jmh {
    BeanTransform beanTransFormsHandler = null;
    Manual manual = new Manual();
    MapperStructConvert mapperStructConvert = MapperStructConvert.INSTANCE;
    //Get SelmaMapper
    SelmaMapper selmaMapper = Selma.builder(SelmaMapper.class).build();
    public Jmh() {
        try {
            beanTransFormsHandler = TransformUtilGenerate.generate(BeanFrom.class, BeanTo.class, true, true, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    BeanFrom from = createCopyFrom();
    @Benchmark
    public void benchMarkMapStruct() throws Exception {
        BeanTo beanTo4 = new BeanTo();
        beanTo4 = mapperStructConvert.transform(from);
    }
    @Benchmark
    public void benchMarkSpringBeanUtils() throws Exception {
        BeanTo beanTo3 = new BeanTo();
        org.springframework.beans.BeanUtils.copyProperties(from, beanTo3);
    }
    @Benchmark
    public void benchMarkBeanTransformsHandler() throws Exception {
        BeanTo beanTo2 = beanTransFormsHandler.beanTransform(BeanFrom.class,
                from,
                BeanTo.class);
    }
    @Benchmark
    public void benchMarkBeanManual() throws Exception {
        BeanTo manualCopy = manual.transformManual(from);
    }
    @Benchmark
    public void benchMarkSelmaMapper() throws Exception {

        BeanTo selmaCopy = selmaMapper.asCopyTo(from);
    }

    public static void main(String[] args) throws RunnerException {
        //String[] args
        //String[] args
        String docPath = System.getProperty("user.dir")+File.separator+"doc"+File.separator+"benchMarkTest.log";;
        System.out.println("jmh test output path: "+docPath);
        Options options = new OptionsBuilder()
                .include(Jmh.class.getSimpleName())
                .output(docPath)
                .build();
        new Runner(options).run();
    }
}

预热模式下,迭代50次,吞吐量测试结果对比图如下所示。当然,不同测试实体类,不同的jmh 测试参数等情况,测试结果会有不同。比如实体类中只有少量基础类型字段,无集合或者Map字段,所有转换框架的吞吐量会按比例线性增长。而且fr.xebia.extras.selma、manual,自研beanTransform 三种的转换效率均值差异以及方差更小。但总体趋势大体类似。

java bean 转成json 在线工具类_List_14

JMH Benchmark 得分率如下:

Benchmark                            Mode  Cnt        Score        Error  Units
Jmh.benchMarkBeanManual             thrpt   50  7159828.750 ±  97549.112  ops/s
Jmh.benchMarkBeanTransformsHandler  thrpt   50  6891863.069 ± 131251.947  ops/s
Jmh.benchMarkMapStruct              thrpt   50  5250084.203 ± 118260.604  ops/s
Jmh.benchMarkSelmaMapper            thrpt   50  6788490.845 ± 105572.909  ops/s
Jmh.benchMarkSpringBeanUtils        thrpt   50   440125.283 ±  11278.856  ops/s

置信区间简化版计算公式如下,假设正态分布。

If (n>=30),
CI = x ± Zα/2 × (σ/√n)
If (n<30),
CI = x ± tα/2 × (σ/√n)
这里,
  x =平均值
  σ = 标准偏差
  α = 1 - (置信水平 /100)
  Zα/2 = Z-表中的值
  tα/2 = t-表中的值
  CI = 置信区间.

java bean 转成json 在线工具类_java_15

  从第一种测试场景结果:manual 方式效率最高,原则上达到理论上限,其次是 自研beanTransformmanual , 均值略低于manual 理论上限 3.88%。 总体来看效率无明显差异。 自研beanTransform 高于spring beanutil 15.65倍以上,比mapStruct 高出 31.27%,略高于selmaMapper1.52%。
  第二种测试场景实体类如下,共8个字段,多层Map字段2个,多层Collection 4字段个,包装类多维数组字段1个,自定义类多维数组字段1个。

java bean 转成json 在线工具类_java_16


  由于功能限制,有些开源工具,比如MapStruct,不支持以上类型的深度拷贝和转换,在此只比较自研beanTransformm 和SelmaMapper 两个工具的性能。测试环境以及JMH测试参数和上面相同。迭代50次,吞吐量测试结果对比图如下所示:

java bean 转成json 在线工具类_List_17


  JMH Benchmark 得分率如下

Benchmark                                            Mode  Cnt        Score       Error  Units
JmhComplexFieldTest.benchMarkBeanTransformsHandler  thrpt   50   696655.563 ± 29790.364  ops/s
JmhComplexFieldTest.benchMarkSelmaMapper            thrpt   50  1151885.051 ± 66939.100  ops/s

  置信区间量化图如下:

java bean 转成json 在线工具类_字节码_18


  由此可见实体类均为 多层Map 、多层Collection 、多维数组等复杂类型实体类字段,selmaMapper 效率更优,比自研beanTransform 高出65%左右。主要原因在于selmaMapper 几乎将所有字段的转换的代码放在同一函数中实现,字节码结构更加紧凑(意味着耦合性也更高),而自研bean转换工具设计时针对这些复杂类型会单独生成对应字段的转换类对象并存储于上层类转换对象的某个字段中,上层类转换对象的转换方法内部调用对象方法实现复杂类型转换。这种设计耦合度低,且可复用各类模块,更加灵活。但是相比于直接转换模式会增加不少方法调用、变量load 、变量store 以及对象checkcast 字节码操作指令,因此效率有所降低。

  实际项目中这种实体类极少出现。性能方面综合而言,自研工具框架和Selma框架的转换效率均非常优秀,不同场景下效率各有优劣。

5.3 稳定性测试

  工具采用动态字节码生成方式产生转换类,可能不断产生转换类字节码byte数组以及转换类对象。此外,工具内部为了提升效率或者多线程安全性设置了一些缓存Map和ThreadLoacl 变量。这些问题处理不当极有可能导致工具存在内存泄漏而崩溃。工具为了提高稳定性在代码实现上也增加了各类异常情况保护逻辑,对缓存数据的清理逻辑等。
  不断创建转换对象,然后调用转换函数生成新对象。持续运行48小时,观察线程、内存、CPU、类加载情况。本次测试中使用jvisualvm 工具监控运行指标。软件环境:idea2019 ,JDK13。

int loop=100000000;
  for (int lo = 0; lo < loop; ++lo) {
            Thread.sleep(100);
            List<ExtensionObjectTransform> extensionObjectTransforms=new ArrayList<>();
            ExtensionObjectTransform extensionObjectTransform=new CommonCodeEnumTransform();
            extensionObjectTransforms.add(extensionObjectTransform);
            BeanTransform beanTransFormsHandler = TransformUtilGenerate.generate(CopyFrom.class, CopyTo.class, true, true,extensionObjectTransforms);
           CopyTo copyTo =  beanTransFormsHandler.beanTransform(CopyFrom.class,
                        from,
                        CopyTo.class);
}

jvisualvm 工具监控面板上只显示最新1小时图表,界面中左上角显示运行总时间。

java bean 转成json 在线工具类_字节码_19

  metaSpace 运行情况:

java bean 转成json 在线工具类_字段_20


  从图中可以看出各项指标均正常,内存周期性回落,无泄漏现象。

6 结语

  自研BeanTransform可适配多种复杂场景。支持基础类型拷贝、基础类型相互强转,自定义嵌套实体类型的深拷贝等情况。复杂场景,比如N层数组类、N层Map类、N层Collection 类或者以上类型的相互嵌套情况,该框架均可自动换映射转换,且支持深拷贝。可以说功能上满足Bean 实体转换的各类需求。相比而言,主流开源框架selma、mapstruct、springbeanutil 、cglib beancopy等对于复杂场景的支持度稍有欠缺,适配性和通用性不如自研bean 映射转换框架,这是自研bean 映射转换框架的优势之一。
  在性能方面,两种场景性能略有差异。第一种场景,如果实体类字段为基础类型或者自定义嵌套类型情况,包含少量Collection或者Map 。自研beanTransform基本接近get/set manual 方式理论上限 , 均值略低于manual 理论上限 3.88%。 总体来看效率无明显差异。 自研beanTransform 高于spring beanutil 15.65倍以上,比mapStruct 高出 31.27%,略高于selmaMapper1.52%。第二种场景,实体类均为 多层Map 、多层Collection 、多维数组等复杂类型实体类字段,selmaMapper 效率更优,比自研beanTransform 高出65%左右。但实际项目中这种实体类极少出现,性能方面综合而言,自研工具框架和Selma框架的转换效率均非常优秀,不同场景下效率各有优劣。因此如果是如果非常追求效率,selma和自研bean 映射转换框架都是比较好的选择。
  在动态性和便捷性方面,自研bean 映射转换框架用户接口简洁,使用方便。而且自研bean 映射转换框架使用字节码动态生成方式,无需静态编译。selma、mapstruct两个开源框架都是编译期生成转换类,每次增加新的类型转换时需要预定义接口然后静态编译生成转换类。意味着如果有多个类需要转换时都需要依次手动编码然后重新编译,如果实体类修改也需要重新编译生成新的转换类,所以说自研bean 映射转换框架 比 selma、mapstruct动态性强,更加通用和便捷。cglib beancopy 也支持动态性,不过,由于在功能上只支持浅拷贝,使用场景受到很大限制。
  运行稳定,无线程、堆内存溢出情况,类加载卸载情况正常。
总体对比结果如下(注意,以下对比只建立在自动转换的模式下,非用户自定义模式):

java bean 转成json 在线工具类_字节码_21

  综上所述,自研bean 映射转换框架是实现对象映射转换的优选工具框架。

7 展望

  自研BeanTransform也还存在不足,比如目前关于字段转换的配置还只能通过注解方法实现。如果是大的开发工程中各个模块分离,实体类模块可能是独立的公共模块,其他模块引入后如果有特殊转换需求,用户需要在公共模块中引入工具包并在实体类的字段中增加工具包的字段注解,然后发布公共模块供其他模块重新引入。这对公共模块而言还是有一定程度的代码侵入。最好的方式是工具框架能支持使用文件配置方式(比如xml)完成对实体类的转换配置。这样只需要单独配置用户模块即可,不影响其他模块。在后续版本中可继续拓展优化。
  在性能方面,如果实体类中存在大量Collection、Map、Array 等类型字段,效率会降低。V1.0.0 版本设计时针对这些复杂类型会单独生成对应字段的转换类对象并存储于上层类转换对象的某个字段中,上层类转换对象的转换方法内部调用对象方法实现复杂类型转换。这种设计优点在于耦合度低,且可复用各类模块,更加灵活。但是相比于直接转换模式会增加不少方法调用、变量load 、变量store 以及对象checkcast 字节码操作指令,因此效率有所降低。后续版本可优化改进。
  Map 类型转换为Class 类型或者直接转换Map或Collection 类型(如果实体类内部字段类型是Map 或者Collection,这种情况支持转换),目前版本不支持。比如以下情况后续版本考虑拓展。

TransformUtilGenerate.generate(Map.class, Map.class, true,true);
TransformUtilGenerate.generate(List.class, List.class, true,true);
TransformUtilGenerate.generate(Map.class, MyClass.class, true,true);