Easy-mapper – 一個靈活可擴展的高性能Bean mapping類庫

太陽花下的 8年前發布 | 17K 次閱讀 Java 高性能 Java開發

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

為此業界有很多開源的解決方案,列出一些常見的如下:

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 </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類庫/

 

 本文由用戶 太陽花下的 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!