当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方法生成。

相关推荐
@Arielle。7 分钟前
【Excel】- 导入报错Can not find ‘Converter‘ support class LocalDateTime
java·后端·excel
天草二十六_简村人43 分钟前
kong搭建一套微信小程序的公司研发环境
java·后端·微信小程序·小程序·kong
鱼樱前端2 小时前
前端程序员集体破防!AI工具same.dev像素级抄袭你的代码,你还能高傲多久?
前端·javascript·后端
羊思茗5203 小时前
Spring Boot中@Valid 与 @Validated 注解的详解
java·spring boot·后端
尤宸翎3 小时前
Julia语言的饼图
开发语言·后端·golang
穆韵澜3 小时前
SQL语言的云计算
开发语言·后端·golang
uhakadotcom4 小时前
提升PyODPS性能的实用技巧
后端·面试·github
字节源流4 小时前
【SpringMVC】入门版
java·后端
MrWho不迷糊4 小时前
Spring Boot 的优雅启停:确保停机不影响交易
spring boot·后端
xjz18424 小时前
Netty底层原理深度解析:高并发网络编程的核心设计
后端