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
- 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/