1. 概述
Orika 是一个 Java Bean 映射框架,核心功能是递归地将数据从一个对象复制到另一个对象。在开发多层应用时,这个框架能极大简化工作。
当在不同层之间传递数据对象时,我们经常需要将对象转换为不同实例以适应不同的 API。传统实现方式包括:硬编码复制逻辑或使用类似 Dozer 的 Bean 映射器。而 Orika 通过字节码生成技术创建高性能映射器,相比基于反射的映射器(如 Dozer)速度更快,开销更小。
2. 简单示例
映射框架的核心是 MapperFactory
类。我们用它配置映射规则,并获取执行实际映射工作的 MapperFacade
实例。
创建 MapperFactory
对象的方式很简单:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
假设有一个源数据对象 Source.java
,包含两个字段:
public class Source {
private String name;
private int age;
public Source(String name, int age) {
this.name = name;
this.age = age;
}
// 标准 getter 和 setter
}
以及结构相似的目标对象 Dest.java
:
public class Dest {
private String name;
private int age;
public Dest(String name, int age) {
this.name = name;
this.age = age;
}
// 标准 getter 和 setter
}
最基础的 Orika 映射用法如下:
@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());
}
可以看到,我们通过映射创建了与 Source
字段完全相同的 Dest
对象。默认情况下,双向映射也是支持的:
@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());
}
3. Maven 配置
在 Maven 项目中使用 Orika,需要在 pom.xml
中添加 orika-core
依赖:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.4.6</version>
</dependency>
最新版本可在 Maven 中央仓库 查询。
4. 使用 MapperFactory
Orika 的通用映射模式包括:创建 MapperFactory
对象、配置映射规则(如需调整默认行为)、获取 MapperFacade
实例,最后执行实际映射。所有示例都会遵循这个模式,但第一个示例展示了默认行为无需任何配置。
4.1. BoundMapperFacade vs MapperFacade
⚠️ 注意:对于特定类型对的映射,使用 BoundMapperFacade
比默认的 MapperFacade
性能更好。之前的测试可优化为:
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
BoundMapperFacade<Source, Dest>
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Source src = new Source("baeldung", 10);
Dest dest = boundMapper.map(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
但 BoundMapperFacade
的双向映射需要显式调用 mapReverse
方法:
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
BoundMapperFacade<Source, Dest>
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Dest src = new Dest("baeldung", 10);
Source dest = boundMapper.mapReverse(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
否则测试会失败。
4.2. 配置字段映射
之前的示例中源对象和目标对象的字段名完全相同。现在处理字段名不同的情况:
假设源对象 Person
包含三个字段:name
、nickname
和 age
:
public class Person {
private String name;
private String nickname;
private int age;
public Person(String name, String nickname, int age) {
this.name = name;
this.nickname = nickname;
this.age = age;
}
// 标准 getter 和 setter
}
目标对象 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;
}
// 标准 getter 和 setter
}
Orika 无法自动处理这种差异,但我们可以用 ClassMapBuilder
API 注册自定义映射:
@Test
public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect() {
mapperFactory.classMap(Personne.class, Person.class)
.field("nom", "name").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(englishPerson.getName(), frenchPerson.getNom());
assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}
✅ 关键点:必须调用 register()
方法注册配置。即使只有一个字段不同,也需显式注册所有字段映射(包括相同的 age
),否则未注册的字段不会被映射。
踩坑提示:如果对象有 20 个字段但只想自定义 1 个,是否需要配置所有映射?
❌ 不需要!使用 byDefault()
让未显式定义的字段使用默认映射:
mapperFactory.classMap(Personne.class, Person.class)
.field("nom", "name").field("surnom", "nickname").byDefault().register();
这样 age
字段虽未定义映射,测试仍会通过。
4.3. 排除字段
若想排除 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());
}
注意第一个断言:name
字段因被排除而保持 null
。
5. 集合映射
有时目标对象有独立属性,而源对象将所有属性保存在集合中。
5.1. 列表和数组
假设源对象只有一个字段——姓名列表:
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
(数组而非列表),相同测试也能通过。
5.2. 映射(Map)
假设源对象包含一个 Map,其中键 first
对应目标对象的 firstName
,键 last
对应 lastName
:
public class PersonNameMap {
private Map<String, String> nameMap;
public PersonNameMap(Map<String, String> nameMap) {
this.nameMap = nameMap;
}
}
同样使用括号表示法,但传入键名而非索引。Orika 支持单引号或双引号(需转义):
@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");
}
6. 映射嵌套字段
假设源对象内部嵌套了另一个 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;
}
}
使用点表示法访问嵌套属性:
@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");
}
7. 映射空值
有时需要控制是否映射 null
值。默认情况下 Orika 会映射 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());
}
可在不同级别自定义此行为:
7.1. 全局配置
在创建 MapperFactory
时全局配置是否映射 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
会覆盖目标对象的非空值。此配置后,目标字段不会被覆盖。
7.2. 局部配置
在 ClassMapBuilder
上使用 mapNulls(true|false)
控制正向映射,或 mapNullsInReverse(true|false)
控制反向映射。配置会影响后续所有字段:
@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");
}
反向映射的 null
控制示例:
@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");
}
7.3. 字段级配置
使用 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");
}
8. Orika 自定义映射
之前使用 ClassMapBuilder
API 的简单自定义已不能满足所有场景。现在通过 Orika 的 CustomMapper
类实现复杂转换。
假设两个对象都有 dtob
字段表示出生时间:
- 一个对象使用 ISO 格式的日期时间字符串:
2007-06-26T21:22:39Z
- 另一个使用 Unix 时间戳长整型:
1182882159000
内置转换器无法处理这种转换,需编写 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;
}
}
自定义映射器实现双向转换:
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);
}
};
测试正向映射:
@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);
}
测试反向映射:
@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);
}
9. 总结
本文深入探讨了 Orika 映射框架的核心特性。虽然还有更高级的功能,但覆盖的场景已能满足绝大多数需求。完整项目代码和示例可在 GitHub 项目 查看。建议同时了解 Dozer 映射框架,两者解决的问题类似但实现方式不同。