首页 > 技术文章 > 使用 Orika 实现bean 映射

geass-jango 2020-01-09 17:40 原文

使用 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映射框架中最重要特性。肯定有更高级的特性能够实现更多的控制,但是在大多数场景中上述功能能够满足。

推荐阅读