背景
在现代 Java 应用程序中,对象映射是一个常见的需求,尤其是在分层架构中,不同层之间的对象往往需要相互转换。为了简化对象映射,开发者可以使用多种技术方案。博客优雅处理对象映射:MapStruct 实战指南中详细讲述了 MapStruct
的优点和用法。本篇博客将介绍其他对象映射方式,包括 BeanUtils
浅拷贝、Orika
和 ModelMapper
,并对比这些方案的优缺点,探讨如何选择适合的技术方案。
对象映射方法
BeanUtils
原理
BeanUtils.copyProperties
是 Apache Commons BeanUtils 库提供的一个工具方法,基于 Java 反射 实现。它通过反射获取源对象和目标对象的属性,并将源对象的属性值复制到目标对象中。
工作流程
- 通过反射获取源对象的所有属性。
- 通过反射获取目标对象的所有属性。
- 将源对象的属性值复制到目标对象的对应属性中。
优点
- 只需一行代码即可完成对象属性的复制。不需要定义映射规则或编写额外的代码。
- 无需引入其他依赖。
缺点
- 性能较差:基于反射实现,运行时性能开销较大。
- 类型安全不足:如果属性类型不匹配,可能会在运行时抛出异常。
- 灵活性低:无法处理嵌套对象、集合或自定义转换逻辑。
- 字段名不一致问题:当源对象和目标对象字段名不同时,映射后可能会丢失部分信息。
实战
对象定义
在本示例中,我们使用用户这一业务实体作为例子,分别定义领域模型 User
和数据库实体 UserEntity
。其中,hobbies
字段需要在字符串和数组之间进行转换,LocalDateTime
字段也需要正确映射。
public class User {
private String name;
private String nickName;
private String email;
private String phoneNumber;
private String address;
private List<String> hobbies;
private LocalDateTime registerTime;
private Boolean isDeleted;
}
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String nickName;
private String email;
private String phoneNumber;
private String address;
private String hobbies;
private LocalDateTime registerTime;
private Boolean isDeleted;
}
映射方法
public class BeanMapper {
public UserEntity toEntity(User domain) {
UserEntity entity = new UserEntity();
BeanUtils.copyProperties(domain, entity);
String hobbiesString = String.join(",", domain.getHobbies());
entity.setHobbies(hobbiesString);
return entity;
}
public static User toDomain(UserEntity entity) {
User domain = new User();
BeanUtils.copyProperties(entity, domain);
List<String> hobbies = Arrays.stream(entity.getHobbies().split(",")).collect(Collectors.toList());
domain.setHobbies(hobbies);
return domain;
}
}
在映射方法中,hobbies
字段需要手动处理,将 List<String>
转换为逗号分隔的字符串,或者将字符串拆分为 List<String>
。这种方式虽然简单,但违反了封装原则,属于经典的反模式。
测试
首先我们插入一条测试数据,用于测试entity到domain的映射:
INSERT INTO users (name, nick_name, email, phone_number, address, hobbies, register_time, is_deleted) VALUES
('Alice', 'testUser', '123@gmail.com', '13112341234', 'test', 'read,travel,movie', '2025-02-26 22:40:23', 0)
紧接着我们在测试中使用BeanMapper定义的toDomain方法:
@Test
void should_get_domain_by_bean_mapper() {
User user = service.getUserByBeanMapper();
assertEquals("Alice", user.getName());
assertEquals("testUser", user.getNickName());
assertEquals("13112341234", user.getPhoneNumber());
assertEquals("123@gmail.com", user.getEmail());
assertEquals(3, user.getHobbies().size());
assertEquals("read", user.getHobbies().get(0));
assertEquals("travel", user.getHobbies().get(1));
assertEquals("movie", user.getHobbies().get(2));
assertNotNull(user.getRegisterTime());
}
MapStruct
原理
MapStruct 是一个基于 注解处理器 的 Java 对象映射工具。它在编译时生成映射代码,生成的代码是纯 Java 代码,没有运行时反射开销。
工作流程
- 开发者定义 Mapper 接口,并使用
@Mapper
和@Mapping
注解指定映射规则。 - 在编译时,MapStruct 的注解处理器会解析这些注解,并生成对应的映射实现类。
- 生成的实现类直接调用目标对象的 setter 方法和源对象的 getter 方法,完成属性复制。
实战
关于 MapStruct 的详细用法,请参考博客:优雅处理对象映射:MapStruct 实战指南。https://lemonpie.asia/archives/167)
Orika
原理
Orika 是一个基于 字节码生成 的 Java 对象映射工具。它在运行时动态生成字节码,并通过生成的字节码完成对象映射。
工作流程
- 开发者通过
MapperFactory
注册映射规则。 - 在运行时,Orika 使用字节码生成技术动态生成映射类的字节码。
- 生成的字节码直接操作对象的属性,完成映射。
优点
-
基于字节码生成,性能接近手动实现。
-
支持嵌套对象、集合和自定义转换逻辑。
缺点
-
需要熟悉 Orika 的配置和 API。
-
需要在运行时生成字节码,可能增加启动时间。
-
对于需要自定义映射方法的字段,需要自定义Converter实现。在业务场景复杂的情况下会造成Converter类过多且冗余的状况。
实战
引入依赖
implementation 'ma.glasnost.orika:orika-core:1.5.4'
对象定义
同上
映射方法
public class OrikaMapper {
private static final MapperFacade ORIKA_MAPPER;
static {
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 注册自定义转换器(用于 hobbies 字段)
mapperFactory.getConverterFactory().registerConverter(new HobbyToDomainConverter());
mapperFactory.getConverterFactory().registerConverter(new HobbyToEntityConverter());
// 注册 LocalDateTime 的 PassThroughConverter(避免 Orika 默认不支持 LocalDateTime 的问题)
mapperFactory.getConverterFactory().registerConverter(new PassThroughConverter(LocalDateTime.class));
ORIKA_MAPPER = mapperFactory.getMapperFacade();
}
public static UserEntity toEntity(User user) {
return ORIKA_MAPPER.map(user, UserEntity.class);
}
public static User toDomain(UserEntity userEntity) {
return ORIKA_MAPPER.map(userEntity, User.class);
}
}
考虑到这个场景中存在日期格式的字段转换,所以需要注册PassThroughConverter
转换器。同时定义字符串类型hobbies与数组类型hobbies字段之间转换的转换器,如下:
public class HobbyToDomainConverter extends CustomConverter<String, List<String>> {
@Override
public List<String> convert(String hobbyString, Type<? extends List<String>> type, MappingContext mappingContext) {
return Arrays.stream(hobbyString.split(",")).collect(Collectors.toList());
}
}
public class HobbyToEntityConverter extends CustomConverter<List<String>, String> {
@Override
public String convert(List<String> hobbies, Type<? extends String> type, MappingContext mappingContext) {
return hobbies != null ? String.join(",", hobbies) : null;
}
}
- Orika使用
MapperFactory
来配置和创建映射器 MapperFacade
是一个核心接口,提供了对象映射的功能- 当需要自定义映射时需要实现
CustomConverter
并将转换器注册到映射器中
测试
测试用数据同上,用于测试将数据库实体映射为领域对象。
@Test
void should_get_domain_by_orika_mapper() {
User user = service.getUserByOrika();
assertEquals("Alice", user.getName());
assertEquals("testUser", user.getNickName());
assertEquals("13112341234", user.getPhoneNumber());
assertEquals("123@gmail.com", user.getEmail());
assertEquals(3, user.getHobbies().size());
assertEquals("read", user.getHobbies().get(0));
assertEquals("travel", user.getHobbies().get(1));
assertEquals("movie", user.getHobbies().get(2));
assertNotNull(user.getRegisterTime());
}
多态场景
Orika 支持多态映射,可以通过 MapperFactory
的 classMap
方法实现:
mapperFactory.classMap(Source.class, Target.class)
.field("sourceField", "targetField")
.register();
ModelMapper
原理
ModelMapper 是一个利用反射原理
的 Java 对象映射工具。它通过反射和智能匹配算法,自动推断源对象和目标对象之间的映射关系。ModelMapper工作时首先在匹配过程中分析源对象和目标对象,根据匹配策略将属性相互匹配。接着在映射过程中根据匹配进行数据映射,将源对象的属性值转化为目标对象的对应字段。
工作流程:
- 开发者创建
ModelMapper
实例。 - ModelMapper 使用反射获取源对象和目标对象的属性。
- 通过智能匹配算法推断属性之间的映射关系。
- 根据推断结果完成属性复制。
优点
-
基于约定,无需显式配置映射规则。
-
支持嵌套对象和集合。
缺点
-
基于反射实现,性能不如 MapStruct 和 Orika。
-
对于复杂的映射场景,可能需要额外配置。
实战
引入依赖
implementation 'org.modelmapper:modelmapper:2.4.4'
对象定义
同上
映射方法
public class ModelMapperUtil {
private static final ModelMapper MODEL_MAPPER;
static {
// 初始化 ModelMapper
MODEL_MAPPER = new ModelMapper();
// 配置 ModelMapper(可选)
MODEL_MAPPER.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
// 注册自定义转换器(用于 hobbies 字段)
MODEL_MAPPER.createTypeMap(User.class, UserEntity.class)
.addMappings(mapper -> {
mapper.using(ctx -> String.join(",", ((List<String>) ctx.getSource())))
.map(User::getHobbies, UserEntity::setHobbies);
});
MODEL_MAPPER.createTypeMap(UserEntity.class, User.class)
.addMappings(mapper -> {
mapper.using(ctx -> List.of(((String) ctx.getSource()).split(",")))
.map(UserEntity::getHobbies, User::setHobbies);
});
}
public static UserEntity toEntity(User user) {
return MODEL_MAPPER.map(user, UserEntity.class);
}
public static User toDomain(UserEntity userEntity) {
return MODEL_MAPPER.map(userEntity, User.class);
}
}
- 使用
modelMapper.createTypeMap()
方法为User
和UserEntity
创建类型映射。 - 通过
addMappings()
方法为hobbies
字段注册自定义映射逻辑:User
到UserEntity
:将List<String>
转换为逗号分隔的字符串。UserEntity
到User
:将逗号分隔的字符串拆分为List<String>
。
测试
测试用数据同上,用于测试将数据库实体映射为领域对象。
@Test
void should_get_domain_by_model_mapper() {
User user = service.getUserByModelMapper();
assertEquals("Alice", user.getName());
assertEquals("testUser", user.getNickName());
assertEquals("13112341234", user.getPhoneNumber());
assertEquals("123@gmail.com", user.getEmail());
assertEquals(3, user.getHobbies().size());
assertEquals("read", user.getHobbies().get(0));
assertEquals("travel", user.getHobbies().get(1));
assertEquals("movie", user.getHobbies().get(2));
assertNotNull(user.getRegisterTime());
}
多态场景
ModelMapper 本身对多态映射的支持较弱,但可以通过自定义转换器实现。通过实现转换器Converter,并在转换器中根据if-else
或switch
判断多态类型进行转换。如果按照Animal示例进行演示,则是:
public class AnimalConverter implements Converter<Animal, AnimalDTO> {
@Override
public AnimalDTO convert(MappingContext<Animal, AnimalDTO> context) {
Animal source = context.getSource();
if (source instanceof Dog) {
DogDTO dogDTO = new DogDTO();
dogDTO.setName(source.getName());
dogDTO.setSound(((Dog) source).getBarkSound());
return dogDTO;
} else if (source instanceof Cat) {
CatDTO catDTO = new CatDTO();
catDTO.setName(source.getName());
catDTO.setSound(((Cat) source).getMeowSound());
return catDTO;
}
return null;
}
}
总结
在上述示例中,我们详细介绍了多种 Java 对象映射工具的用法。这些工具各有优缺点,具体使用需根据场景选择:
- 如果场景简单且性能要求不高,可以使用
BeanUtils
,但需要手动处理特殊字段。 - 如果需要高性能和复杂映射,亦或者是多态场景下的映射,推荐使用
MapStruct
或Orika
,它们都支持自定义字段映射逻辑。 - 如果更注重易用性和快速开发,可以选择
ModelMapper
,但需要注意其性能较低。
留言