关于MapStruct的使用

一、引言

最近项目使用 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>
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java MapStruct是一个代码生成器,用于处理Java bean之间的映射。它通过在编译时生成映射代码来提高性能,并且可以自定义映射逻辑。以下是使用Java MapStruct的步骤: 1. 添加MapStruct依赖项到Maven或Gradle项目中。 2. 创建一个Java接口,该接口定义了要映射的源和目标bean之间的映射方法。 3. 在接口上使用@Mapper注释,指定MapStruct生成的实现类的名称。 4. 在映射方法上使用@Mapping注释,指定源和目标bean属性之间的映射关系。 5. 在Maven或Gradle项目中运行编译命令,以生成MapStruct实现类。 6. 在代码中使用MapStruct生成的实现类来执行bean之间的映射。 下面是一个使用Java MapStruct的简单示例: 1. 添加MapStruct依赖项到Maven或Gradle项目中。 ```xml <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.4.2.Final</version> </dependency> ``` 2. 创建一个Java接口,该接口定义了要映射的源和目标bean之间的映射方法。 ```java @Mapper public interface CarMapper { CarDto carToCarDto(Car car); } ``` 3. 在接口上使用@Mapper注释,指定MapStruct生成的实现类的名称。 ```java @Mapper(componentModel = "spring") public interface CarMapper { CarDto carToCarDto(Car car); } ``` 4. 在映射方法上使用@Mapping注释,指定源和目标bean属性之间的映射关系。 ```java @Mapper(componentModel = "spring") public interface CarMapper { @Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto(Car car); } ``` 5. 在Maven或Gradle项目中运行编译命令,以生成MapStruct实现类。 6. 在代码中使用MapStruct生成的实现类来执行bean之间的映射。 ```java @Autowired private CarMapper carMapper; public void example() { Car car = new Car("Morris", 5); CarDto carDto = carMapper.carToCarDto(car); } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值