在日常的开发过程中,经常遇到对象转换的场景,那么不同方式的对象转换,有什么样的区别呢?
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
手动转换 | 灵活度高,完全可控 | 代码冗余,维护成本高 | 少量字段或复杂业务逻辑 |
BeanUtils | 简单易用,无需手写代码 | 性能较差,反射机制效率低 | 快速原型开发 |
ModelMapper | 支持复杂映射,配置灵活 | 学习成本高,性能一般 | 中等复杂度的对象转换 |
MapStruct | 性能最优,编译期生成代码 | 需学习特定注解 | 大规模对象转换 |
在这篇文章中,主要推荐MapStruct,针对简单、复杂场景使用都很方便。
1. 简单对象映射
当源对象和目标对象的字段名称和类型完全一致时,MapStruct 可以自动完成映射。
java
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
// 源对象
@Data
class Source {
private String name;
private int age;
}
// 目标对象
@Data
class Target {
private String name;
private int age;
}
// MapStruct 映射接口
@Mapper
public interface SimpleMapper {
SimpleMapper INSTANCE = Mappers.getMapper(SimpleMapper.class);
Target sourceToTarget(Source source);
}
// 测试代码
public class SimpleMappingExample {
public static void main(String[] args) {
Source source = new Source();
source.setName("John");
source.setAge(30);
Target target = SimpleMapper.INSTANCE.sourceToTarget(source);
System.out.println("Name: " + target.getName() + ", Age: " + target.getAge());
}
}
@Mapper
注解表明这是一个 MapStruct 映射接口。SimpleMapper.INSTANCE
是获取映射器实例的方式。sourceToTarget
方法将Source
对象映射到Target
对象。
2. 字段名称不同的映射
当源对象和目标对象的部分字段名称不同时,需要使用 @Mapping
注解指定映射关系。
java
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
// 源对象
@Data
class SourceWithDifferentName {
private String fullName;
private int yearsOld;
}
// 目标对象
@Data
class TargetWithDifferentName {
private String name;
private int age;
}
// MapStruct 映射接口
@Mapper
public interface DifferentNameMapper {
DifferentNameMapper INSTANCE = Mappers.getMapper(DifferentNameMapper.class);
@Mapping(source = "fullName", target = "name")
@Mapping(source = "yearsOld", target = "age")
TargetWithDifferentName sourceToTarget(SourceWithDifferentName source);
}
// 测试代码
public class DifferentNameMappingExample {
public static void main(String[] args) {
SourceWithDifferentName source = new SourceWithDifferentName();
source.setFullName("Alice");
source.setYearsOld(25);
TargetWithDifferentName target = DifferentNameMapper.INSTANCE.sourceToTarget(source);
System.out.println("Name: " + target.getName() + ", Age: " + target.getAge());
}
}
@Mapping
注解用于指定源字段和目标字段的映射关系。source
属性指定源对象的字段名,target
属性指定目标对象的字段名。
3. 类型转换的映射
当源对象和目标对象的字段类型不同时,需要自定义类型转换方法。
java
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.time.LocalDate;
import java.util.Date;
// 源对象
@Data
class SourceWithTypeConversion {
private Date birthDate;
}
// 目标对象
@Data
class TargetWithTypeConversion {
private LocalDate birthLocalDate;
}
// MapStruct 映射接口
@Mapper
public interface TypeConversionMapper {
TypeConversionMapper INSTANCE = Mappers.getMapper(TypeConversionMapper.class);
@Mapping(source = "birthDate", target = "birthLocalDate", qualifiedByName = "dateToLocalDate")
TargetWithTypeConversion sourceToTarget(SourceWithTypeConversion source);
default LocalDate dateToLocalDate(Date date) {
return date != null ? date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate() : null;
}
}
// 测试代码
public class TypeConversionMappingExample {
public static void main(String[] args) {
SourceWithTypeConversion source = new SourceWithTypeConversion();
source.setBirthDate(new Date());
TargetWithTypeConversion target = TypeConversionMapper.INSTANCE.sourceToTarget(source);
System.out.println("Birth Local Date: " + target.getBirthLocalDate());
}
}
@Mapping
注解的qualifiedByName
属性指定了自定义类型转换方法的名称。dateToLocalDate
方法用于将Date
类型转换为LocalDate
类型。
4. 集合映射
当需要将源对象的集合映射到目标对象的集合时,MapStruct 可以自动处理。
java
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
// 源对象
@Data
class SourceForCollection {
private String name;
}
// 目标对象
@Data
class TargetForCollection {
private String name;
}
// MapStruct 映射接口
@Mapper
public interface CollectionMapper {
CollectionMapper INSTANCE = Mappers.getMapper(CollectionMapper.class);
TargetForCollection sourceToTarget(SourceForCollection source);
List<TargetForCollection> sourcesToTargets(List<SourceForCollection> sources);
}
// 测试代码
import java.util.Arrays;
import java.util.List;
public class CollectionMappingExample {
public static void main(String[] args) {
SourceForCollection source1 = new SourceForCollection();
source1.setName("Bob");
SourceForCollection source2 = new SourceForCollection();
source2.setName("Charlie");
List<SourceForCollection> sources = Arrays.asList(source1, source2);
List<TargetForCollection> targets = CollectionMapper.INSTANCE.sourcesToTargets(sources);
for (TargetForCollection target : targets) {
System.out.println("Name: " + target.getName());
}
}
}
- 定义了单个对象的映射方法
sourceToTarget
。 sourcesToTargets
方法用于将SourceForCollection
列表映射到TargetForCollection
列表。
5. 嵌套对象映射
当源对象和目标对象包含嵌套对象时,需要处理嵌套对象的映射。
java
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
// 源嵌套对象
@Data
class SourceNested {
private String name;
private SourceAddress address;
}
@Data
class SourceAddress {
private String street;
}
// 目标嵌套对象
@Data
class TargetNested {
private String name;
private TargetAddress address;
}
@Data
class TargetAddress {
private String street;
}
// MapStruct 映射接口
@Mapper
public interface NestedObjectMapper {
NestedObjectMapper INSTANCE = Mappers.getMapper(NestedObjectMapper.class);
@Mapping(source = "address.street", target = "address.street")
TargetNested sourceToTarget(SourceNested source);
}
// 测试代码
public class NestedObjectMappingExample {
public static void main(String[] args) {
SourceAddress sourceAddress = new SourceAddress();
sourceAddress.setStreet("123 Main St");
SourceNested source = new SourceNested();
source.setName("David");
source.setAddress(sourceAddress);
TargetNested target = NestedObjectMapper.INSTANCE.sourceToTarget(source);
System.out.println("Name: " + target.getName() + ", Street: " + target.getAddress().getStreet());
}
}
@Mapping
注解用于指定嵌套对象的字段映射关系。- 通过
address.street
形式指定嵌套字段的映射。
6. 映射忽略字段
当某些字段不需要进行映射时,可以使用 @Mapping
注解的 ignore
属性忽略这些字段。 java
java
import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
// 源对象
@Data
class SourceWithIgnoredField {
private String name;
private int age;
private String secret;
}
// 目标对象
@Data
class TargetWithIgnoredField {
private String name;
private int age;
}
// MapStruct 映射接口
@Mapper
public interface IgnoredFieldMapper {
IgnoredFieldMapper INSTANCE = Mappers.getMapper(IgnoredFieldMapper.class);
@Mapping(target = "secret", ignore = true)
TargetWithIgnoredField sourceToTarget(SourceWithIgnoredField source);
}
// 测试代码
public class IgnoredFieldMappingExample {
public static void main(String[] args) {
SourceWithIgnoredField source = new SourceWithIgnoredField();
source.setName("Eve");
source.setAge(22);
source.setSecret("TopSecret");
TargetWithIgnoredField target = IgnoredFieldMapper.INSTANCE.sourceToTarget(source);
System.out.println("Name: " + target.getName() + ", Age: " + target.getAge());
}
}
@Mapping
注解的ignore
属性设置为true
时,该字段将不会被映射。
lombok
- 核心优势
- 代码简洁 :通过
@Data
减少 70% 样板代码 - 统一规范:自动生成标准 getter/setter 方法
- 功能扩展 :支持
@Builder
、@Slf4j
等实用注解
- 潜在问题
- 可读性下降:隐藏代码实现细节
- 反射问题:某些框架依赖反射获取字段信息
- 工具兼容性:与代码生成工具(如 MapStruct)存在冲突
- 调试困难:IDE 调试时可能无法直接定位生成代码
- 最佳实践
- 优先使用
@Data
替代手动编写 getter/setter - 复杂对象建议配合
@Builder
使用 - 避免在关键业务逻辑中过度依赖 Lombok
当lombok遇到mapstruct
核心冲突场景:当 Lombok 的@Data
与 MapStruct 联用时,可能出现以下错误:
java
[ERROR] No property named "id" exists in source parameter(s). Did you mean "null"?
根本原因是:字段可见性问题 :Lombok 生成的 getter/setter 默认使用public
,MapStruct 可能无法正确识别;构造方法缺失 :默认无参构造函数导致对象实例化失败;注解处理器顺序:Lombok 注解处理器可能在 MapStruct 之前执行
典型错误场景
java
@Data
public class UserDO {
private Long id;
private String name;
}
@Mapper
public interface UserMapper {
UserDTO toDTO(UserDO userDO);
}
解决方案
1、版本兼容性配置
Maven 依赖:
xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
Gradle 配置:
gradle
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
implementation 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok:1.18.26'
}
2、构建工具配置
Maven 编译器插件:
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
该方式是执行lombok和mapstruct的执行顺序,确保在生成mapstruct的实现类时,lombok已经将对象的get set方法生成。