🎯 学习目标
通过本篇教程,你将学会:
- 掌握 Atlas Mapper 的核心注解使用
- 理解基础映射规则和自动映射机制
- 学会处理字段名不匹配的映射场景
- 掌握忽略字段和常量映射的技巧
📋 概念讲解:Atlas Mapper 注解体系
注解层次结构
graph TB
subgraph "核心注解"
A1[@Mapper] --> A2[标记映射器接口]
A3[@Mapping] --> A4[配置字段映射]
A5[@Mappings] --> A6[多个映射配置]
end
subgraph "配置注解"
B1[@Named] --> B2[命名自定义方法]
B3[@InheritInverseConfiguration] --> B4[继承反向配置]
B5[@BeanMapping] --> B6[Bean级别配置]
end
subgraph "策略枚举"
C1[ReportingPolicy] --> C2[报告策略]
C3[NullValueMappingStrategy] --> C4[空值映射策略]
C5[NullValueCheckStrategy] --> C6[空值检查策略]
end
A1 --> B1
A3 --> C1
style A1 fill:#e8f5e8
style A3 fill:#e3f2fd
style B1 fill:#fff3e0
映射规则优先级
flowchart TD
A[开始映射] --> B{是否有@Mapping注解?}
B -->|有| C[使用@Mapping配置]
B -->|无| D{字段名是否相同?}
D -->|相同| E{类型是否兼容?}
D -->|不同| F[跳过映射]
E -->|兼容| G[自动映射]
E -->|不兼容| H{是否有类型转换器?}
H -->|有| I[应用类型转换]
H -->|无| J[编译错误]
C --> K[完成映射]
G --> K
I --> K
F --> K
style C fill:#c8e6c9
style G fill:#e8f5e8
style I fill:#fff3e0
style J fill:#ffcdd2
🔧 实现步骤:核心注解详细使用
步骤 1:@Mapper 注解详解
@Mapper
是最核心的注解,用于标记映射器接口:
java
/**
* @Mapper 注解的完整配置选项
*/
@Mapper(
// 🔥 组件模型 - 决定如何创建映射器实例
componentModel = "spring", // 生成Spring Bean
// componentModel = "default", // 使用 Mappers.getMapper() 获取
// componentModel = "cdi", // CDI 依赖注入
// 📋 未映射目标字段的处理策略
unmappedTargetPolicy = ReportingPolicy.IGNORE, // 忽略
// unmappedTargetPolicy = ReportingPolicy.WARN, // 警告
// unmappedTargetPolicy = ReportingPolicy.ERROR, // 错误
// 🔄 空值映射策略
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
// nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT,
// ✅ 空值检查策略
nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION
// nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface UserMapper {
// 映射方法定义...
}
步骤 2:@Mapping 注解详解
@Mapping
用于配置具体的字段映射规则:
java
public interface UserMapper {
/**
* @Mapping 注解的完整配置选项
*/
@Mapping(
target = "fullName", // 🎯 目标字段名
source = "name", // 📥 源字段名
// 🔄 类型转换配置
dateFormat = "yyyy-MM-dd", // 日期格式
numberFormat = "#.##", // 数字格式
// 📝 表达式映射
expression = "java(source.getFirstName() + ' ' + source.getLastName())",
// 🔧 自定义方法映射
qualifiedByName = "mapStatus", // 引用自定义方法
// ⚠️ 忽略字段
ignore = false, // 是否忽略此字段
// 🔄 空值处理
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT,
// 📊 默认值
defaultValue = "未知" // 当源字段为null时的默认值
)
UserDto toDto(User user);
}
步骤 3:基础映射规则
自动映射规则
java
/**
* 自动映射示例 - 无需配置的情况
*/
public class User {
private Long id; // ✅ 自动映射到 UserDto.id
private String name; // ✅ 自动映射到 UserDto.name
private String email; // ✅ 自动映射到 UserDto.email
private Integer age; // ✅ 自动映射到 UserDto.age
}
public class UserDto {
private Long id; // 字段名相同,类型兼容
private String name; // 字段名相同,类型相同
private String email; // 字段名相同,类型相同
private Integer age; // 字段名相同,类型兼容
}
@Mapper(componentModel = "spring")
public interface UserMapper {
// 🎯 无需任何@Mapping注解,自动映射所有兼容字段
UserDto toDto(User user);
}
💻 示例代码:完整的基础映射示例
示例 1:字段名映射
java
// 源实体类
public class Product {
private Long productId; // 字段名:productId
private String productName; // 字段名:productName
private BigDecimal unitPrice; // 字段名:unitPrice
private String description;
private LocalDateTime createdTime;
private Boolean isActive; // 字段名:isActive
// getter/setter 省略...
}
// 目标 DTO 类
public class ProductDto {
private Long id; // 字段名:id (不同)
private String name; // 字段名:name (不同)
private BigDecimal price; // 字段名:price (不同)
private String description; // 字段名相同
private String createTime; // 类型不同
private String status; // 字段名和类型都不同
// getter/setter 省略...
}
// 映射器接口
@Mapper(componentModel = "spring")
public interface ProductMapper {
@Mapping(target = "id", source = "productId") // 字段名映射
@Mapping(target = "name", source = "productName") // 字段名映射
@Mapping(target = "price", source = "unitPrice") // 字段名映射
@Mapping(target = "createTime", source = "createdTime",
dateFormat = "yyyy-MM-dd HH:mm:ss") // 类型转换
@Mapping(target = "status", source = "isActive",
qualifiedByName = "mapActiveToStatus") // 自定义映射
ProductDto toDto(Product product);
/**
* 自定义映射方法:Boolean -> String
*/
@Named("mapActiveToStatus")
default String mapActiveToStatus(Boolean isActive) {
if (isActive == null) {
return "未知";
}
return isActive ? "上架" : "下架";
}
}
示例 2:忽略字段和常量映射
java
// 用户实体类
public class User {
private Long id;
private String username;
private String password; // 敏感信息,需要忽略
private String email;
private String phone;
private LocalDateTime lastLoginTime;
// getter/setter 省略...
}
// 用户 DTO 类
public class UserDto {
private Long id;
private String username;
// 注意:没有 password 字段
private String email;
private String phone;
private String lastLogin;
private String userType; // 常量字段
private Integer version; // 版本号,固定值
// getter/setter 省略...
}
// 映射器接口
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "lastLogin", source = "lastLoginTime",
dateFormat = "yyyy-MM-dd HH:mm")
@Mapping(target = "userType", constant = "NORMAL") // 🔥 常量映射
@Mapping(target = "version", constant = "1") // 🔥 常量映射
@Mapping(target = "password", ignore = true) // 🔥 忽略敏感字段
UserDto toDto(User user);
/**
* 反向映射 - 从 DTO 到实体
*/
@Mapping(target = "lastLoginTime", source = "lastLogin",
dateFormat = "yyyy-MM-dd HH:mm")
@Mapping(target = "password", ignore = true) // 🔥 忽略密码字段
@Mapping(target = "userType", ignore = true) // 🔥 忽略常量字段
@Mapping(target = "version", ignore = true) // 🔥 忽略版本字段
User toEntity(UserDto dto);
}
示例 3:多个映射配置 (@Mappings)
java
// 订单实体类
public class Order {
private Long orderId;
private String orderNumber;
private BigDecimal totalAmount;
private LocalDateTime orderTime;
private LocalDateTime paymentTime;
private String customerName;
private String customerPhone;
private Integer orderStatus; // 0:待付款, 1:已付款, 2:已发货, 3:已完成
// getter/setter 省略...
}
// 订单 DTO 类
public class OrderDto {
private Long id;
private String number;
private String amount; // 格式化后的金额
private String orderTime;
private String paymentTime;
private String customerInfo; // 组合字段
private String status; // 状态描述
// getter/setter 省略...
}
// 映射器接口
@Mapper(componentModel = "spring")
public interface OrderMapper {
/**
* 使用 @Mappings 注解包含多个映射配置
*/
@Mappings({
@Mapping(target = "id", source = "orderId"),
@Mapping(target = "number", source = "orderNumber"),
@Mapping(target = "amount", source = "totalAmount",
numberFormat = "¥#,##0.00"), // 🔥 数字格式化
@Mapping(target = "orderTime", source = "orderTime",
dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(target = "paymentTime", source = "paymentTime",
dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(target = "customerInfo",
expression = "java(order.getCustomerName() + \" (\" + order.getCustomerPhone() + \")\")"
), // 🔥 表达式映射
@Mapping(target = "status", source = "orderStatus",
qualifiedByName = "mapOrderStatus") // 🔥 自定义映射
})
OrderDto toDto(Order order);
/**
* 订单状态映射
*/
@Named("mapOrderStatus")
default String mapOrderStatus(Integer status) {
if (status == null) {
return "未知状态";
}
switch (status) {
case 0: return "待付款";
case 1: return "已付款";
case 2: return "已发货";
case 3: return "已完成";
default: return "未知状态";
}
}
}
🎬 效果演示:测试基础映射功能
创建测试控制器
java
@RestController
@RequestMapping("/api/mapping-demo")
public class MappingDemoController {
@Autowired
private ProductMapper productMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 演示产品映射
*/
@GetMapping("/product")
public ProductDto getProduct() {
Product product = new Product();
product.setProductId(1001L);
product.setProductName("iPhone 15 Pro");
product.setUnitPrice(new BigDecimal("8999.00"));
product.setDescription("最新款 iPhone,性能强劲");
product.setCreatedTime(LocalDateTime.now());
product.setIsActive(true);
// 🎯 映射转换
ProductDto dto = productMapper.toDto(product);
System.out.println("原始产品: " + product);
System.out.println("映射结果: " + dto);
return dto;
}
/**
* 演示用户映射(忽略字段和常量映射)
*/
@GetMapping("/user")
public UserDto getUser() {
User user = new User();
user.setId(2001L);
user.setUsername("zhangsan");
user.setPassword("secret123"); // 这个字段会被忽略
user.setEmail("zhangsan@example.com");
user.setPhone("13800138000");
user.setLastLoginTime(LocalDateTime.now().minusHours(2));
// 🎯 映射转换
UserDto dto = userMapper.toDto(user);
System.out.println("原始用户: " + user);
System.out.println("映射结果: " + dto);
System.out.println("注意:密码字段被忽略,userType 和 version 是常量值");
return dto;
}
/**
* 演示订单映射(复杂映射场景)
*/
@GetMapping("/order")
public OrderDto getOrder() {
Order order = new Order();
order.setOrderId(3001L);
order.setOrderNumber("ORD20250109001");
order.setTotalAmount(new BigDecimal("1299.99"));
order.setOrderTime(LocalDateTime.now().minusDays(1));
order.setPaymentTime(LocalDateTime.now().minusDays(1).plusMinutes(15));
order.setCustomerName("李四");
order.setCustomerPhone("13900139000");
order.setOrderStatus(2); // 已发货
// 🎯 映射转换
OrderDto dto = orderMapper.toDto(order);
System.out.println("原始订单: " + order);
System.out.println("映射结果: " + dto);
return dto;
}
/**
* 演示反向映射
*/
@PostMapping("/user-reverse")
public User createUser(@RequestBody UserDto dto) {
// 🎯 反向映射:DTO -> Entity
User user = userMapper.toEntity(dto);
// 设置被忽略的字段
user.setPassword("defaultPassword"); // 手动设置默认密码
System.out.println("DTO输入: " + dto);
System.out.println("反向映射结果: " + user);
return user;
}
}
测试映射效果
bash
# 测试产品映射
curl http://localhost:8080/api/mapping-demo/product
# 期望返回:
# {
# "id": 1001,
# "name": "iPhone 15 Pro",
# "price": 8999.00,
# "description": "最新款 iPhone,性能强劲",
# "createTime": "2025-01-09 15:30:45",
# "status": "上架"
# }
# 测试用户映射
curl http://localhost:8080/api/mapping-demo/user
# 期望返回:
# {
# "id": 2001,
# "username": "zhangsan",
# "email": "zhangsan@example.com",
# "phone": "13800138000",
# "lastLogin": "2025-01-09 13:30",
# "userType": "NORMAL",
# "version": 1
# }
# 注意:没有 password 字段
# 测试订单映射
curl http://localhost:8080/api/mapping-demo/order
# 期望返回:
# {
# "id": 3001,
# "number": "ORD20250109001",
# "amount": "¥1,299.99",
# "orderTime": "2025-01-08 15:30:45",
# "paymentTime": "2025-01-08 15:45:45",
# "customerInfo": "李四 (13900139000)",
# "status": "已发货"
# }
查看生成的映射代码
编译后查看生成的实现类:
java
// ProductMapperImpl.java (生成的代码示例)
@Component
public class ProductMapperImpl implements ProductMapper {
@Override
public ProductDto toDto(Product product) {
if (product == null) {
return null;
}
ProductDto productDto = new ProductDto();
// 字段名映射
productDto.setId(product.getProductId());
productDto.setName(product.getProductName());
productDto.setPrice(product.getUnitPrice());
// 自动映射
productDto.setDescription(product.getDescription());
// 日期格式化
if (product.getCreatedTime() != null) {
productDto.setCreateTime(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(product.getCreatedTime())
);
}
// 自定义方法映射
productDto.setStatus(mapActiveToStatus(product.getIsActive()));
return productDto;
}
}
❓ 常见问题
Q1: 什么时候需要使用 @Mapping 注解?
A: 以下情况需要使用 @Mapping 注解:
java
// 1. 字段名不匹配
@Mapping(target = "fullName", source = "name")
// 2. 类型需要转换
@Mapping(target = "createTime", source = "createdAt", dateFormat = "yyyy-MM-dd")
// 3. 需要忽略字段
@Mapping(target = "password", ignore = true)
// 4. 需要设置常量值
@Mapping(target = "version", constant = "1.0")
// 5. 需要自定义映射逻辑
@Mapping(target = "status", source = "active", qualifiedByName = "mapStatus")
Q2: 如何处理嵌套对象映射?
A: Atlas Mapper 会自动处理嵌套对象:
java
public class User {
private Address address; // 嵌套对象
}
public class UserDto {
private AddressDto address; // 对应的 DTO
}
// 需要同时定义 Address 的映射器
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressDto toDto(Address address);
}
// User 映射器会自动使用 AddressMapper
@Mapper(componentModel = "spring", uses = AddressMapper.class)
public interface UserMapper {
UserDto toDto(User user); // 自动处理 address 字段映射
}
Q3: @Mappings 和多个 @Mapping 有什么区别?
A: 功能相同,只是写法不同:
java
// 方式一:使用 @Mappings
@Mappings({
@Mapping(target = "id", source = "userId"),
@Mapping(target = "name", source = "userName")
})
UserDto toDto(User user);
// 方式二:多个 @Mapping(推荐)
@Mapping(target = "id", source = "userId")
@Mapping(target = "name", source = "userName")
UserDto toDto(User user);
Q4: 如何调试映射问题?
A: 几种调试方法:
- 查看生成的代码:
bash
# 生成的代码在这个目录
target/generated-sources/annotations/
- 启用详细日志:
xml
<compilerArgs>
<arg>-Amapstruct.verbose=true</arg>
</compilerArgs>
- 使用 IDE 断点调试:
java
// 在生成的实现类中打断点
@Override
public UserDto toDto(User user) {
// 👈 在这里打断点
if (user == null) {
return null;
}
// ...
}
🎯 本章小结
通过本章学习,你应该掌握了:
- 核心注解:@Mapper、@Mapping、@Mappings 的使用
- 映射规则:自动映射和手动配置的优先级
- 字段映射:处理字段名不匹配的场景
- 特殊映射:忽略字段、常量映射、自定义映射
📖 下一步学习
在下一篇教程中,我们将学习:
- 高级映射技巧和复杂场景处理
- 类型转换器的使用和自定义
- 集合映射和嵌套对象映射