转载

Easy-mapper – 一个灵活可扩展的高性能Bean mapping类库

1 背景

做Java开发都避免不了和各种Bean打交道,包括POJO、BO、VO、PO、DTO等,而Java的应用非常讲究分层的架构,因此就会存在对象在各个层次之间作为参数或者输出传递的过程,这里转换的工作往往非常繁琐。

这里举个例子,做过Java的都会深有体会,下面代码的set/get看起来不那么优雅 Easy-mapper – 一个灵活可扩展的高性能Bean mapping类库

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());

为此业界有很多开源的解决方案,列出一些常见的如下:

Apache PropertyUtils

Apache BeanUtils

Cglib BeanCopier

Spring BeanUtils

Dozer

这些框架在使用中或多或少都会存在一些问题:

1、扩展性不高,例如自定义的属性转换往往不太方便。

2、属性名相同、类型不匹配或者类型匹配、属性名不同,不能很好的支持。

3、不支持Java8的lambda表达式。

4、一些框架性能不佳,例如Apache的两个和Dozer(BeanCopier使用ASM字节码生成技术,性能会非常好)。

5、对象的clone拷贝往往并不是使用者需要的,一般场景引用拷贝即可满足要求。

那么,为了解决或者优化这些问题,类库easy-mapper就应运而生。

2 Easy-mapper特点

1、扩展性强。基于SPI技术,对于各种类型之间的转换提供默认的策略,使用者可自行添加。

2、性能高。使用Javassist字节码增强技术,在运行时动态生成mapping过程的源代码,并且使用缓存技术,一次生成后续直接使用。默认策略为基于引用拷贝,因此在Java分层的架构中可以避免对象拷贝的代价,当然这有违背于函数式编程的不可变特性,easy-mapper赞同不可变,这里只不过提供了一种选择而已,请开放兼并。

3、映射灵活。源类型和目标类型属性名可以指定,支持Java8 lambda表达式的转换函数,支持排除属性,支持全局的自定义mapping。

4、代码可读高。基于Fluent式API,链式风格。惰性求值的方式,可随意注册映射关系,最后再统一做映射。

3 获取Easy-mapper

项目托管在github上,地址点此 https://github.com/neoremind/easy-mapper 。使用Apache2 License开源。

Easy-mapper – 一个灵活可扩展的高性能Bean mapping类库

最新发布的Jar包可以在maven中央仓库找到,地址 点此 。

4 上手

4.1 引入依赖

Maven:

<dependency>
    <groupId>com.baidu.unbiz</groupId>
    <artifactId>easy-mapper</artifactId>
    <version>1.0.1</version>
</dependency>

Gradle:

compile 'com.baidu.unbiz:easy-mapper:1.0.1'

注:最新release请及时参考 github 。

4.2 开发Java Bean

POJO如下:

public class Person {
    private String firstName;
    private String lastName;
    private List<String> jobTitles;
    private long salary;
    // getter and setter...
}

DTO(Data Transfer Object)如下:

public class PersonDto {
    private String firstName;
    private String lastName;
    private List<String> jobTitles;
    private long salary;
    // getter and setter...
}

4.3 映射之Helloworld

从POJO到DTO的映射如下,

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);

5 深入实践

5.1 注册和映射分开

helloworld中使用了registerAndMap(..)方法,其实可以分开使用,register只是让easy-mapper去解析属性并生成代码,一旦生成即缓存,然后随时map。 

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .register()
                .map(p, PersonDto.class);

先注册,拿到mapper,再映射。

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .register()
Mapper mapper = MapperFactory.getCopyByRefMapper();
PersonDto dto = mapper.map(p, PersonDto.class);
先注册,拿到mapper直接映射。
PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .register()
Mapper mapper = MapperFactory.getCopyByRefMapper().map(p, PersonDto.class);

5.2 指定属性名称

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .field("salary", "salary")
                .register()
                .map(p, PersonDto.class);

5.3 忽略某个属性

从源类型中排查某个属性,不做映射。

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                .exclude("lastName")
                .register()
                .map(p, PersonDto.class);

5.4 自定义属性转换

使用Transformer接口。

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);

Java8的lambda表达式使用方式如下。

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                    .field("firstName", "firstName", (String s) -> s.toLowerCase())
                    .register()
                    .map(p, PersonDto.class);

Java8的stream方式如下。

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                    .field("jobTitles", "jobTitleLetterCounts",
                            (List<String> s) -> s.stream().map(String::length).toArray(Integer[]::new))
                    .register()
                    .map(p, PersonDto.class);

如果指定了属性了类型,那么lambda表达式则不用写类型,Java编译器可以推测。

MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                    .field("firstName", "firstName", String.class, String.class, s -> s.toLowerCase())
                    .register()
                    .map(p, PersonDto.class);

5.5 自定义额外的全局转换

AtoBMapping接口做源对象到目标对象的转换。

PersonDto6 dto = new PersonDto6();
MapperFactory.getCopyByRefMapper().mapClass(Person6.class, PersonDto6.class)
        .customMapping((a, b) -> b.setLastName(a.getLastName().toUpperCase()))
        .register()
        .map(p, dto);

5.6 映射已经新建的对象

registerAndMap和map方法的第二个参数支持Class,同时也支持已经新建好的对象。如果传入Class,则使用反射新建一个对象再赋值,目标对象可以没有默认构造方法,框架会努力寻找一个最合适的构造方法构造。

PersonDto dto = new PersonDto();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).registerAndMap(p, dto);

5.7 源属性为空是否映射

如果源属性为空,那么默认则不映射到目标属性,可以强制赋空。 

PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class)
                    .mapOnNull(true)
                    .register()
                    .map(p, PersonDto.class);

5.8 级联映射

如果Person类型中有Address,而PersonDto类型中有Address2,那么需要首先映射下,如下所示。 

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
Person p = getPerson(); 
p.setAddress(new Address("beverly hill", 10086));
PersonDto dto = MapperFactory.getCopyByRefMapper()
					.mapClass(Person.class, PersonDto.class)
					.register()
					.map(p, PersonDto.class);

如果没有提前注册,那么会抛出如下异常:

com.baidu.unbiz.easymapper.exception.MappingException: No class map found for (Address, Address2), make sure type or nested type is registered beforehand

5.9 输出生产的源代码

可指定log的level为debug,则会在console输出生成的源代码。

另外,可在环境变量中指定如下参数,输出源代码或者编译后的class文件到本地文件系统。 

-Dcom.baidu.unbiz.easymapper.enableWriteSourceFile=true 
-Dcom.baidu.unbiz.easymapper.writeSourceFileAbsolutePath="..."
-Dcom.baidu.unbiz.easymapper.enableWriteClassFile=true 
-Dcom.baidu.unbiz.easymapper.writeClassFileAbsolutePath="..."

6 框架映射规则

默认使用SPI技术加载框架预置的属性处理器。

在META-INF/services/com.baidu.unbiz.easymapper.mapping.MappingHandler文件中,规则优先级由高到低如下:

1、指定了Transformer,则用自定义的transformer。

2、属性类型相同,则直接按引用拷贝赋值;primitive以及wrapper类型,直接使用“=”操作符赋值。

3、如果目标属性类型是String,那么尝试源对象直接调用toString()方法映射。

4、如果源属性是目标属性的子类,则直接引用拷贝。

5、如果是其他情况,则级联的调用mapper.map(..),注意框架未处理dead cycle的情况。

最后,如果5仍然不能完成映射,那么框架会抛出如下异常:

com.baidu.unbiz.easymapper.exception.MappingCodeGenerationException: No appropriate mapping strategy found for FieldMap[jobTitles(List<string>)-->jobTitles(List<integer>)] 
... 
com.baidu.unbiz.easymapper.exception.MappingException: Generating mapping code failed for ClassMap([A]:Person6, [B]:PersonDto6), this should not happen, probably the framework could not handle mapping correctly based on your bean.

7、框架依赖类库

+- 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

8、性能测试报告

以下测试基于Oracal Hotspot JVM,参数如下:

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

首先充分预热,各个框架,各调用一次,然后再进行benchmark。

测试机器配置如下:

CPU: Intel(R) Core(TM) i5-4278U CPU @ 2.60GHz

MEM: 8G

测试代码见链接 BenchmarkTest.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      |
-------------------------------------
-------------------------------------
| 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      |
-------------------------------------
-------------------------------------
| 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      |
-------------------------------------

结论:

首先基于大量的反射技术的Apache的两个工具BeanUtils和PropertyUtils性能均不理想,Dozer的性能则更为不好。

其次,基于ASM字节码增强技术的Cglib库真是经久不衰,性能在各个场景下均表现非常突出,甚至好于纯手写的get/set。

最后,在调用10,000次时,easy-mapper好于Spring的BeanUtils,100,000次时持平,但是达到1,000,000次时,则落后。由于Spring BeanUtils非常的简单,采用了反射技术Method.invoke(..)做赋值处理,一般现代编译器都会对“热点”代码做优化,如R神的 《关于反射调用方法的一个log》 提到的,可以看出超过一定调用次数后,基于profiling信息,JIT同样可以对反射做自适应的代码优化,这里对Method.invoke(..)在调动超过一定次数时会转为代理类来做实现,而不是调用native方法,因此JIT就可以做很多dereflection的事情优化性能,因此Spring的BeanUtils性能也不差。

可以看出相比于老派的框架,easy-mapper性能非常优秀,虽然和Cglib BeanCopier有差距,这也可以看出使用Javassist的source level的API来做字节码操作性能肯定不会优于直接用ASM,但是easy-mapper的特点在于灵活、可扩展性、良好的编程体验方面,因此从这个tradeoff来看,easy-mapper非常适用于生产环境和工业界,而Cglib可用于一些对性能非常考究的框架内使用。

9、与高阶函数搭配使用

和 guava 一起使用做集合的转换。 

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).register();
List<Person> personList = getPersonList();
Collection<PersonDto> personDtoList = Collections2.transform(personList,
        p -> MapperFactory.getCopyByRefMapper().map(p, PersonDto.class));
System.out.println(personDtoList);

和 functional java 一起使用做集合的转换。

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).register();
List<Person> personList = getPersonList();
fj.data.List<PersonDto> personDtoList = fj.data.List.fromIterator(personList.iterator()).map(
        person -> MapperFactory.getCopyByRefMapper().map(person, PersonDto.class));
personDtoList.forEach(e -> System.out.println(e));

和Java8的stream API的配合做map。

MapperFactory.getCopyByRefMapper().mapClass(Address.class, Address2.class).register();
MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).register();
List<Person> personList = getPersonList();
List<PersonDto> personDtoList = personList.stream().map(p -> MapperFactory.getCopyByRefMapper().map(p,
        PersonDto.class)).collect(Collectors.toList());

在Scala中使用

object EasyMapperTest {
 
  def main(args: Array[String]) {
    MapperFactory.getCopyByRefMapper.mapClass(classOf[Person], classOf[PersonDto]).register
    val personList = List(
      new Person("neo1", 100),
      new Person("neo2", 200),
      new Person("neo3", 300)
    )
    val personDtoList = personList.map(p => MapperFactory.getCopyByRefMapper.map(p, classOf[PersonDto]))
    personDtoList.foreach(println)
  }
 
}

转载时请注明转自neoremind.com。

原文  http://neoremind.com/2016/08/easy-mapper-一个灵活可扩展的高性能bean-mapping类库/
正文到此结束
Loading...