使用 Orika 實現bean 映射
Orika是java Bean映射框架,可以實現從一個對象遞歸拷貝數據至另一個對象。在開發多層應用程序中非常有用。在這些層之間交換數據時,通常為了適應不同API需要轉換一個實例至另一個實例。
有很多方法可以實現:硬代碼拷貝或Dozer實現bean映射等。總之,需要簡化不同層對象之間映射過程。
Orika使用字節碼生成器創建開銷最小的快速映射,比其他基於反射方式實現(如,Dozer)更快。
簡單示例
映射框架的基礎類是MapperFactory類,其用於配置映射並獲得用於執行實際映射工作的MapperFacade實例。
創建MapperFactory對象如下:
1 MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
假設源數據對象Source.java,帶兩個字段:
1 public class Source { 2 private String name; 3 private int age; 4 5 public Source(String name, int age) { 6 this.name = name; 7 this.age = age; 8 } 9 10 // standard getters and setters 11 }
類似的目標數據對象,Dest.java:
1 public class Dest { 2 private String name; 3 private int age; 4 5 public Dest(String name, int age) { 6 this.name = name; 7 this.age = age; 8 } 9 10 // standard getters and setters 11 }
使用Orika實現基本的bean映射:
@Test public void givenSrcAndDest_whenMaps_thenCorrect() { mapperFactory.classMap(Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade(); Source src = new Source("Baeldung", 10); Dest dest = mapper.map(src, Dest.class); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), src.getName()); }
我們創建Dest對象帶有與Souce相同的屬性,實現簡單映射,也可以實現雙向映射或反向映射:
@Test public void givenSrcAndDest_whenMapsReverse_thenCorrect() { mapperFactory.classMap(Source.class, Dest.class).byDefault(); MapperFacade mapper = mapperFactory.getMapperFacade(); Dest src = new Dest("Baeldung", 10); Source dest = mapper.map(src, Source.class); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), src.getName()); }
Maven依賴
使用Orika 映射框架,我們需要加入maven映射:
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.4.6</version> </dependency>
讀者可以查找最新版本。
使用MapperFactory
使用Orika進行映射的一般模式為創建MapperFactory對象,在必要時調整默認映射行為對其進行配置,從MapperFactory獲取MapperFacade對象,最后進行實際的映射。
我們將在我們所有的例子中觀察這種模式。但我們的第一個示例顯示了mapper的默認行為,並沒有任何調整。
BoundMapperFacade vs MapperFacade
另外需要注意的是我們可以選擇BoundMapperFacade 代替缺省性能較慢的 MapperFacade 類。一些場景中一對類型需要相互映射。下面舉例說明:
1 @Test 2 public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() { 3 BoundMapperFacade<Source, Dest> 4 boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class); 5 Source src = new Source("baeldung", 10); 6 Dest dest = boundMapper.map(src); 7 8 assertEquals(dest.getAge(), src.getAge()); 9 assertEquals(dest.getName(), src.getName()); 10 }
然而,對於BoundMapperFacade的雙向映射,我們必須明確地調用mapReverse方法,而不是我們在默認MapperFacade中看到的map方法,否則下面示例測試會失敗:
1 @Test 2 public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() { 3 BoundMapperFacade<Source, Dest> 4 boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class); 5 Dest src = new Dest("baeldung", 10); 6 Source dest = boundMapper.mapReverse(src); 7 8 assertEquals(dest.getAge(), src.getAge()); 9 assertEquals(dest.getName(), src.getName()); 10 }
配置字段映射
到目前為止,我們示例中源對象和目標對象有相同字段名稱,本節我們討論兩個對象中字段名稱不同情況:
假設源對象Person,有三個字段分別為name,nickname,age:
1 public class Person { 2 private String name; 3 private String nickname; 4 private int age; 5 6 public Person(String name, String nickname, int age) { 7 this.name = name; 8 this.nickname = nickname; 9 this.age = age; 10 } 11 12 // standard getters and setters 13 }
應用其他層有類似對象,為了更好理解業務,對象命名為Personne,帶有字段名稱分別為nom,surnom,age,各隊對應上面對象的三個字段:
public class Personne { private String nom; private String surnom; private int age; public Personne(String nom, String surnom, int age) { this.nom = nom; this.surnom = surnom; this.age = age; } // standard getters and setters }
Orika不能自動解決這些差異,但我們可以使用ClassMapBuilder API去注冊這些唯一映射。我們之前已經使用過,但么有涉及其強大特性,前面示例使用缺省MapperFacade使用ClassMapBuilder API去注冊需要進行映射的兩個類:
mapperFactory.classMap(Source.class, Dest.class);
我們也可以映射所有字段使用缺省配置,使其更清晰:
mapperFactory.classMap(Source.class, Dest.class).byDefault()
通過調用byDefault()方法,我們已經使用ClassMapBuilder API配置映射行為:
1 @Test 2 public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect() { 3 mapperFactory.classMap(Personne.class, Person.class) 4 .field("nom", "name").field("surnom", "nickname") 5 .field("age", "age").register(); 6 MapperFacade mapper = mapperFactory.getMapperFacade(); 7 Personne frenchPerson = new Personne("Claire", "cla", 25); 8 Person englishPerson = mapper.map(frenchPerson, Person.class); 9 10 assertEquals(englishPerson.getName(), frenchPerson.getNom()); 11 assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom()); 12 assertEquals(englishPerson.getAge(), frenchPerson.getAge()); 13 }
不要忘了調用.register()方法,為了給MapperFactory注冊配置信息。
即使只有一個字段不同,我們也必須注冊所有字段映射,包括兩個對象都有擁有相同名稱的age字段,否則未注冊字段不能被映射,則單元測試不能通過。
這顯然是多余的,如果我們僅需映射20個字段中的一個,也必須配置所有這些映射?當然不,我們可以通過設定缺省映射配置,則無需顯示定義映射:
mapperFactory.classMap(Personne.class, Person.class).field("nom", "name").field("surnom", "nickname").byDefault().register();
這里,我們沒有定義age字段映射,但單元測試可以通過。
排除字段
假設我們需要排除Personne對象中的nom字段映射,主要Person對象僅接受沒有被排除字段的值:
@Test public void givenSrcAndDest_whenCanExcludeField_thenCorrect() { mapperFactory.classMap(Personne.class, Person.class).exclude("nom") .field("surnom", "nickname").field("age", "age").register(); MapperFacade mapper = mapperFactory.getMapperFacade(); Personne frenchPerson = new Personne("Claire", "cla", 25); Person englishPerson = mapper.map(frenchPerson, Person.class); assertEquals(null, englishPerson.getName()); assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom()); assertEquals(englishPerson.getAge(), frenchPerson.getAge()); }
因為我們在配置排除了nom字段映射,所以單元測試中第一個行name值斷言為null。
集合映射
有時目標對象可能有多個屬性,而源對象則在集合中維護每個屬性。
List和數值
假設元數據對象僅有一個字段,person的name 列表:
public class PersonNameList { private List<String> nameList; public PersonNameList(List<String> nameList) { this.nameList = nameList; } }
目標對象拆分為firstName和lastName兩個字段:
public class PersonNameParts { private String firstName; private String lastName; public PersonNameParts(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
假設我們確定索引0映射到firstName,索引1映射至lastName。Orika允許使用括號來表示集合成員:
@Test public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() { mapperFactory.classMap(PersonNameList.class, PersonNameParts.class) .field("nameList[0]", "firstName") .field("nameList[1]", "lastName").register(); MapperFacade mapper = mapperFactory.getMapperFacade(); List<String> nameList = Arrays.asList(new String[] { "Sylvester", "Stallone" }); PersonNameList src = new PersonNameList(nameList); PersonNameParts dest = mapper.map(src, PersonNameParts.class); assertEquals(dest.getFirstName(), "Sylvester"); assertEquals(dest.getLastName(), "Stallone"); }
即使使用 PersonNameArray數組代替PersonNameList,測試結果一樣會通過。
Map
假設源對象有map存儲值,map有個鍵名為first,其值對應目標對象的firstName,另一個鍵last,其值對應目標對象的lastName。
public class PersonNameMap { private Map<String, String> nameMap; public PersonNameMap(Map<String, String> nameMap) { this.nameMap = nameMap; } }
與上述示例類似,我們使用括號標識,但使用名稱而不是使用索引獲取源對象的值。
Orika支持兩種方式返回key對應值,測試代碼如下:
@Test public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() { mapperFactory.classMap(PersonNameMap.class, PersonNameParts.class) .field("nameMap['first']", "firstName") .field("nameMap[\"last\"]", "lastName") .register(); MapperFacade mapper = mapperFactory.getMapperFacade(); Map<String, String> nameMap = new HashMap<>(); nameMap.put("first", "Leornado"); nameMap.put("last", "DiCaprio"); PersonNameMap src = new PersonNameMap(nameMap); PersonNameParts dest = mapper.map(src, PersonNameParts.class); assertEquals(dest.getFirstName(), "Leornado"); assertEquals(dest.getLastName(), "DiCaprio"); }
我們可以使用單引號或雙引號,但后者必須轉義。
映射嵌套字段
接着前面的集合示例,假設在源數據對象中,有另一個數據傳輸對象(DTO)保存我們要映射的值。
public class PersonContainer { private Name name; public PersonContainer(Name name) { this.name = name; } } public class Name { private String firstName; private String lastName; public Name(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
為了訪問嵌套DTO的屬性,並映射至我們的目標對象,我們使用點號,代碼如下:
@Test public void givenSrcWithNestedFields_whenMaps_thenCorrect() { mapperFactory.classMap(PersonContainer.class, PersonNameParts.class) .field("name.firstName", "firstName") .field("name.lastName", "lastName").register(); MapperFacade mapper = mapperFactory.getMapperFacade(); PersonContainer src = new PersonContainer(new Name("Nick", "Canon")); PersonNameParts dest = mapper.map(src, PersonNameParts.class); assertEquals(dest.getFirstName(), "Nick"); assertEquals(dest.getLastName(), "Canon"); }
映射null值
有時需要控制null值是否映射,缺省情況下,遇到null值會映射。
@Test public void givenSrcWithNullField_whenMapsThenCorrect() { mapperFactory.classMap(Source.class, Dest.class).byDefault(); MapperFacade mapper = mapperFactory.getMapperFacade(); Source src = new Source(null, 10); Dest dest = mapper.map(src, Dest.class); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), src.getName()); }
這種特性依賴你在不同級別的設置。
全局設置
我們可以在創建MapperFactory之前,進行設置映射null值或忽略null值。如我們第一個示例所示,我們增加額外的配置:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder()
.mapNulls(false).build();
我們運行測試進行缺省,null值沒有被映射:
@Test public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect() { mapperFactory.classMap(Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade(); Source src = new Source(null, 10); Dest dest = new Dest("Clinton", 55); mapper.map(src, dest); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), "Clinton"); }
缺省情況下,null會被映射,這意味着源對象字段值為null,而目標對象對應字段值有實際值,映射后將被覆蓋為null。上述示例中如果源對象字段值為null,目標字段沒有被覆蓋。
局部配置
映射null值可以通過ClassMapBuilder類mapNulls(true|false)方法進行控制,或 mapNullsInReverse(true|false)方法進行反向映射控制。
通過ClassMapBuilder實例設置值,ClassMapBuilder設置之前有相同方式的映射,之后采用設置后的相同方式映射。下面進行示例測試:
@Test public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect() { mapperFactory.classMap(Source.class, Dest.class).field("age", "age") .mapNulls(false).field("name", "name").byDefault().register(); MapperFacade mapper = mapperFactory.getMapperFacade(); Source src = new Source(null, 10); Dest dest = new Dest("Clinton", 55); mapper.map(src, dest); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), "Clinton"); }
mapNulls在這次name字段之前調用,導致后續所有字段忽略null值映射。雙向映射也接受映射null值:
@Test public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect() { mapperFactory.classMap(Source.class, Dest.class).byDefault(); MapperFacade mapper = mapperFactory.getMapperFacade(); Dest src = new Dest(null, 10); Source dest = new Source("Vin", 44); mapper.map(src, dest); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), src.getName()); }
我們通過設置mapNullsInReverse方法的參數為false來改變這種行為:
@Test public void givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect() { mapperFactory.classMap(Source.class, Dest.class).field("age", "age") .mapNullsInReverse(false).field("name", "name").byDefault() .register(); MapperFacade mapper = mapperFactory.getMapperFacade(); Dest src = new Dest(null, 10); Source dest = new Source("Vin", 44); mapper.map(src, dest); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), "Vin"); }
字段級別配置
通過使用fieldMap進行字段級別配置,示例代碼如下:
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.fieldMap("name", "name").mapNulls(false).add().byDefault().register();
這種情況下,配置僅影響name字段:
@Test public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect() { mapperFactory.classMap(Source.class, Dest.class).field("age", "age") .fieldMap("name", "name").mapNulls(false).add().byDefault().register(); MapperFacade mapper = mapperFactory.getMapperFacade(); Source src = new Source(null, 10); Dest dest = new Dest("Clinton", 55); mapper.map(src, dest); assertEquals(dest.getAge(), src.getAge()); assertEquals(dest.getName(), "Clinton"); }
Orika自定義映射
目前為止,我們看了ClassMapBuilder API實現簡單自定義映射示例。接下來我們仍然使用相同的API,但使用Orika的CustomMapper 類自定義映射行為。
假設我們有兩個數據對象,每個帶有特定字段為dtob,表示人的出生日期。
一個對象使用 ISO 格式的日期字符串表示該值:
2007-06-26T21:22:39Z
另一個使用long類型的unix timestamp格式表示該值:
1182882159000
顯然,目前我們學習的方法不能滿足兩個對象映射過程中進行轉換,即使Orika內置轉換器也不能處理這種情況。這時我們必須寫一個CustomMapper實現必要的映射過程轉換。
下面創建第一個數據對象:
public class Person3 { private String name; private String dtob; public Person3(String name, String dtob) { this.name = name; this.dtob = dtob; } }
第二個對象:
public class Personne3 { private String name; private long dtob; public Personne3(String name, long dtob) { this.name = name; this.dtob = dtob; } }
我們現在不標記哪個是源,哪個是目標,因為CustomMapper允許我們處理雙向映射。
下面是CustomMapper 抽象類的具體實現:
class PersonCustomMapper extends CustomMapper<Personne3, Person3> { @Override public void mapAtoB(Personne3 a, Person3 b, MappingContext context) { Date date = new Date(a.getDtob()); DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); String isoDate = format.format(date); b.setDtob(isoDate); } @Override public void mapBtoA(Person3 b, Personne3 a, MappingContext context) { DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); Date date = format.parse(b.getDtob()); long timestamp = date.getTime(); a.setDtob(timestamp); } };
我們已經實現mapAtoB 和 mapBtoA 方法用於雙向映射轉換功能。每個方法都暴露我們需要映射的數據對象,負責將字段值從一個復制到另一個。這里我們自定義邏輯實現源數據根據需要格式進行轉換並復制至目標對象,下面運行測試查看自定義映射情況:
@Test public void givenSrcAndDest_whenCustomMapperWorks_thenCorrect() { mapperFactory.classMap(Personne3.class, Person3.class) .customize(customMapper).register(); MapperFacade mapper = mapperFactory.getMapperFacade(); String dateTime = "2007-06-26T21:22:39Z"; long timestamp = new Long("1182882159000"); Personne3 personne3 = new Personne3("Leornardo", timestamp); Person3 person3 = mapper.map(personne3, Person3.class); assertEquals(person3.getDtob(), dateTime); }
我們仍然通過ClassMapBuilder API傳遞我們自定義的映射類給Orika的映射器,與之前示例代碼類似。
反向映射測試也可以正常工作:
@Test public void givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect() { mapperFactory.classMap(Personne3.class, Person3.class) .customize(customMapper).register(); MapperFacade mapper = mapperFactory.getMapperFacade(); String dateTime = "2007-06-26T21:22:39Z"; long timestamp = new Long("1182882159000"); Person3 person3 = new Person3("Leornardo", dateTime); Personne3 personne3 = mapper.map(person3, Personne3.class); assertEquals(person3.getDtob(), timestamp); }
自定義轉換器
當兩者類型在大多數場合都需要進行轉換時,每次都要定義CustomMapper,未免顯得比較麻煩,雖然其可以進行細粒度控制。如Date到String類型的轉換,能否定義一次就,則在多數場景中進行復用。
orika有一些內置的轉換器可以實現該功能。本節簡單介紹如何實現自定義轉換器。
自定義單向轉換器
當內置功能不能滿足時,需要自定義,示例代碼如下:
public class MyConverter extends CustomConverter<Date,MyDate> { public MyDate convert(Date source, Type<? extends MyDate> destinationType) { // return a new instance of destinationType with all properties filled } }
自定義雙向轉換器
雙向轉換器需要實現兩個方法,示例代碼如下:
public class MyConverter extends BidirectionalConverter<Date,MyDate> { public MyDate convertTo(Date source, Type<MyDate> destinationType) { // convert in one direction } public Date convertFrom(MyDate source, Type<Date> destinationType) { // convert in the other direction } }
全局注冊轉換器
為了轉換器能被識別並在映射過程中使用,必須通過ConverterFactory 進行注冊。有兩種方式進行注冊,下面是全局方式注冊:
ConverterFactory converterFactory = mapperFactory.getConverterFactory(); converterFactory.registerConverter(new MyConverter());
當源和目標類型與轉換器定義的類型兼容時,在全局級別注冊的轉換器將被使用。
注冊字段級別轉換器
需要兩步進行注冊,首先定義轉換器的id(字符串),代碼如下:
ConverterFactory converterFactory = mapperFactory.getConverterFactory(); converterFactory.registerConverter("myConverterIdValue", new MyConverter());
指定轉換器id至任何字段映射中:
mapperFactory.classMap( Source.class, Destination.class ) .fieldMap("sourceField1", "sourceField2").converter("myConverterIdValue").add() ... .register();
結論
本文我們瀏覽了Orika映射框架中最重要特性。肯定有更高級的特性能夠實現更多的控制,但是在大多數場景中上述功能能夠滿足。
