我们在写多层架构时,数据传输对象(DTO)、实体类和其他业务对象之间的转换是不可避免的。手动编写这些映射逻辑不仅耗时而且容易出错。为了提高开发效率和代码质量,我们建议引入 MapStruct 作为我们的对象映射工具。
MapStruct 的优势
1. 编译时检查
- 类型安全: 编译器会在编译时检查映射逻辑,确保源对象和目标对象的字段匹配。
- 错误报告: 如果映射出现问题,编译器会立即报错,便于调试和修复。
2. 高性能
- 无反射: 生成的映射代码不依赖反射,执行速度快。
- 零运行时开销: 不需要额外的库或运行时组件。
3. 清晰的配置
- 注解驱动: 使用简洁的注解来定义映射规则,易于理解和维护。
- 默认行为: 提供合理的默认行为,减少样板代码。
4. 强大的功能支持
- 嵌套对象: 自动处理嵌套对象的映射。
- 集合映射: 支持列表、数组等多种集合类型的映射。
- 自定义方法: 可以通过自定义方法实现复杂的映射逻辑。
- 枚举映射: 支持枚举类型的映射。
代码实操
<dependencies>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<!-- MapStruct Processor -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
<scope>provided</scope>
</dependency>
<!-- Lombok (Optional, for reducing boilerplate code) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
实体类 Address
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
publicclass Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String city;
// Getters and Setters (or use Lombok)
}
实体类 User
import javax.persistence.*;
import java.util.List;
@Entity
publicclass User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private Boolean isActive;
@OneToOne(cascade = CascadeType.ALL)
private Address address;
@OneToMany(cascade = CascadeType.ALL)
private List<Address> addresses;
// Getters and Setters (or use Lombok)
}
DTO 类 AddressDto
public class AddressDto {
private Long id;
private String street;
private String city;
// Getters and Setters (or use Lombok)
}
枚举类 Status
public enum Status {
ACTIVE,
INACTIVE
}
DTO 类 UserDto
import java.util.List;
publicclass UserDto {
private Long id;
private String name;
private Integer age;
private Status status;
private AddressDto address;
private List<AddressDto> addresses;
// Getters and Setters (or use Lombok)
}
创建两个 Mapper 接口来分别处理
Address
到AddressDto
的映射和User
到UserDto
的映射。
AddressMapper 接口
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
AddressDto addressToAddressDto(Address address);
Address addressDtoToAddress(AddressDto addressDto);
}
UserMapper 接口
import org.mapstruct.*;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper(componentModel = "spring", uses = {AddressMapper.class})
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "address", target = "address")
@Mapping(source = "addresses", target = "addresses")
@Mapping(target = "status", source = "isActive", qualifiedByName = "activeToStatus")
@Mapping(target = "age", defaultValue = "18") // Default value if age is null
UserDto userToUserDto(User user);
@Mapping(source = "address", target = "address")
@Mapping(source = "addresses", target = "addresses")
@Mapping(target = "isActive", source = "status", qualifiedByName = "statusToActive")
User userDtoToUser(UserDto userDto);
@Named("activeToStatus")
default Status activeToStatus(Boolean isActive) {
return isActive != null && isActive ? Status.ACTIVE : Status.INACTIVE;
}
@Named("statusToActive")
default Boolean statusToActive(Status status) {
return status == Status.ACTIVE;
}
}
代码解读
- 默认值 (
defaultValue
):
- 在
UserMapper
中,@Mapping(target = "age", defaultValue = "18")
指定了如果User
对象中的age
属性为null
,则默认值为18
。
- 条件映射 (
qualifiedByName
):
- 使用
@Named
注解定义了两个方法activeToStatus
和statusToActive
,用于在User
和UserDto
之间进行状态转换。 - 这些方法通过
@Mapping(target = ..., source = ..., qualifiedByName = ...)
被引用。
- 自定义方法:
- 自定义方法
activeToStatus
和statusToActive
直接在UserMapper
接口中实现,提供了灵活的数据转换逻辑。
- 枚举映射:
- 通过自定义方法将布尔类型的
isActive
字段映射到枚举类型的Status
字段,并反之。
常见问题与解决方案
MapStruct 是否支持循环引用?
MapStruct 默认不支持循环引用,但可以通过自定义方法或使用 @AfterMapping
注解来处理循环引用的情况。
如何处理不同命名的属性?
使用 @Mapping
注解中的 source
和 target
属性来指定不同的属性名称:
@Mapping(source = "entityPropertyName", target = "dtoPropertyName")
如何处理日期格式转换?
使用 @Mapping
注解中的 dateFormat
属性:
@Mapping(source = "dateField", dateFormat = "yyyy-MM-dd")
如何处理集合过滤?
使用 @IterableMapping
注解结合 qualifiedByName
或 qualifiedBy
来过滤集合:
@IterableMapping(qualifiedByName = "filterNulls")
List<String> filterStrings(List<String> strings);
@Named("filterNulls")
default String filterNulls(String string) {
return string != null ? string : "";
}