前言
在现在流行且广泛应用的各类 Java 分层代码框架中,不同的层定义了不同类型的实体类,如 Entity、DO、DTO、VO 等。层与层之间的对象总是需要进行各种转换和映射,这些操作重复繁琐,于是催生了各种各样的工具,用来快捷、高效地完成这些操作。本文先介绍一款 Java 的拷贝工具 Orika
,简单讲解一下它的特性,以及进行一些常用的、基础的代码演示。
更加详细和高级的用法,可以参考其官方文档:http://orika-mapper.github.io/orika-docs/index.html
基本介绍
Orika
使用反射来访问数据对象的属性,它会自动收集类的元数据,生成映射对象。这些映射对象可用于将数据从一个对象递归拷贝到另一个对象。Orika 的整体设计试图在保持相对简单和开放的同时,提供许多方便的功能,从而使用者可以根据自己的需要对其进行扩展和调整。
Orika
是基于 JavaBeans
规范的属性拷贝框架,默认情况下是读取符合 JavaBeans
标准的对象属性。这意味着,具有 getName
或 setName
方法的对象将被称为具有名为 name
的属性;setXXX
方法不是必需的,对于布尔返回类型,getXXX
方法可以选择命名为 isXXX
。Orika
在此基础上更进一步,将 java.lang.Boolean
类型纳入可选的 isXXX
命名中,而且 Orika
还将公共字段识别为可使用的属性。因此 Orika
可以看作支持了我们日常开发工作中大部分常用的数据类型。
实战应用
添加 maven 依赖
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.4.0</version>
<scope>compile</scope>
</dependency>
主要需要添加的服务依赖是 orika-core
,另外添加了 lombok
和 testng
的依赖用以支持测试用例的编写,作为示例。
定义实体类
这边分别定义两组实体类:一组是字段名称相同的成员信息 MemberInfoDTO
和 MemberInfoVO
;另一组是个别字段名不一致的人员信息 PersonSrcInfo
和 PersonDestInfo
。
@Data
public class MemberInfoDTO {
private Long id;
private String name;
private Integer age;
private String email;
private String identity;
private String group;
}
@Data
public class MemberInfoVO {
private Long id;
private String name;
private Integer age;
private String email;
private String identity;
private String group;
}
@Data
@FieldNameConstants
public class PersonSrcInfo {
private Long id;
private String name;
private Integer age;
private LocalDate birth;
private String email;
// 无用字段,用于演示拷贝时指定排除字段
private String removeVal;
}
@Data
@FieldNameConstants
public class PersonDesInfo {
private Long id;
private String personName;
private Integer age;
private LocalDate dateOfBirth;
private String emailAddress;
}
这里使用了 @FieldNameConstants
注解,可以更加方便地指定实体类中成员变量的名称,用来指定特定字段之间的映射关联。后面具体用到的时候会提到。
简单功能演示
这里先写一个基础的 OrikaDemoTest
,显式调用了 Orika
的映射工厂,对指定的类实例进行了数据的拷贝。共有三个演示用例:
- 字段名完全相同的两个实例拷贝,DTO 转 VO。
- 可以使用返回参数的形式
MemberInfoVO vo = facade.map(dto, MemberInfoVO.class);
- 也可以使用参数内置的形式
facade.map(dto, vo);
- 可以使用返回参数的形式
- 字段名完全相同的两个实例列表拷贝,List<DTO> 转 List<VO>。
- 使用
List<MemberInfoVO> voList = facade.mapAsList(dtoList, MemberInfoVO.class);
- 使用
- 部分字段名不同、且部分字段不参与拷贝的两个实例拷贝。
mapperFactory.classMap
指定参与映射的两个类.field()
方法指定映射关联的两个字段。.field(PersonSrcInfo.Fields.name, PersonDesInfo.Fields.personName)
当中的.Fields
则是通过注解@FieldNameConstants
直接方法字段名称的方式.exclude(PersonSrcInfo.Fields.removeVal)
指定不参与拷贝的成员变量.byDefault()
剩余成员默认按照名称进行匹配
public class OrikaDemoTest {
@Test
void testBasicMap() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade facade = mapperFactory.getMapperFacade();
MemberInfoDTO dto = initMemberInfo();
MemberInfoVO vo = facade.map(dto, MemberInfoVO.class);
// facade.map(dto, vo);
assertMemberInfoEquals(dto, vo);
}
@Test
void testMapAsList() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade facade = mapperFactory.getMapperFacade();
MemberInfoDTO dto1 = initMemberInfo();
MemberInfoDTO dto2 = initMemberInfo2();
List<MemberInfoDTO> dtoList = Lists.newArrayList(dto1, dto2);
List<MemberInfoVO> voList = facade.mapAsList(dtoList, MemberInfoVO.class);
for (int i = 0; i < dtoList.size(); ++i) {
assertMemberInfoEquals(dtoList.get(i), voList.get(i));
}
}
private MemberInfoDTO initMemberInfo() {
MemberInfoDTO member = new MemberInfoDTO();
member.setId(1000100010001L);
member.setName("Gogo");
member.setAge(14);
member.setEmail("gogo@oo.com");
member.setIdentity("employee");
member.setGroup("marketing");
return member;
}
private MemberInfoDTO initMemberInfo2() {
MemberInfoDTO member = new MemberInfoDTO();
member.setId(1000100010002L);
member.setName("Pato");
member.setAge(42);
member.setEmail("Pato@oo.com");
member.setIdentity("manager");
member.setGroup("customer-service");
return member;
}
private void assertMemberInfoEquals(MemberInfoDTO dto, MemberInfoVO vo) {
assertEquals(dto.getId(), vo.getId());
assertEquals(dto.getName(), vo.getName());
assertEquals(dto.getAge(), vo.getAge());
assertEquals(dto.getGroup(), vo.getGroup());
assertEquals(dto.getEmail(), vo.getEmail());
assertEquals(dto.getIdentity(), vo.getIdentity());
}
@Test
void testMapDiffFieldName() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 使用注解显式获取变量名,需要指定所有的字段映射关系,剩余通过 byDefault() 指定
mapperFactory.classMap(PersonSrcInfo.class, PersonDesInfo.class)
.field(PersonSrcInfo.Fields.name, PersonDesInfo.Fields.personName)
.field(PersonSrcInfo.Fields.birth, PersonDesInfo.Fields.dateOfBirth)
.field(PersonSrcInfo.Fields.email, PersonDesInfo.Fields.emailAddress)
.exclude(PersonSrcInfo.Fields.removeVal)
.byDefault()
.register();
MapperFacade facade = mapperFactory.getMapperFacade();
PersonSrcInfo src = initPersonInfo();
PersonDesInfo dest = facade.map(src, PersonDesInfo.class);
assertPersonInfoEquals(src, dest);
}
private PersonSrcInfo initPersonInfo() {
PersonSrcInfo person = new PersonSrcInfo();
person.setId(1000100010001L);
person.setName("Gogo");
person.setAge(14);
person.setBirth(LocalDate.of(2004, 6, 19));
person.setEmail("gogo@oo.com");
person.setRemoveVal("to be removed!");
return person;
}
private void assertPersonInfoEquals(PersonSrcInfo src, PersonDesInfo dest) {
assertEquals(src.getId(), dest.getId());
assertEquals(src.getName(), dest.getPersonName());
assertEquals(src.getAge(), dest.getAge());
assertEquals(src.getEmail(), dest.getEmailAddress());
assertEquals(src.getBirth(), dest.getDateOfBirth());
assertEquals(src.getRemoveVal(), "to be removed!");
assertNull(dest.getRemoveVal());
}
}
整体来看,Orika
的实例拷贝主要是通过 DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
和 MapperFacade facade = mapperFactory.getMapperFacade();
这两行代码获取到 mapper
,进而对指定的实体类执行字段值拷贝。当遇到字段名不完全相同的实例映射,则需要额外指定其字段间的映射关联关系。因此,我们可以对其进行封装,定义成一个本地的通用工具类进行使用。
通用工具类实现
这里将 Orika
的拷贝方法封装成工具类,不再需要重复的初始化 MapperFactory
工厂实例,这里可以通过静态成员变量实现其单例,通过复用提升性能。下面部分的代码则是封装了拷贝对象用到的基本方法,只需要传入需要进行映射的两个类或者其实例对象即可,非常简便。
/**
* orika基础映射工具类
*/
public final class OrikaMapperUtils {
private static final MapperFacade MAPPER_FACADE;
static {
MapperFactory mapperFactory = new DefaultMapperFactory
.Builder()
.useAutoMapping(true)
.mapNulls(true)
.build();
MAPPER_FACADE = mapperFactory.getMapperFacade();
}
/*************************** 工具方法部分 ***************************/
public static <S, D> void map(S src, D dest) {
MAPPER_FACADE.map(src, dest);
}
public static <S, D> D map(S src, Class<D> destClazz) {
return MAPPER_FACADE.map(src, destClazz);
}
public static <S, D> List<D> mapAsList(Iterable<S> src, Class<D> destClazz) {
return MAPPER_FACADE.mapAsList(src, destClazz);
}
}
同样的,这里通过三组测试用例来演示 Orika 通用映射工具类的功能实现。
public class OrikaMapperUtilsTest {
@Test
void testBasicObjectMap() {
MemberInfoDTO dto = initMemberInfo();
MemberInfoVO vo = new MemberInfoVO();
OrikaMapperUtils.map(dto, vo);
assertMemberInfoEquals(dto, vo);
}
@Test
void testBasicClazzMap() {
MemberInfoDTO dto = initMemberInfo();
MemberInfoVO vo = OrikaMapperUtils.map(dto, MemberInfoVO.class);
assertMemberInfoEquals(dto, vo);
}
@Test
void testMapAsList() {
MemberInfoDTO dto1 = initMemberInfo();
MemberInfoDTO dto2 = initMemberInfo2();
List<MemberInfoDTO> dtoList = Lists.newArrayList(dto1, dto2);
List<MemberInfoVO> voList = OrikaMapperUtils.mapAsList(dtoList, MemberInfoVO.class);
for (int i = 0; i < dtoList.size(); ++i) {
assertMemberInfoEquals(dtoList.get(i), voList.get(i));
}
}
private MemberInfoDTO initMemberInfo() {
MemberInfoDTO member = new MemberInfoDTO();
member.setId(1000100010001L);
member.setName("Gogo");
member.setAge(14);
member.setEmail("gogo@oo.com");
member.setIdentity("employee");
member.setGroup("marketing");
return member;
}
private MemberInfoDTO initMemberInfo2() {
MemberInfoDTO member = new MemberInfoDTO();
member.setId(1000100010002L);
member.setName("Pato");
member.setAge(42);
member.setEmail("Pato@oo.com");
member.setIdentity("manager");
member.setGroup("customer-service");
return member;
}
private void assertMemberInfoEquals(MemberInfoDTO dto, MemberInfoVO vo) {
assertEquals(dto.getId(), vo.getId());
assertEquals(dto.getName(), vo.getName());
assertEquals(dto.getAge(), vo.getAge());
assertEquals(dto.getGroup(), vo.getGroup());
assertEquals(dto.getEmail(), vo.getEmail());
assertEquals(dto.getIdentity(), vo.getIdentity());
}
}
进阶的通用工具类实现
从上面的工具类方法可以看出,上述的代码只是满足字段名完全相同的实例对象的拷贝,如果需要支持多组字段名不完全一致的对象进行拷贝,则还需要维护每组映射关系,否则就只能像最前面的简单功能演示一样,每次映射都预先定义字段的映射关系,这样代码就会变得非常繁琐。即便 MapperFactory
实现了单例,但是每次映射时都需要重新请求获取映射关系对应的 mapperFacade
实例,还是会影响到执行的性能。
下面是拓展后的通用工具类定义,新增一个静态全局的 CACHE_MAPPER
用来缓存映射关系,结合方法 register
和 classMap
实现映射关系的写入和查询。
/**
* orika进阶映射工具类
* - 映射时指定字段的映射关系
* - 指定一组两个实体类的字段映射关系可缓存在本地
*/
public final class OrikaAdvancedMapperUtils {
private static final MapperFactory MAPPER_FACTORY = new DefaultMapperFactory
.Builder()
.useAutoMapping(true)
.mapNulls(true)
.build();
/**
* 缓存 mapperFacade 实例
*/
private static final Map<String, MapperFacade> CACHE_MAPPER = new ConcurrentHashMap<>();
public static Map<String, MapperFacade> getCacheMapper() {
return CACHE_MAPPER;
}
/**
* 工具类私有方法,结合字段映射关系的缓存表,获取指定映射实体的 mapperFacade 实例,
*
* @param src 源实体
* @param dest 目标实体
* @param fieldMap 字段映射表
* @param <S> 源类
* @param <D> 目标类
* @return mapperFacade 实例
*/
private static synchronized <S, D> MapperFacade classMap(Class<S> src, Class<D> dest, Map<String, String> fieldMap) {
String key = src.getCanonicalName() + ":" + dest.getCanonicalName();
if (CACHE_MAPPER.containsKey(key)) {
return CACHE_MAPPER.get(key);
}
// 缓存字段映射表
return register(src, dest, fieldMap);
}
/**
* 注册实体映射,字段映射表可选
*
* @param src 源实体
* @param dest 目标实体
* @param fieldMap 字段映射表
* @param <S> 源类
* @param <D> 目标类
*/
public static synchronized <S, D> MapperFacade register(Class<S> src, Class<D> dest, Map<String, String> fieldMap) {
if (CollectionUtils.isEmpty(fieldMap)) {
MAPPER_FACTORY.classMap(src, dest).byDefault().register();
} else {
ClassMapBuilder<S, D> classMapBuilder = MAPPER_FACTORY.classMap(src, dest);
fieldMap.forEach(classMapBuilder::field);
classMapBuilder.byDefault().register();
}
String key = src.getCanonicalName() + ":" + dest.getCanonicalName();
MapperFacade mapperFacade = MAPPER_FACTORY.getMapperFacade();
CACHE_MAPPER.put(key, mapperFacade);
return mapperFacade;
}
/*************************** 工具方法部分 ***************************/
/**
* 字段名相同的实体映射
*
* @param src 源实体
* @param dest 目标实体
* @param <S> 源类
* @param <D> 目标类
*/
public static <S, D> void map(S src, D dest) {
if (src == null) {
return;
}
map(src, dest, null);
}
public static <S, D> D map(S src, Class<D> destClazz) {
if (src == null) {
return null;
}
return map(src, destClazz, null);
}
public static <S, D> List<D> mapAsList(List<S> src, Class<D> destClazz) {
if (src == null) {
return null;
}
return mapAsList(src, destClazz, null);
}
/**
* 携带字段映射关系的实体映射
*
* @param src 源实体
* @param dest 目标实体
* @param fieldMap 字段映射表
* @param <S> 源类
* @param <D> 目标类
*/
public static <S, D> void map(S src, D dest, Map<String, String> fieldMap) {
if (src == null) {
return;
}
classMap(src.getClass(), dest.getClass(), fieldMap).map(src, dest);
}
public static <S, D> D map(S src, Class<D> destClazz, Map<String, String> fieldMap) {
if (src == null) {
return null;
}
return classMap(src.getClass(), destClazz, fieldMap).map(src, destClazz);
}
public static <S, D> List<D> mapAsList(List<S> src, Class<D> destClazz, Map<String, String> fieldMap) {
if (src == null) {
return null;
}
return classMap(src.get(0).getClass(), destClazz, fieldMap).mapAsList(src, destClazz);
}
}
代码的功能都比较容易读懂,这里重点说明一下 synchronized <S, D> MapperFacade classMap(Class<S> src, Class<D> dest, Map<String, String> fieldMap)
方法。这里使用了 synchronized
关键字对方法加锁,配合 CACHE_MAPPER
的 ConcurrentHashMap
,保证在拷贝方法出现并发请求时,同样实体组合的映射关系在写入时的线程安全。构建 key 时调用的 getCanonicalName
则是获取了类包括其包路径的完成名称,以进行映射的两个类的路径名构建唯一标识,缓存其映射关系表。
classMap
方法的调用是需要调用 map
方法时同时传入其映射关系表 fieldMap
,这是一种被动的、懒惰式关系加载。工具类中还对外提供了一个 register
方法,可用于项目启动时主动加载不同实体组合的映射关联表。至于选取哪种方式进行缓存加载,则需要考虑服务的业务以及性能要求。
通过上述的这种本地缓存机制,拷贝操作在执行时就提升获取 mapperFacade
实例的效率,提升整体的性能。
绑定实体类的双向拷贝 BoundMapperFacade
上面我们提及到的映射和拷贝都是单项的,有时候在某些业务场景下,我们可能需要双向的实体映射操作,比如通过接口调用对数据库写入数据,以及从数据库读取数据进行返回,这里就需要实现注入 DTO -> Entity 以及 Entity -> DTO 的转化。此时如果使用上面描述的方法,则需要分别定义正反两个方向的映射关联表。如果使用 BoundMapperFacade
则只需要定义一次即可。
这里同样提供 BoundMapperFacade
的简单功能演示。在 getMapperFacade
接口中来源实体与目标实体的先后顺序不变,使用 BoundMapperFacade
执行拷贝操作时,只要调用相应的 Reverse 方法,即可实现方向的拷贝。同样地,拷贝时可以使用返回参数的形式,也可以使用参数内置的形式。
/**
* orika实体绑定映射
*/
public class OrikaBoundMapperTest {
@Test
void testBasicMap() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
BoundMapperFacade<MemberInfoDTO, MemberInfoVO> facade =
mapperFactory.getMapperFacade(MemberInfoDTO.class, MemberInfoVO.class);
MemberInfoDTO dto = initMemberInfo();
MemberInfoVO vo = facade.map(dto);
// facade.map(dto, vo);
assertMemberInfoEquals(dto, vo);
}
@Test
void testBasicReverseMap() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
BoundMapperFacade<MemberInfoVO, MemberInfoDTO> facade =
mapperFactory.getMapperFacade(MemberInfoVO.class, MemberInfoDTO.class);
MemberInfoDTO dto = initMemberInfo();
MemberInfoVO vo = facade.mapReverse(dto);
// facade.mapReverse(dto, vo);
assertMemberInfoEquals(dto, vo);
}
private MemberInfoDTO initMemberInfo() {
MemberInfoDTO member = new MemberInfoDTO();
member.setId(1000100010001L);
member.setName("Gogo");
member.setAge(14);
member.setEmail("gogo@oo.com");
member.setIdentity("employee");
member.setGroup("marketing");
return member;
}
private void assertMemberInfoEquals(MemberInfoDTO dto, MemberInfoVO vo) {
assertEquals(dto.getId(), vo.getId());
assertEquals(dto.getName(), vo.getName());
assertEquals(dto.getAge(), vo.getAge());
assertEquals(dto.getGroup(), vo.getGroup());
assertEquals(dto.getEmail(), vo.getEmail());
assertEquals(dto.getIdentity(), vo.getIdentity());
}
@Test
void testMapDiffFieldName() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 使用注解显式获取变量名,需要指定所有的字段映射关系,剩余通过 byDefault() 指定
mapperFactory.classMap(PersonSrcInfo.class, PersonDesInfo.class)
.field(PersonSrcInfo.Fields.name, PersonDesInfo.Fields.personName)
.field(PersonSrcInfo.Fields.birth, PersonDesInfo.Fields.dateOfBirth)
.field(PersonSrcInfo.Fields.email, PersonDesInfo.Fields.emailAddress)
.byDefault()
.register();
BoundMapperFacade<PersonSrcInfo, PersonDesInfo> facade =
mapperFactory.getMapperFacade(PersonSrcInfo.class, PersonDesInfo.class);
PersonSrcInfo src = initPersonInfo();
PersonDesInfo dest = facade.map(src);
assertPersonInfoEquals(src, dest);
}
@Test
void testReverseMapDiffFieldName() {
DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 使用注解显式获取变量名,需要指定所有的字段映射关系,剩余通过 byDefault() 指定
mapperFactory.classMap(PersonSrcInfo.class, PersonDesInfo.class)
.field(PersonSrcInfo.Fields.name, PersonDesInfo.Fields.personName)
.field(PersonSrcInfo.Fields.birth, PersonDesInfo.Fields.dateOfBirth)
.field(PersonSrcInfo.Fields.email, PersonDesInfo.Fields.emailAddress)
.byDefault()
.register();
BoundMapperFacade<PersonDesInfo, PersonSrcInfo> facade =
mapperFactory.getMapperFacade(PersonDesInfo.class, PersonSrcInfo.class);
PersonSrcInfo src = initPersonInfo();
PersonDesInfo dest = facade.mapReverse(src);
assertPersonInfoEquals(src, dest);
}
private PersonSrcInfo initPersonInfo() {
PersonSrcInfo person = new PersonSrcInfo();
person.setId(1000100010001L);
person.setName("Gogo");
person.setAge(14);
person.setBirth(LocalDate.of(2004, 6, 19));
person.setEmail("gogo@oo.com");
return person;
}
private void assertPersonInfoEquals(PersonSrcInfo src, PersonDesInfo dest) {
assertEquals(src.getId(), dest.getId());
assertEquals(src.getName(), dest.getPersonName());
assertEquals(src.getAge(), dest.getAge());
assertEquals(src.getEmail(), dest.getEmailAddress());
assertEquals(src.getBirth(), dest.getDateOfBirth());
}
}
性能提升
网上很多文章在比较不同的 Java 拷贝工具时,很多会说 Orika 性能比较好的原因是它在拷贝过程中没有用到内置的反射。这个表达不太准确,严格来说 Orika 在使用 map 方法时,如果传入的是 class 类,内部依然会通过反射机制创建出对应的目标对象,只不过在拷贝成员变量时没有使用反射而已。相比于其他工具全程使用反射进行实例化以及赋值操作,Orika 在赋值时没有使用到反射,具有更优的性能开销。
同时,在其内部实现中也通过不同的策略,减少了映射的开销。其中一部分策略已经在上面的演示中提及到,这里再简单做个整理。
-
将
MapperFactory
用作单例Orika 中性能开销最大的部分之一就是
MapperFactory
的实例化和初始化,以及从中获得的MapperFacade
。由于它们都是线程安全对象,可以作为单例在代码中安全地共享。具体可参考上面 进阶的通用工具类实现 一节。 -
使用
BoundMapperFacade
以避免重复查找映射策略为给定的一对类型构建一个
BoundMapperFacade<A,B>
来避免为输入集查找适当映射策略的开销,同时还能避免在递归映射调用中进行额外的查找。 -
当对象关系不存在循环时使用自定义
BoundMapperFacade
映射过程中最昂贵的部分之一是在给定的映射上下文
MappingContext
中对已映射过的对象进行哈希查找。如果知道对象关系中不存在循环,就可以避免这种查找,从而大大加快映射过程。通过使用MapperFactory
上的getMapperFacade(typeA,TypeB,false)
方法定义一个无循环的实体映射实例:BoundMapperFacade<Person,PersonDto> mapper = mapperFactory.getMapperFacade(PersonSrcInfo.class, PersonDesInfo.class, false); public PersonDesInfo convert(PersonSrcInfo person) { return mapper.map(person); }