Atlas Mapper 教程系列 (3/10):核心注解详解与基础映射

🎯 学习目标

通过本篇教程,你将学会:

  • 掌握 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: 几种调试方法:

  1. 查看生成的代码
bash 复制代码
# 生成的代码在这个目录
target/generated-sources/annotations/
  1. 启用详细日志
xml 复制代码
<compilerArgs>
    <arg>-Amapstruct.verbose=true</arg>
</compilerArgs>
  1. 使用 IDE 断点调试
java 复制代码
// 在生成的实现类中打断点
@Override
public UserDto toDto(User user) {
    // 👈 在这里打断点
    if (user == null) {
        return null;
    }
    // ...
}

🎯 本章小结

通过本章学习,你应该掌握了:

  1. 核心注解:@Mapper、@Mapping、@Mappings 的使用
  2. 映射规则:自动映射和手动配置的优先级
  3. 字段映射:处理字段名不匹配的场景
  4. 特殊映射:忽略字段、常量映射、自定义映射

📖 下一步学习

在下一篇教程中,我们将学习:

  • 高级映射技巧和复杂场景处理
  • 类型转换器的使用和自定义
  • 集合映射和嵌套对象映射
相关推荐
tqs_123452 小时前
redis zset 处理大规模数据分页
java·算法·哈希算法
尚学教辅学习资料2 小时前
基于Spring Boot的家政服务管理系统+论文示例参考
java·spring boot·后端·java毕设
杨杨杨大侠2 小时前
Atlas Log 0.2.0 版本
java·github·apache log4j
快乐肚皮2 小时前
TransmittableThreadLocal:穿透线程边界的上下文传递艺术
java
渣哥2 小时前
别再无脑 synchronized 了!Java 锁优化的 7 个狠招
java
緈諨の約錠3 小时前
JVM基础篇以及JVM内存泄漏诊断与分析
java·jvm
Slaughter信仰3 小时前
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第十三章知识点问答(15题)
java·开发语言·jvm
绝无仅有3 小时前
大厂Redis高级面试题与答案
后端·面试·github
Java进阶笔记3 小时前
JVM默认栈大小
java·jvm·后端