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

443 阅读5分钟

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

方式优点缺点适用场景
手动转换灵活度高,完全可控代码冗余,维护成本高少量字段或复杂业务逻辑
BeanUtils简单易用,无需手写代码性能较差,反射机制效率低快速原型开发
ModelMapper支持复杂映射,配置灵活学习成本高,性能一般中等复杂度的对象转换
MapStruct性能最优,编译期生成代码需学习特定注解大规模对象转换

在这篇文章中,主要推荐MapStruct,针对简单、复杂场景使用都很方便。

1. 简单对象映射

当源对象和目标对象的字段名称和类型完全一致时,MapStruct 可以自动完成映射。

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 注解指定映射关系。

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. 类型转换的映射

当源对象和目标对象的字段类型不同时,需要自定义类型转换方法。

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 可以自动处理。

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. 嵌套对象映射

当源对象和目标对象包含嵌套对象时,需要处理嵌套对象的映射。

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

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 联用时,可能出现以下错误:

[ERROR] No property named "id" exists in source parameter(s). Did you mean "null"?

根本原因是:字段可见性问题:Lombok 生成的 getter/setter 默认使用public,MapStruct 可能无法正确识别;构造方法缺失:默认无参构造函数导致对象实例化失败;注解处理器顺序:Lombok 注解处理器可能在 MapStruct 之前执行

典型错误场景
@Data
public class UserDO {
    private Long id;
    private String name;
}

@Mapper
public interface UserMapper {
    UserDTO toDTO(UserDO userDO);
}
解决方案

1、版本兼容性配置

Maven 依赖

<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 配置

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 编译器插件

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