Easy-mapper – 一個靈活可擴展的高性能Bean mapping類庫
1 背景
做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());
為此業界有很多開源的解決方案,列出一些常見的如下:
這些框架在使用中或多或少都會存在一些問題:
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開源。
最新發布的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
</code></pre>
首先充分預熱,各個框架,各調用一次,然后再進行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 |
-------------------------------------</code></pre>
-------------------------------------
| 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>
-------------------------------------
| 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>
結論:
首先基于大量的反射技術的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) } }</strong></strong></strong></code></pre>
來自:http://neoremind.com/2016/08/easy-mapper-一個靈活可擴展的高性能bean-mapping類庫/