BeanCopy问题
我们复制对象最常用的方法是使用 BeanCopy 工具类,这是一种常见的 DTO 对象复制方法。然而,BeanCopy 在处理复杂继承和嵌套类型时常常出现问题,导致开发人员需要花费大量时间来手动处理 DTO 对象之间的映射关系。
- 无法处理继承关系:Bean Copy 不能正确地处理继承关系,如果源对象和目标对象之间存在继承关系,Bean Copy 可能会复制不正确的属性或出现运行时错误。
- 递归复制问题:Bean Copy 可能会导致递归复制的问题,例如A对象中包含B对象,而B对象中又包含A对象的引用,这种情况下,Bean Copy 可能会导致无限递归的问题。
- 不支持复杂类型:Bean Copy 通常只能复制简单类型的 Bean ,如果需要复制的 Bean 中包含嵌套的复杂类型,需要实现自定义的转换处理。
- 性能问题:Bean Copy 是基于反射实现的,因此在复制大量对象时可能会存在性能问题,影响系统的响应速度和性能。
- 对象赋值黑盒,当业务变的复杂,对象层层转换,很难找到属性是在哪里赋值,后期扩展及排查问题埋下隐患。
举个例子:
假设您有一个源对象 User 和一个目标对象 UserDTO ,它们的属性如下所示:
代码语言:javascript复制public class User {
private Long id;
private String givenName;
private String email;
private List<Role> roles;
// constructor, getters and setters
}
public class UserDto {
private Long id;
private String firstName;
private String email;
private List<Role> roles;
// constructor, getters and setters
}
如果要将一个 User 对象拷贝到另一个 UserDTO 对象中,可以使用 Bean Copy 的方式,如下:
代码语言:javascript复制User sourceUser = new User();
sourceUser.setId(1L);
sourceUser.setGivenName("John");
sourceUser.setEmail("johndoe@example.com");
sourceUser.setRoles(roles);
UserDTO targetUserDTO = new UserDTO();
BeanUtils.copyProperties(sourceUser, targetUserDTO);
System.out.println(targetUserDTO.getFirstName()); // Output: null
但是,使用 Bean Copy 的方式会造成以下问题:
- 属性名不同无法赋值,因为源对象的 givenName 属性与目标对象的 firstName 属性名不同,需要编写自定义的映射逻辑。
- 浅拷贝:使用 Bean Copy 的方式,目标对象拷贝的是源对象的地址。如果 User 对象的 role 改动,UserDTO 对象中的 role 也回会随着一起改变
使用MapStruct改进
MapStruct 是一个代码生成器,它可以自动生成映射器代码,可以解决 BeanCopy 存在的问题。接下来,我们来看看如何使用 MapStruct 来解决这个问题。需要先定义一个 Mapper 接口,代码如下:
代码语言:javascript复制@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
@Mapping(target = "firstName", source = "givenName")
UserDTO toDto(User user);
List<UserDTO> toDtoList(List<User> userList);
}
在上面的例子中,使用注解 @Mapper 定义了一个接口 UserConveter,该接口编译时会由 MapStruct 动态生成实现类,使用该实现类进行 Java Bean 对象的拷贝。并且使用 @Mapping 注解指定了 User 对象的 givenName 和 UserDTO 对象 firstName 的映射关系。
使用 MapStruct 进行 User 对象拷贝:
代码语言:javascript复制UserDTO dto = UserConverter.INSTANCE.toDto(user);
MapStruct一些基本用法
使用 MapStruct 的主要优点如下:
- 简化配置:使用 MapStruct 可以简化 Java Bean 对象拷贝的配置,避免了使用 Bean Copy 时出现的配置问题;
- 提高效率:MapStruct 在编译时会自动将 Java Bean 对象拷贝的代码进行优化,提高了拷贝的效率;
- 支持复杂类型:MapStruct 支持复杂 Java Bean 对象之间的属性拷贝,包括集合、继承、多态等;
- 易于维护:使用 MapStruct 可以使代码更加清晰和易于维护。
下面列一下 MapStruct 的常见的一些用法
- 使用 @Mapper 注解时,添加 componentModel = "spring" 参数可以指定使用 Spring 作为注入依赖的框架,这样就可以在我们业务 Service 中用 @Autowired 注解引入当作 Bean 来使用。
@Mapper(componentModel = "spring")
public interface UserConverter {
// ...
}
使用
@Autowired
private UserConverter userConverter;
// 转化
UserDTO userDTO = userConverter.toDto(user);
- 可以支持自定义字段映射,只需要在方法签名上,使用 @Mapping 注解,并指明需要转换的源对象的名字和目标对象的名字就可以了,并且支持多层级对象。 使用 @InheritInverseConfiguration 注解来自动生成反向映射方法,避免手动编写反向映射方法。
public interface EmployeeMapper {
@Mapping(target = "name", source = "person.name")
@Mapping(target = "age", source = "person.age")
@Mapping(target = "employeeId", source = "employeeId")
EmployeeDTO employeeToEmployeeDTO(Employee employee);
@InheritInverseConfiguration
Employee employeeDTOToEmployee(EmployeeDTO employeeDTO);
}
- 使用 @Mapping 注解时,可以添加 expression 参数来指定自定义映射逻辑,例如计算、格式化等。
public interface EmployeeMapper {
@Mapping(target = "age", expression = "java(LocalDate.now().getYear() - employee.getBirthDate().getYear())")
EmployeeDTO employeeToEmployeeDTO(Employee employee);
}
- @AfterMapping 注解可以被应用于映射的方法,这样在调用映射方法之后,自动执行标注了 @AfterMapping注解的方法。
将 EpPlanApply 对象中逗号分隔的字符串 auditUserIds 转化为 PlanApply 中的 Set auditUserIds:
代码语言:javascript复制PlanApply poToDomain(EpPlanApply planApply);
@AfterMapping
default void afterPoToDomain(@MappingTarget PlanApply planApply, EpPlanApply epPlanApply) {
String auditUserIdsStr = epPlanApply.getAuditUserIds();
if (StringUtils.isNotBlank(auditUserIdsStr)) {
Set<Long> auditUserIds = new HashSet<>();
for (String split : auditUserIdsStr.split(",")) {
auditUserIds.add(Long.valueOf(split));
}
planApply.setAuditUserIds(auditUserIds);
}
}
总结
Bean Copy 和 MapStruct 都是 Java 中用于对象之间复制属性的工具,它们可以大大简化代码的编写和维护。
MapStruct 的性能更高,因为它在编译时生成代码,而 Bean Copy 需要使用反射机制获取对象的属性信息。但是MapStruct 的学习成本相对较高,因为需要了解注解的使用方法以及如何配置和生成代码。
BeanCopy 适用于简单的属性复制场景,而 MapStruct 则适用于复杂的属性复制场景,尤其是在需要频繁进行属性复制的情况下,使用 MapStruct 可以提高代码的执行效率。选择使用哪种工具取决于具体的业务需求和开发团队的技术水平。