一、引言
最近项目使用 DDD(领域驱动设计) 模式的开发,利用分层架构来组织代码,降低了各层级间的耦合性,便于维护和扩展。在该架构中,存在大量 Entity 转 DTO、Request转Command、Entity转DO、DO转Entity等转换逻辑,用于解耦业务逻辑。这些转换方法在项目中频繁出现,手动编写转换代码繁琐且易出错,不利于维护。因此,项目引入了 MapStruct 工具来简化代码。
MapStruct是一个Java注解库,它可以自动生成Entity与DTO、Request与Command等不同类间的转换代码,避免了手动编写转换逻辑。
通过定义包含源对象与目标对象之间映射关系的接口,MapStruct就能在编译时自动生成该接口的实现类,提高了代码的可维护性和可重用性,使得代码逻辑更清晰、易于维护。此外,MapStruct 还能提高代码的类型安全性和性能。由于生成的代码是静态的,因此在运行时不会有额外的性能开销。
二、MapStruct 使用
1、引入依赖:mapstruct、mapstruct-processor
<!-- 包含所需的注解,如 @Mapping -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
<!-- 包含注解处理器,用于生成映射接口的实现类 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
</dependency>
2、明确要转化的对象 BirthdayDTO 和 BirthdayEntity:
public class BirthdayDTO {
private String birthday;
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
}
public class BirthdayEntity {
private String birthday;
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
}
明确要转化的对象后即可创建转化类并使用了。
3、定义转化 BirthdayConverter 类:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
}
4、使用 BirthdayConverter 进行转化:
转化类有两种使用方式:单例实例转化 和 Spring环境映射转化:
单例实例转化:当在非Spring环境中使用MapStruct时,需要使用Mappers.getMapper(转换类名.class)
来获取转换类的实例。
public class SomeClass {
private BirthdayConverter birthdayConverter = Mappers.getMapper(BirthdayConverter.class);
public void someMethod() {
BirthdayEntity birthdayEntity = new BirthdayEntity();
birthdayEntity.setBirthday("20221212");
// 使用birthdayConverter进行转换
BirthdayDTO birthdayDTO = birthdayConverter.toDTO(birthdayEntity);
}
}
Spring环境映射转化:使用 @Autowired
注解来自动装配转化类的实例。
@Service
public class SomeService {
private BirthdayConverter birthdayConverter;
@Autowired
public SomeService(BirthdayConverter birthdayConverter) {
this.birthdayConverter = birthdayConverter;
}
public void someMethod() {
BirthdayEntity birthdayEntity = new BirthdayEntity();
birthdayEntity.setBirthday("20221212");
// 使用birthdayConverter进行转换
BirthdayDTO birthdayDTO = birthdayConverter.toDTO(birthdayEntity);
}
}
三、MapStruct 特殊使用
MapStruct使用注解来定义转换规则:
@Mapper
注解:定义转换器接口,让MapStruct代码生成器在构建时为接口创建一个实现。
@Mapping
注解:定义转换规则,指定了源属性和目标属性之间的映射关系。
@InheritInverseConfiguration
:逆映射,该注解表明方法继承相应的前向方法,不需要为逆映射方法重复定义相同的映射规则。
对于特殊类型或不同类型、不同对象间的字段赋值,MapStruct可以通过定义转换规则来进行字段赋值,代码生成器通过识别定义的规则来自动生成转换代码。
@Mapping
注解常用属性介绍:
source:源字段名
target:目标字段名
dateFormat:用于日期类型属性的映射,指定日期格式
qualifiedByName:用于定义复杂的映射规则。通常与 @Named 注解一起使用,以提供一个唯一的名称来标识不同的映射方法。
expression:允许使用Java表达式来定义复杂的映射逻辑。该表达式在代码生成时被插入到映射方法中,用于计算目标字段的值。限制1:目前仅支持Java表达式;限制2:不能与 source()、defaultValue()、defaultExpression()、qualifiedBy()、qualifiedByName() 或 constant() 这些属性一起使用(原因:这些属性都定义了如何计算目标字段的值,而 MapStruct 需要一个明确的策略来避免冲突)。
constant:用于设置目标对象的属性为一个常量值。无论源对象的相应字段的值是什么,目标字段都将被赋予一个固定的值。限制2同上。
ignore:设置为 true 时,MapStruct 会忽略该属性的映射。用于转换前后有相同字段名,但不想将源对象的某个字段映射到目标对象的情况。
uses:用于指定一个或多个自定义的子映射方法,这些子映射方法会在映射过程中被调用。例如,一个对象包含多个子对象,每个子对象有不同的映射逻辑,此时,先对子对象的映射逻辑创建多个自定义映射器,最后在主映射器中使用uses来引用自定义的映射器。
1、不同字段名之间的映射
如上例所示,若 BirthdayEntity 中有String类型的属性名 character,BirthdayDTO 中String类型的属性名 characterType,其映射关系:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(source = "character", target = "characterType")
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
}
2、不同类型间的映射
2.1 String 与 Type 类型间的映射
若 BirthdayDTO 中的 characterType 为 CharacterType 类型,将其与String类型的character 进行映射:
public class CharacterType {
public static final CharacterType MAN = new CharacterType("1");
String type;
public CharacterType(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
映射函数:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(source = "birthday", target = "birthday")
@Mapping(source = "character", target = "characterType.type")
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
@Mapping(source = "birthday", target = "birthday")
@Mapping(source = "characterType.type", target = "character")
BirthdayEntity toEntity(BirthdayDTO birthdayDTO);
}
需要注意的是,在toDTO
的转化过程中,当BirthdayEntity
中的属性character = null
时,映射到BirthdayDTO
中的属性characterType != null
,而是CharacterType(type=null)
。在一些特殊映射关系的时候需要注意到这点,避免类型转换出错。
2.2 String 与 Date 类型间的映射
若 BirthdayDTO 中存在 Date 类型的属性 cratTime 和 String类型的 updtTime,BirthdayEntity 中存在 String 类型的 cratTime 和 Date类型的 updtTime,可以利用dateFormat属性进行映射。dateFormat通过指定一个日期格式的字符串来格式化Date对象。
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(source = "birthday", target = "birthday")
@Mapping(source = "character", target = "characterType.type")
@Mapping(source = "cratTime", target = "cratTime", dateFormat = "yyyyMMddhhmmss")
@Mapping(source = "updtTime", target = "updtTime", dateFormat = "yyyyMMddhhmmss" )
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
}
3、复杂逻辑映射
3.1 采用 default 关键字
在 MapStruct 中,对于特定类型的映射,不能由MapStruct直接生成,如上文 “ 2 不同类型间的映射”——String类型的character与CharacterType类型的characterType之间的映射存在前者为null,后者不为null的情况。对于这类问题,可以直接在映射器接口中实现自定义方法作为默认方法(Java 8以上)。MapStruct在进行映射时将在参数和返回类型匹配时自动调用该默认方法,无需声明。
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(source = "birthday", target = "birthday")
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
}
default CharacterType string2CharacterType(String character) {
if (character == null) {
return null;
}
return new CharacterType(character);
}
3.2 采用 qualifiedByName 属性
利用 @Named
注解定义一个实现转换方法 seatCountMapping()
。而后在 @Mapping
注解中利用qualifiedByName
属性指定使用名为 “seatCountMapping” 的方法来转换 numberOfSeats
属性,赋值给CarDto
类中的seatCount
属性。
@Mapper
public interface CarMapper {
@Mapping(target = "seatCount", source = "numberOfSeats", qualifiedByName = "seatCountMapping")
CarDto carToCarDto(Car car);
@Named("seatCountMapping")
default int seatCountMapping(int numberOfSeats) {
// 自定义逻辑,例如增加2个座位
return numberOfSeats + 2;
}
}
3.3 采用 expression 属性
@Mapper
public interface CarMapper {
// 调用Car类中的逻辑处理方法 getNumberOfSeats(),赋值给CarDto类中的 seatCount
@Mapping(target = "seatCount", expression = "java(car.getNumberOfSeats())")
// 给CarDto类中的 creationDate 赋值为当前日期时间
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
CarDto carToCarDto(Car car);
}
3.4 采用 constant 属性
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(target = "characterType", constant = "any value")
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
}
3.5 采用不同对象间的映射—uses
(1) 有一个Order对象,它包含多个子对象,如Customer、Product:
// Customer.java
public class Customer {
private String name;
// getters and setters
}
// Product.java
public class Product {
private String name;
private double price;
// getters and setters
}
// Order.java
public class Order {
private Customer customer;
private List<Product> products;
// getters and setters
}
要将其转化为 OrderDto:
// CustomerDto.java
public class CustomerDto {
private String capitalizedName;
// getters and setters
}
// ProductDto.java
public class ProductDto {
private String name;
private double discountedPrice;
// getters and setters
}
// OrderDto.java
public class OrderDto {
private CustomerDto customer;
private List<ProductDto> products;
// getters and setters
}
(2) 分别为Customer和Product创建各自的映射器(子映射器):
// CustomerMapper.java
@Mapper
public interface CustomerMapper {
CustomerDto customerToCustomerDto(Customer customer);
Customer customerDtoToCustomer(CustomerDto customerDto);
default String capitalizeName(String name) {
return name != null ? name.substring(0, 1).toUpperCase() + name.substring(1) : null;
}
}
// ProductMapper.java
@Mapper
public interface ProductMapper {
ProductDto productToProductDto(Product product);
Product productDtoToProduct(ProductDto productDto);
default double calculateDiscountedPrice(double price) {
// Apply some discount logic
return price * 0.9; // Example: 10% discount
}
}
(3) 最后创建主映射器接口,并使用uses属性来指定自定义映射器:
// OrderMapper.java
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = {CustomerMapper.class, ProductMapper.class})
public interface OrderMapper {
OrderDto orderToOrderDto(Order order);
Order orderDtoToOrder(OrderDto orderDto);
}
3.6 采用ignore属性 ------ 忽略映射
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(target = "birthday", ignore = true)
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
}
3.7 逆映射
逆映射指双向映射,如上文 2、不同类型间的映射 节中,BirthdayDTO 与 BirthdayEntity 之间存在双向映射。在定义了 BirthdayEntity 到 BirthdayDTO 的映射关系后,使用 @InheritInverseConfiguration
注解能基于前者的映射关系逆向生成 BirthdayDTO 到 BirthdayEntity 的映射关系。
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface BirthdayConverter {
@Mapping(source = "birthday", target = "birthday")
@Mapping(source = "character", target = "characterType.type")
BirthdayDTO toDTO(BirthdayEntity birthdayEntity);
@InheritInverseConfiguration
BirthdayEntity toEntity(BirthdayDTO birthdayDTO);
}
四、注意事项
在使用MapStruct过程中,项目组存在如下问题:
1、Lombok与MapStruct冲突
项目组一开始将MapStruct配置到Maven的pom.xml中,接着又引入了Lombok工具。此时存在编译不通过的情况: “No property named “属性” exists in source parameter(s)…”。
这是因为MapStruct和Lombok都在编译期生成代码,MapStruct利用getter、setter进行源属性和目标属性映射时,Lombok还未加载,因此getter、setter方法还无法使用,找不到类中的属性。因此, Lombok 应该在 MapStruct 之前处理类。Lombok 在 MapStruct 处理类之前生成 getter、setter 等方法,以避免潜在的冲突。
pom.xml 依赖项的顺序如下:
<dependencies>
<!-- 其他依赖项 -->
<!-- 确保 Lombok 在 MapStruct 之前 -->
<!-- Lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version> <!-- 使用最新的 Lombok 版本 -->
<scope>provided</scope> <!-- Lombok 注解在编译时需要,但在运行时不需要 -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
</dependency>
<!-- 其他依赖项 -->
</dependencies>