Easy-mapper – 一个灵活可扩展的高性能Bean mapping类库
太阳花下的
8年前
<h2>1 背景</h2> <p>做Java开发都避免不了和各种Bean打交道,包括POJO、BO、VO、PO、DTO等,而Java的应用非常讲究分层的架构,因此就会存在对象在各个层次之间作为参数或者输出传递的过程,这里转换的工作往往非常繁琐。</p> <p>这里举个例子,做过Java的都会深有体会,下面代码的set/get看起来不那么优雅 <img src="https://simg.open-open.com/show/519bf3d0051b841e0cb08dc4f4831eed.png"></p> <pre> <code class="language-java">ElementConf ef = new ElementConf(); ef.setTplConfId(tplConfModel.getTplConfIdKey()); ef.setTemplateId(tplConfModel.getTemplateId()); ef.setBlockNo(input.getBlockNo()); ef.setElementNo(input.getElementNo()); ef.setElementName(input.getElementName()); ef.setElementType(input.getElementType()); ef.setValue(input.getValue()); ef.setUseType(input.getUseType()); ef.setUserId(tplConfModel.getUserId());</code></pre> <p>为此业界有很多开源的解决方案,列出一些常见的如下:</p> <p><a href="/misc/goto?guid=4958823928708321762" rel="nofollow,noindex">Apache PropertyUtils</a></p> <p><a href="/misc/goto?guid=4958823928708321762" rel="nofollow,noindex">Apache BeanUtils</a></p> <p><a href="/misc/goto?guid=4959646057198156943" rel="nofollow,noindex">Cglib BeanCopier</a></p> <p><a href="/misc/goto?guid=4958974510927287708" rel="nofollow,noindex">Spring BeanUtils</a></p> <p><a href="/misc/goto?guid=4959618589649232630" rel="nofollow,noindex">Dozer</a></p> <p>这些框架在使用中或多或少都会存在一些问题:</p> <p>1、扩展性不高,例如自定义的属性转换往往不太方便。</p> <p>2、属性名相同、类型不匹配或者类型匹配、属性名不同,不能很好的支持。</p> <p>3、不支持Java8的lambda表达式。</p> <p>4、一些框架性能不佳,例如Apache的两个和Dozer(BeanCopier使用ASM字节码生成技术,性能会非常好)。</p> <p>5、对象的clone拷贝往往并不是使用者需要的,一般场景引用拷贝即可满足要求。</p> <p>那么,为了解决或者优化这些问题,类库easy-mapper就应运而生。</p> <h2>2 Easy-mapper特点</h2> <p>1、扩展性强。基于SPI技术,对于各种类型之间的转换提供默认的策略,使用者可自行添加。</p> <p>2、性能高。使用Javassist字节码增强技术,在运行时动态生成mapping过程的源代码,并且使用缓存技术,一次生成后续直接使用。默认策略为基于引用拷贝,因此在Java分层的架构中可以避免对象拷贝的代价,当然这有违背于函数式编程的不可变特性,easy-mapper赞同不可变,这里只不过提供了一种选择而已,请开放兼并。</p> <p>3、映射灵活。源类型和目标类型属性名可以指定,支持Java8 lambda表达式的转换函数,支持排除属性,支持全局的自定义mapping。</p> <p>4、代码可读高。基于Fluent式API,链式风格。惰性求值的方式,可随意注册映射关系,最后再统一做映射。</p> <h2>3 获取Easy-mapper</h2> <p>项目托管在github上,地址点此 <a href="/misc/goto?guid=4959676304166361624" rel="nofollow,noindex">https://github.com/neoremind/easy-mapper</a> 。使用Apache2 License开源。</p> <p><img src="https://simg.open-open.com/show/1cdb7c08097c0a08456329fc3b772fba.png" alt="Easy-mapper – 一个灵活可扩展的高性能Bean mapping类库" width="1223" height="303"></p> <p>最新发布的Jar包可以在maven中央仓库找到,地址 <a href="/misc/goto?guid=4959676304274853399" rel="nofollow,noindex">点此</a> 。</p> <h2>4 上手</h2> <h2>4.1 引入依赖</h2> <p>Maven:</p> <pre> <code class="language-java"><strong><dependency<strong>> <strong><groupId<strong>>com.baidu.unbiz<strong></groupId<strong>> <strong><artifactId<strong>>easy-mapper<strong></artifactId<strong>> <strong><version<strong>>1.0.1<strong></version<strong>> <strong></dependency<strong>></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></strong></code></pre> <p>Gradle:</p> <pre> <code class="language-java">compile 'com.baidu.unbiz:easy-mapper:1.0.1'</code></pre> <p>注:最新release请及时参考 <a href="/misc/goto?guid=4959676304166361624" rel="nofollow,noindex">github</a> 。</p> <h2>4.2 开发Java Bean</h2> <p>POJO如下:</p> <pre> <code class="language-java">public class Person { private String firstName; private String lastName; private List<String> jobTitles; private long salary; // getter and setter... }</code></pre> <p>DTO(Data Transfer Object)如下:</p> <pre> <code class="language-java">public class PersonDto { private String firstName; private String lastName; private List<String> jobTitles; private long salary; // getter and setter... }</code></pre> <h2>4.3 映射之Helloworld</h2> <p>从POJO到DTO的映射如下,</p> <pre> <code class="language-java">Person p = new Person(); p.setFirstName("NEO"); p.setLastName("jason"); p.setJobTitles(Lists.newArrayList("abc", "dfegg", "iii")); p.setSalary(1000L); PersonDto dto = MapperFactory.getCopyByRefMapper() .mapClass(Person.class, PersonDto.class) .registerAndMap(p, PersonDto.class); System.out.println(dto);</code></pre> <h2>5 深入实践</h2> <h2>5.1 注册和映射分开</h2> <p>helloworld中使用了registerAndMap(..)方法,其实可以分开使用,register只是让easy-mapper去解析属性并生成代码,一旦生成即缓存,然后随时map。 </p> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .register() .map(p, PersonDto.class);</code></pre> <p>先注册,拿到mapper,再映射。</p> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .register() Mapper mapper = MapperFactory.getCopyByRefMapper(); PersonDto dto = mapper.map(p, PersonDto.class);</code></pre> <p>先注册,拿到mapper直接映射。</p> <pre> <code>PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .register() Mapper mapper = MapperFactory.getCopyByRefMapper().map(p, PersonDto.class);</code></pre> <h2>5.2 指定属性名称</h2> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class) .field("salary", "salary") .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></code></pre> <h2>5.3 忽略某个属性</h2> <p>从源类型中排查某个属性,不做映射。</p> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class) .exclude("lastName") .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></code></pre> <h2>5.4 自定义属性转换</h2> <p>使用Transformer接口。</p> <pre> <code class="language-java">PersonDto6 dto = new PersonDto6(); MapperFactory.getCopyByRefMapper().mapClass(Person6.class, PersonDto6.class) .field("jobTitles", "jobTitles", new Transformer<List<String>, List<Integer>>() { @Override public List<Integer> transform(List<String> source) { return Lists.newArrayList(1, 2, 3, 4); } }) .register() .map(p, dto);</code></pre> <p>Java8的lambda表达式使用方式如下。</p> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class) .field("firstName", "firstName", (String s) -> s.toLowerCase()) .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></code></pre> <p>Java8的stream方式如下。</p> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class) .field("jobTitles", "jobTitleLetterCounts", (List<String> s) -> s.stream().map(String::length).toArray(Integer[]::<strong>new)) .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></strong></code></pre> <p>如果指定了属性了类型,那么lambda表达式则不用写类型,Java编译器可以推测。</p> <pre> <code class="language-java">MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class) .field("firstName", "firstName", String.<strong>class, String.<strong>class, s -> s.toLowerCase()) .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></strong></strong></code></pre> <h2>5.5 自定义额外的全局转换</h2> <p>AtoBMapping接口做源对象到目标对象的转换。</p> <pre> <code class="language-java">PersonDto6 dto = <strong>new PersonDto6(); MapperFactory.getCopyByRefMapper().mapClass(Person6.<strong>class, PersonDto6.<strong>class) .customMapping((a, b) -> b.setLastName(a.getLastName().toUpperCase())) .register() .map(p, dto);</strong></strong></strong></code></pre> <h2>5.6 映射已经新建的对象</h2> <p>registerAndMap和map方法的第二个参数支持Class,同时也支持已经新建好的对象。如果传入Class,则使用反射新建一个对象再赋值,目标对象可以没有默认构造方法,框架会努力寻找一个最合适的构造方法构造。</p> <pre> <code class="language-java">PersonDto dto = <strong>new PersonDto(); MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class).registerAndMap(p, dto);</strong></strong></strong></code></pre> <h2>5.7 源属性为空是否映射</h2> <p>如果源属性为空,那么默认则不映射到目标属性,可以强制赋空。 </p> <pre> <code class="language-java">PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class) .mapOnNull(<strong>true) .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></strong></code></pre> <h2>5.8 级联映射</h2> <p>如果Person类型中有Address,而PersonDto类型中有Address2,那么需要首先映射下,如下所示。 </p> <pre> <code class="language-java">MapperFactory.getCopyByRefMapper().mapClass(Address.<strong>class, Address2.<strong>class).register(); Person p = getPerson(); p.setAddress(<strong>new Address("beverly hill", 10086)); PersonDto dto = MapperFactory.getCopyByRefMapper() .mapClass(Person.<strong>class, PersonDto.<strong>class) .register() .map(p, PersonDto.<strong>class);</strong></strong></strong></strong></strong></strong></code></pre> <p>如果没有提前注册,那么会抛出如下异常:</p> <pre> <code class="language-java">com.baidu.unbiz.easymapper.exception.MappingException: No <strong>class map found <strong>for (Address, Address2), make sure type or nested type is registered beforehand</strong></strong></code></pre> <h2>5.9 输出生产的源代码</h2> <p>可指定log的level为debug,则会在console输出生成的源代码。</p> <p>另外,可在环境变量中指定如下参数,输出源代码或者编译后的class文件到本地文件系统。 </p> <pre> <code class="language-java">-Dcom.baidu.unbiz.easymapper.enableWriteSourceFile=<strong>true -Dcom.baidu.unbiz.easymapper.writeSourceFileAbsolutePath="..." -Dcom.baidu.unbiz.easymapper.enableWriteClassFile=<strong>true -Dcom.baidu.unbiz.easymapper.writeClassFileAbsolutePath="..."</strong></strong></code></pre> <h2>6 框架映射规则</h2> <p>默认使用SPI技术加载框架预置的属性处理器。</p> <p>在META-INF/services/com.baidu.unbiz.easymapper.mapping.MappingHandler文件中,规则优先级由高到低如下:</p> <p>1、指定了Transformer,则用自定义的transformer。</p> <p>2、属性类型相同,则直接按引用拷贝赋值;primitive以及wrapper类型,直接使用“=”操作符赋值。</p> <p>3、如果目标属性类型是String,那么尝试源对象直接调用toString()方法映射。</p> <p>4、如果源属性是目标属性的子类,则直接引用拷贝。</p> <p>5、如果是其他情况,则级联的调用mapper.map(..),注意框架未处理dead cycle的情况。</p> <p>最后,如果5仍然不能完成映射,那么框架会抛出如下异常:</p> <pre> <code class="language-java">com.baidu.unbiz.easymapper.exception.MappingCodeGenerationException: No appropriate mapping strategy found <strong>for FieldMap[jobTitles(List<string>)-->jobTitles(List<integer>)] ... com.baidu.unbiz.easymapper.exception.MappingException: Generating mapping code failed <strong>for ClassMap([A]:Person6, [B]:PersonDto6), <strong>this should not happen, probably the framework could not handle mapping correctly based on your bean.</strong></strong></strong></code></pre> <h2>7、框架依赖类库</h2> <pre> <code class="language-java">+- org.slf4j:slf4j-api:jar:1.7.7:compile +- org.slf4j:slf4j-log4j12:jar:1.7.7:compile | \- log4j:log4j:jar:1.2.17:compile +- org.javassist:javassist:jar:3.18.1-GA:compile</code></pre> <h2>8、性能测试报告</h2> <p>以下测试基于Oracal Hotspot JVM,参数如下:</p> <pre> <code class="language-java">java version "1.8.0_51" Java(TM) SE Runtime Environment (build 1.8.0_51-b16) Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode) -Xmx512m -Xms512m -XX:MetaspaceSize=256m </code></pre> <p>首先充分预热,各个框架,各调用一次,然后再进行benchmark。</p> <p>测试机器配置如下:</p> <p>CPU: Intel(R) Core(TM) i5-4278U CPU @ 2.60GHz</p> <p>MEM: 8G</p> <p>测试代码见链接 <a href="/misc/goto?guid=4959676304373968352" rel="nofollow,noindex">BenchmarkTest.java</a> 。</p> <pre> <code class="language-java">------------------------------------- | Create object number: 10000 | ------------------------------------- | Framework | time cost | ------------------------------------- | Pure get/set | 11ms | | Easy mapper | 44ms | | Cglib beancopier | 7ms | | BeanUtils | 248ms | | PropertyUtils | 129ms | | Spring BeanUtils | 95ms | | Dozer | 772ms | -------------------------------------</code></pre> <pre> <code class="language-java">------------------------------------- | Create object number: 100000 | ------------------------------------- | Framework | time cost | ------------------------------------- | Pure get/set | 56ms | | Easy mapper | 165ms | | Cglib beancopier | 30ms | | BeanUtils | 921ms | | PropertyUtils | 358ms | | Spring BeanUtils | 152ms | | Dozer | 1224ms | -------------------------------------</code></pre> <pre> <code class="language-java">------------------------------------- | Create object number: 1000000 | ------------------------------------- | Framework | time cost | ------------------------------------- | Pure get/set | 189ms | | Easy mapper | 554ms | | Cglib beancopier | 48ms | | BeanUtils | 4210ms | | PropertyUtils | 4386ms | | Spring BeanUtils | 367ms | | Dozer | 6319ms | -------------------------------------</code></pre> <p>结论:</p> <p>首先基于大量的反射技术的Apache的两个工具BeanUtils和PropertyUtils性能均不理想,Dozer的性能则更为不好。</p> <p>其次,基于ASM字节码增强技术的Cglib库真是经久不衰,性能在各个场景下均表现非常突出,甚至好于纯手写的get/set。</p> <p>最后,在调用10,000次时,easy-mapper好于Spring的BeanUtils,100,000次时持平,但是达到1,000,000次时,则落后。由于Spring BeanUtils非常的简单,采用了反射技术Method.invoke(..)做赋值处理,一般现代编译器都会对“热点”代码做优化,如R神的 <a href="http://www.open-open.com/lib/view/open1470728181531.html">《关于反射调用方法的一个log》</a> 提到的,可以看出超过一定调用次数后,基于profiling信息,JIT同样可以对反射做自适应的代码优化,这里对Method.invoke(..)在调动超过一定次数时会转为代理类来做实现,而不是调用native方法,因此JIT就可以做很多dereflection的事情优化性能,因此Spring的BeanUtils性能也不差。</p> <p>可以看出相比于老派的框架,easy-mapper性能非常优秀,虽然和Cglib BeanCopier有差距,这也可以看出使用Javassist的source level的API来做字节码操作性能肯定不会优于直接用ASM,但是easy-mapper的特点在于灵活、可扩展性、良好的编程体验方面,因此从这个tradeoff来看,easy-mapper非常适用于生产环境和工业界,而Cglib可用于一些对性能非常考究的框架内使用。</p> <h2>9、与高阶函数搭配使用</h2> <p>和 <a href="/misc/goto?guid=4958973085838435916" rel="nofollow,noindex">guava</a> 一起使用做集合的转换。 </p> <pre> <code class="language-java">MapperFactory.getCopyByRefMapper().mapClass(Address.<strong>class, Address2.<strong>class).register(); MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class).register(); List<Person> personList = getPersonList(); Collection<PersonDto> personDtoList = Collections2.transform(personList, p -> MapperFactory.getCopyByRefMapper().map(p, PersonDto.<strong>class)); System.out.println(personDtoList);</strong></strong></strong></strong></strong></code></pre> <p>和 <a href="/misc/goto?guid=4959676304496990043" rel="nofollow,noindex">functional java</a> 一起使用做集合的转换。</p> <pre> <code class="language-java">MapperFactory.getCopyByRefMapper().mapClass(Address.<strong>class, Address2.<strong>class).register(); MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class).register(); List<Person> personList = getPersonList(); fj.data.List<PersonDto> personDtoList = fj.data.List.fromIterator(personList.iterator()).map( person -> MapperFactory.getCopyByRefMapper().map(person, PersonDto.<strong>class)); personDtoList.<strong>forEach(e -> System.out.println(e));</strong></strong></strong></strong></strong></strong></code></pre> <p>和Java8的stream API的配合做map。</p> <pre> <code class="language-java">MapperFactory.getCopyByRefMapper().mapClass(Address.<strong>class, Address2.<strong>class).register(); MapperFactory.getCopyByRefMapper().mapClass(Person.<strong>class, PersonDto.<strong>class).register(); List<Person> personList = getPersonList(); List<PersonDto> personDtoList = personList.stream().map(p -> MapperFactory.getCopyByRefMapper().map(p, PersonDto.<strong>class)).collect(Collectors.toList());</strong></strong></strong></strong></strong></code></pre> <p>在Scala中使用</p> <pre> <code class="language-java">object EasyMapperTest { def main(args: Array[String]) { MapperFactory.getCopyByRefMapper.mapClass(classOf[Person], classOf[PersonDto]).register val personList = List( <strong>new Person("neo1", 100), <strong>new Person("neo2", 200), <strong>new Person("neo3", 300) ) val personDtoList = personList.map(p => MapperFactory.getCopyByRefMapper.map(p, classOf[PersonDto])) personDtoList.<strong>foreach(println) } }</strong></strong></strong></strong></code></pre> <p> </p> <p>来自:http://neoremind.com/2016/08/easy-mapper-一个灵活可扩展的高性能bean-mapping类库/</p> <p> </p>