MapStruct 使用及原理浅析

MapStruct 使用及原理浅析

前言

在 Java 开发中,对象间的属性拷贝(如 DTO 与 Entity 的转换)是高频且容易出错的场景。传统的手工编码方式效率低下,反射工具类(如 Apache BeanUtils)存在性能损耗,而今天我们要介绍的 MapStruct 通过编译期自动生成映射代码,兼顾了开发效率和运行性能。本文将结合案例与实践,带领读者快速掌握 MapStruct 的核心用法与实现原理。

MapStruct 是一个基于 Java 注解处理器(Annotation Processor)的代码生成工具,专门用于实现 Java Bean 之间的类型安全、高性能的属性映射。相较于其他映射方案:

● 通过编译期生成原生 Java 代码(非反射实现)

● 提供类型安全检查(编译期报错)

● 支持复杂映射规则(自定义转换方法、表达式等)

● 默认支持 Lombok(2022 年起官方提供兼容支持)

案例:如何使用 MapStruct?

场景一:简单对象映射

@Data
public class Customer {
    private Long id;
    private String name;
    public String numberStr;
    private Collection<OrderItem> orderItems;
}

@Data
public class CustomerDto {
    public Long id;  
    public String customerName;  
    public Integer number;  
    public List<OrderItemDto> orders;
}

@Data
public class OrderItem {
    private String name;
    private Long quantity;
}

@Data
public class OrderItemDto {
    public String name;
    public Long quantity;
}

@Mapper(uses = { OrderItemMapper.class })
public interface CustomerMapper {
    CustomerMapper MAPPER = Mappers.getMapper( CustomerMapper.class );
    @Mapping(source = "orders", target = "orderItems")
    @Mapping(source = "customerName", target = "name")
    @Mapping(source = "number", target = "numberStr")
    Customer toCustomer(CustomerDto customerDto);
    @InheritInverseConfiguration
    CustomerDto fromCustomer(Customer customer);
}

@Mapper
public interface OrderItemMapper {
    OrderItemMapper MAPPER = Mappers.getMapper(OrderItemMapper.class);
    OrderItem toOrder(OrderItemDto orderItemDto);
    @InheritInverseConfiguration
    OrderItemDto fromOrder(OrderItem orderItem);
}

场景二:嵌套属性映射

@Data
public class DeliveryAddress {
    Street street;  
}

@Data
public class Street {
    String name;
    int number;
}

@Mapper
public interface AddressMapper {
    @Mapping(target = "streetName", source = "address.street.name")
    @Mapping(target = "streetNo", source = "address.street.number")
    AddressDto convert(DeliveryAddress address);
}

场景三:多源参数映射

@Mapper
public interface AddressMapper {
    @Mapping(target = "description", source = "person.description")
    @Mapping(target = "houseNumber", source = "address.houseNo")
    @Mapping(target = "id", source = "idNo")
    DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address, Integer idNo);
}

场景四:更新已有对象

// 更新已有对象,并使用点符号自动处理嵌套
@Mapping(target = ".", source = "address.street")
void updateExisting(Address address, @MappingTarget AddressDto dto);

场景五:集合映射

@Mapper
public interface CollectionMapper {
    @Mapping
    UserDTO toDTO(User user);
    @Mapping
    List<UserDTO> toDTOList(List<User> users);

    @Mapping(key = "userId", value = "user.id")
    Map<String, String> toMap(User user);
    @Mapping(target = "name", source = "userName")
    User toUser(Map<String, String> map);
    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}

场景六:自定义映射方法

// 自定义加密方法
public class SecurityUtils {
    public static String encrypt(String data) {
        return Base64.getEncoder().encodeToString(data.getBytes());
    }
}
         
// 在Mapper中引用
@Mapper(uses = SecurityUtils.class)
public interface UserMapper {
    @Mapping(target = "password", 
        expression = "java(SecurityUtils.encrypt(user.getRawPassword()))")
    UserDTO toDTO(User user);
}

○ 场景七:对象深度拷贝

@Mapper(mappingControl = DeepClone.class)
public interface Cloner {
    Cloner MAPPER = Mappers.getMapper(Cloner.class);
    CustomerDto clone(CustomerDto customerDto);
}

原理:MapStruct 是如何实现的?

APT(Annotation Processing Tool)

注解处理器

我们通过一个简化版的 @Builder 注解处理器,演示其工作原理:

(1)自定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
}

(2)实现处理器

@SupportedAnnotationTypes("com.example.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class BuilderProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, 
                          RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Builder.class)) {
            if (element.getKind() != ElementKind.CLASS) {
                continue;
            }
            // 生成Builder类
            String className = element.getSimpleName() + "Builder";
            JavaFileObject file = processingEnv.getFiler()
                .createSourceFile(className);
            try (PrintWriter out = new PrintWriter(file.openWriter())) {
                out.println("public class " + className + " {");
                out.println("    // 自动生成的Builder代码");
                out.println("}");
            }
        }
        return true;
    }
}

(3)注册处理器

在 resources/META-INF/services/javax.annotation.processing.Processor 文件中写入:

com.example.BuilderProcessor

  1. MapStruct 的注解处理流程

MapStruct 的处理流程包含以下关键步骤:

// 伪代码展示核心逻辑
public class MapStructProcessor extends AbstractProcessor {
    public boolean process(...) {
        // 步骤1:收集所有@Mapper接口
        Set<TypeElement> mappers = collectMappers(roundEnv);
        // 步骤2:解析每个Mapper接口
        for (TypeElement mapper : mappers) {
            // 创建AST模型
            MapperModel model = parseMapper(mapper);
            // 类型校验
            validateModel(model);
            // 生成实现类
            generateImplClass(model);
        }
    }
    private void generateImplClass(MapperModel model) {
        // 使用JavaPoet生成代码
        TypeSpec.Builder classBuilder = TypeSpec.classBuilder(model.getImplName());
        // 添加字段映射代码
        for (MappingMethod method : model.getMethods()) {
            MethodSpec methodSpec = buildMethodSpec(method);
            classBuilder.addMethod(methodSpec);
        }
        // 写入文件系统
        JavaFile.builder(packageName, classBuilder.build())
            .build()
            .writeTo(filer);
    }
}

Java 代码生成技术对比

不同阶段的代码生成技术对比

生成阶段 代表技术 实现方式 优点 缺点
编译 MapStruct 生成.java 文件并编译 类型安全、可调试 需要处理生成代码
构建 Lombok 修改 AST 代码简洁 代码不可见,依赖特定构建工具
加载 ASM 修改.class 文件 运行时灵活 调试困难
运行 Java 动态代理 生成子类 无需预编译 性能损耗大

背景知识:Java 编译过程详解

Java 的编译过程主要分为以下几个阶段:

(1)词法分析(Lexical Analysis)

将源代码字符流转换为 Token 序列,识别关键字、标识符、运算符等基本元素。

(2)语法分析(Syntax Analysis)

根据 Token 构建抽象语法树(Abstract Syntax Tree, AST),检查代码的语法结构是否正确(如括号匹配、语句结构)。

(3)语义分析(Semantic Analysis)

进行类型检查、变量作用域验证、方法重载解析等语义层面的分析,确保代码逻辑合法。

(4)注解处理(Annotation Processing)

执行自定义的注解处理器(如 Lombok、MapStruct),生成新的代码或修改现有 AST。

注:此阶段可能多轮执行,直到没有新文件生成。

(5)字节码生成(Bytecode Generation)

将最终的 AST 转换为 JVM 可执行的字节码(.class 文件)。

官方文档:

https://mapstruct.org/documentation/stable/reference/html/