当lombok遇到mapstruct,会碰撞出什么样的火花

在日常的开发过程中,经常遇到对象转换的场景,那么不同方式的对象转换,有什么样的区别呢?

方式 优点 缺点 适用场景
手动转换 灵活度高,完全可控 代码冗余,维护成本高 少量字段或复杂业务逻辑
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

  1. 核心优势
  • 代码简洁 :通过@Data减少 70% 样板代码
  • 统一规范:自动生成标准 getter/setter 方法
  • 功能扩展 :支持@Builder@Slf4j等实用注解
  1. 潜在问题
  • 可读性下降:隐藏代码实现细节
  • 反射问题:某些框架依赖反射获取字段信息
  • 工具兼容性:与代码生成工具(如 MapStruct)存在冲突
  • 调试困难:IDE 调试时可能无法直接定位生成代码
  1. 最佳实践
  • 优先使用@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方法生成。

相关推荐
Asthenia041218 分钟前
操作系统/进程线程/僵尸进程/IPC与PPC/进程大小/进程的内存组成/协程相关/Netty相关拷打
后端
Asthenia04121 小时前
深入解析 MySQL 执行更新语句、查询语句及 Redo Log 与 Binlog 一致性
后端
杨充1 小时前
10.接口而非实现编程
后端
等什么君!2 小时前
springmvc入门案例
后端·spring
苏三说技术2 小时前
基于SpringBoot的课程管理系统
java·spring boot·后端
桦说编程2 小时前
警惕AI幻觉!Deepseek对Java线程池中断机制的理解有误
java·后端·deepseek
用户276174834213 小时前
GitLab-CE 及 GitLab Runner 安装部署
后端
前端涂涂3 小时前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端
博弈美业系统Java源码3 小时前
连锁美业管理系统「数据分析」的重要作用分析︳博弈美业系统疗愈系统分享
java·大数据·前端·后端·创业创新
秋野酱3 小时前
基于javaweb的SpringBoot扶农助农平台管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端