在Java项目开发中,随着业务模块拆分越来越细,不同模块可能会定义结构相似但包路径不同的同名DTO,当需要把其中一个包下的DTO列表转换为另一个包下的同名DTO列表时,就需要处理类型转换与映射的问题。这种场景在微服务间数据传递、模块间解耦交互中非常常见,处理不当会导致大量重复代码或者运行时类型转换错误。

场景说明
假设我们有两个不同包下的同名DTO,结构完全一致,只是包路径不同:
包com.example.module_a.dto下的UserDTO:
public class UserDTO {
private Long id;
private String name;
private Integer age;
// 省略getter、setter方法
}
包com.example.module_b.dto下的UserDTO:
public class UserDTO {
private Long id;
private String name;
private Integer age;
// 省略getter、setter方法
}
现在我们需要将com.example.module_a.dto.UserDTO的列表转换为com.example.module_b.dto.UserDTO的列表,下面介绍几种实现方式。
方案一:手动字段映射
手动映射是最基础的方式,逐个获取源对象的字段值,设置到目标对象中,适合字段数量少、转换逻辑简单的场景。
import com.example.module_a.dto.UserDTO as ModuleAUserDTO;
import com.example.module_b.dto.UserDTO as ModuleBUserDTO;
import java.util.ArrayList;
import java.util.List;
public class DTOConverter {
public static List<ModuleBUserDTO> convertListManual(List<ModuleAUserDTO> sourceList) {
if (sourceList == null) {
return null;
}
List<ModuleBUserDTO> targetList = new ArrayList<>();
for (ModuleAUserDTO source : sourceList) {
ModuleBUserDTO target = new ModuleBUserDTO();
target.setId(source.getId());
target.setName(source.getName());
target.setAge(source.getAge());
targetList.add(target);
}
return targetList;
}
}
这种方式的优点是逻辑清晰,没有额外依赖,缺点是当字段数量多或者需要转换的DTO类型多时,代码冗余度高,后期如果DTO字段变更,需要同步修改所有转换代码,维护成本高。
方案二:使用Spring BeanUtils批量转换
Spring框架提供的BeanUtils工具类可以自动复制同名同类型的字段,适合字段名和类型完全一致的场景,不需要手动逐个赋值。
import com.example.module_a.dto.UserDTO as ModuleAUserDTO;
import com.example.module_b.dto.UserDTO as ModuleBUserDTO;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
public class DTOConverter {
public static List<ModuleBUserDTO> convertListByBeanUtils(List<ModuleAUserDTO> sourceList) {
if (sourceList == null) {
return null;
}
List<ModuleBUserDTO> targetList = new ArrayList<>();
for (ModuleAUserDTO source : sourceList) {
ModuleBUserDTO target = new ModuleBUserDTO();
BeanUtils.copyProperties(source, target);
targetList.add(target);
}
return targetList;
}
}
这种方式的优点是代码简洁,减少重复劳动,缺点是无法处理字段名不同、类型不同或者需要自定义转换逻辑的场景,而且反射调用会有一定的性能损耗,不过在普通业务场景下损耗可以忽略。
方案三:使用MapStruct框架映射
MapStruct是一个基于注解的代码生成器,在编译期生成类型转换的实现代码,性能接近手动映射,同时支持自定义转换逻辑,适合复杂转换场景。
首先添加Maven依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
然后定义转换接口:
import com.example.module_a.dto.UserDTO as ModuleAUserDTO;
import com.example.module_b.dto.UserDTO as ModuleBUserDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface UserDTOMapper {
UserDTOMapper INSTANCE = Mappers.getMapper(UserDTOMapper.class);
ModuleBUserDTO toModuleBUserDTO(ModuleAUserDTO source);
List<ModuleBUserDTO> toModuleBUserDTOList(List<ModuleAUserDTO> sourceList);
}
使用时直接调用接口方法即可:
import com.example.module_a.dto.UserDTO as ModuleAUserDTO;
import com.example.module_b.dto.UserDTO as ModuleBUserDTO;
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<ModuleAUserDTO> sourceList = Arrays.asList(
new ModuleAUserDTO(1L, "张三", 20),
new ModuleAUserDTO(2L, "李四", 22)
);
List<ModuleBUserDTO> targetList = UserDTOMapper.INSTANCE.toModuleBUserDTOList(sourceList);
System.out.println(targetList);
}
}
MapStruct的优点是编译期生成代码,性能高,支持自定义字段映射、类型转换,适合中大型项目的复杂转换需求,缺点是需要额外引入依赖,学习成本比前两种方案稍高。
方案对比与选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动映射 | 无依赖、逻辑清晰 | 代码冗余、维护成本高 | 字段少、转换逻辑简单的临时场景 |
| Spring BeanUtils | 代码简洁、无复杂依赖 | 不支持自定义转换、反射有性能损耗 | 字段完全一致、无自定义逻辑的普通业务场景 |
| MapStruct | 性能高、支持自定义逻辑、维护方便 | 需要引入额外依赖 | 中大型项目、复杂转换场景、多类型转换需求 |
在实际开发中,我们可以根据项目规模、转换复杂度选择合适的方案,如果项目已经引入了Spring框架,优先选择Spring BeanUtils处理简单场景;如果是长期维护的中大型项目,推荐使用MapStruct来统一处理所有DTO转换需求,提升代码的可维护性。