SpringBoot与MapStruct整合,解决复杂对象转换中的类型安全漏洞问题

开发 前端
我们在写多层架构时,数据传输对象(DTO)、实体类和其他业务对象之间的转换是不可避免的。手动编写这些映射逻辑不仅耗时而且容易出错。

我们在写多层架构时,数据传输对象(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;
    }
}

代码解读

  1. 默认值 (defaultValue):
  • 在 UserMapper 中,@Mapping(target = "age", defaultValue = "18") 指定了如果 User 对象中的 age 属性为 null,则默认值为 18
  1. 条件映射 (qualifiedByName):
  • 使用 @Named 注解定义了两个方法 activeToStatus 和 statusToActive,用于在 User 和 UserDto 之间进行状态转换。
  • 这些方法通过 @Mapping(target = ..., source = ..., qualifiedByName = ...) 被引用。
  1. 自定义方法:
  • 自定义方法 activeToStatus 和 statusToActive 直接在 UserMapper 接口中实现,提供了灵活的数据转换逻辑。
  1. 枚举映射:
  • 通过自定义方法将布尔类型的 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 : "";
}


责任编辑:武晓燕 来源: Java知识日历
相关推荐

2010-04-20 09:44:24

2013-05-07 13:32:25

2020-08-16 08:50:03

Zoom网络攻击漏洞

2009-07-05 11:27:09

2023-07-28 12:44:04

2025-03-06 08:37:01

2010-11-01 13:38:11

2013-06-03 14:39:01

2011-03-31 13:46:51

2022-09-02 08:17:40

MapStruct代码工具

2013-01-30 09:56:56

2010-07-26 15:37:12

telnet安全漏洞

2011-05-30 09:19:00

2009-11-06 10:09:55

2022-07-28 16:47:32

漏洞网络安全风险

2017-06-08 12:25:52

2015-03-19 09:36:39

2014-06-03 09:23:41

2023-07-10 12:06:53

2014-06-03 11:36:18

点赞
收藏

51CTO技术栈公众号