做Java开发都避免不了和各种Bean打交道,包括POJO、BO、VO、PO、DTO等,而Java的应用非常讲究分层的架构,因此就会存在对象在各个层次之间作为参数或者输出传递的过程,这里转换的工作往往非常繁琐。
这里举个例子,做过Java的都会深有体会,下面代码的set/get看起来不那么优雅
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就应运而生。
1、扩展性强。基于SPI技术,对于各种类型之间的转换提供默认的策略,使用者可自行添加。
2、性能高。使用Javassist字节码增强技术,在运行时动态生成mapping过程的源代码,并且使用缓存技术,一次生成后续直接使用。默认策略为基于引用拷贝,因此在Java分层的架构中可以避免对象拷贝的代价,当然这有违背于函数式编程的不可变特性,easy-mapper赞同不可变,这里只不过提供了一种选择而已,请开放兼并。
3、映射灵活。源类型和目标类型属性名可以指定,支持Java8 lambda表达式的转换函数,支持排除属性,支持全局的自定义mapping。
4、代码可读高。基于Fluent式API,链式风格。惰性求值的方式,可随意注册映射关系,最后再统一做映射。
项目托管在github上,地址点此 https://github.com/neoremind/easy-mapper 。使用Apache2 License开源。
最新发布的Jar包可以在maven中央仓库找到,地址 点此 。
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 。
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... }
从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);
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);
PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .register() Mapper mapper = MapperFactory.getCopyByRefMapper().map(p, PersonDto.class);
PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .field("salary", "salary") .register() .map(p, PersonDto.class);
从源类型中排查某个属性,不做映射。
PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .exclude("lastName") .register() .map(p, PersonDto.class);
使用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);
AtoBMapping接口做源对象到目标对象的转换。
PersonDto6 dto = new PersonDto6(); MapperFactory.getCopyByRefMapper().mapClass(Person6.class, PersonDto6.class) .customMapping((a, b) -> b.setLastName(a.getLastName().toUpperCase())) .register() .map(p, dto);
registerAndMap和map方法的第二个参数支持Class,同时也支持已经新建好的对象。如果传入Class,则使用反射新建一个对象再赋值,目标对象可以没有默认构造方法,框架会努力寻找一个最合适的构造方法构造。
PersonDto dto = new PersonDto(); MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class).registerAndMap(p, dto);
如果源属性为空,那么默认则不映射到目标属性,可以强制赋空。
PersonDto dto = MapperFactory.getCopyByRefMapper().mapClass(Person.class, PersonDto.class) .mapOnNull(true) .register() .map(p, PersonDto.class);
如果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
可指定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="..."
默认使用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.
+- 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
以下测试基于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可用于一些对性能非常考究的框架内使用。
和 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。