第一部分:MapStruct 在四者之间的最佳转换实践
在分层架构中,MapStruct 的核心价值在于用编译期代码生成替代运行期反射,兼顾了手写代码的高性能和自动化工具的便捷。
1. 基础配置与约定
首先,在 pom.xml 中引入依赖。同时,如果项目使用 Lombok,请确保其版本在 1.18.10 及以上,以避免兼容性问题。
核心约定: 对于属性名和类型完全一致 的字段,无需任何配置,MapStruct 会自动完成映射。你只需关注有差异的部分。
2. 四层对象转换的典型实践
我们通过代码来看各层之间如何转换。
-
Entity/Domain (Model) → DTO
在 Service 层,将领域模型转换为 DTO 用于内部传输或 RPC 调用,核心是过滤敏感字段(如密码)。
java@Mapper(componentModel = "spring") public interface UserMapper { // 将 User 转换为 UserDTO,忽略 password 字段 @Mapping(target = "password", ignore = true) UserDTO toDto(User user); }ignore = true是过滤敏感信息最直接的方式。 -
Request DTO → Entity/Domain (Model)
在 Controller 层,将前端传来的 DTO 转换为领域模型。
java@Mapper(componentModel = "spring") public interface UserMapper { // 将 UserCreateDTO 转换为 User 实体 User toEntity(UserCreateDTO dto); } -
Domain (Model) → VO
在 Controller 层,将领域模型转换为最终返回给前端的 VO,主要处理字段映射 和类型格式化。
java@Mapper(componentModel = "spring") public interface UserMapper { // 将 User 转换为 UserVO @Mapping(source = "username", target = "displayName") // 字段名不同 @Mapping(source = "birthday", target = "birthdayStr", dateFormat = "yyyy-MM-dd") // 日期格式化 @Mapping(target = "password", ignore = true) // 忽略敏感字段 UserVO toVo(User user); }当源和目标字段名不同时,用
@Mapping指定映射关系。
3. 高级实践:复用与自定义
-
定义 BaseMapping 基类
抽取通用的转换逻辑(如统一的日期格式),避免重复配置。
java@MapperConfig public interface BaseMapping<SOURCE, TARGET> { @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") TARGET sourceToTarget(SOURCE source); // ... 反向及集合转换方法 }之后,具体的 Mapper 可以通过
@Mapper(config = BaseMapping.class)来继承这些配置。 -
处理复杂类型转换
对于 MapStruct 无法自动映射的复杂类型,可以定义自定义方法来完成。
java@Mapper(componentModel = "spring") public interface UserMapper { // 自定义方法:将 JSON 字符串解析为 List<UserConfig> default List<UserConfig> stringToConfigList(String config) { // ... 解析逻辑 } // 映射时会自动调用上面的自定义方法 UserVO toVo(User user); }
4. 异常处理:忽略未映射的字段
为避免因字段遗漏导致编译警告,可以在 @Mapper 上设置策略。
java
@Mapper(componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
// ...
}
最佳实践总结 :遵循"一方向一 Mapper "的原则,为每个转换方向(如
toDto,toVo)定义独立方法,保持职责清晰。严禁让 VO 继承 DTO 或 Model,它们应是完全独立的 POJO。
第二部分:充血模型(Rich Domain Model)下 Model 如何写业务逻辑
充血模型的核心思想是将数据(状态)和行为(业务逻辑)封装在同一个领域对象中。
充血模型与贫血模型的根本区别在于业务逻辑的归属:
- 贫血模型:Model 只是"数据袋子",所有业务逻辑在 Service 层实现。
- 充血模型:Model 自身包含业务逻辑,Service 层仅做协调和编排。
1. 充血模型代码示例
下面是一个充血模型下 Order 类的写法:
java
public class Order {
// === 状态(数据) ===
private Long id;
private OrderStatus status;
private List<OrderItem> items;
private Customer customer;
private Money totalAmount;
// === 行为(业务逻辑) ===
// 1. 业务规则:订单能否被取消
public boolean canBeCancelled() {
return this.status == OrderStatus.PENDING
|| this.status == OrderStatus.PAID;
}
// 2. 业务操作:添加商品
public void addItem(Product product, int quantity) {
// 业务逻辑:检查库存、计算价格等
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
OrderItem item = new OrderItem(product, quantity);
this.items.add(item);
// 充血模型:状态变更由自身方法触发
this.recalculateTotalAmount();
}
// 3. 业务操作:取消订单
public void cancel() {
if (!this.canBeCancelled()) {
throw new IllegalStateException("当前状态不可取消");
}
this.status = OrderStatus.CANCELLED;
// 可以触发领域事件,通知库存服务等
}
// 4. 内部方法:重新计算总价
private void recalculateTotalAmount() {
this.totalAmount = this.items.stream()
.map(OrderItem::getSubTotal)
.reduce(Money.ZERO, Money::add);
}
}
2. 职责划分:业务逻辑放哪里?
这是实践中最容易困惑的地方。核心原则是根据逻辑的内聚性来判断:
-
放在 Model 中的逻辑(内聚性高):
- 校验逻辑 :与自己状态相关的校验(如
canBeCancelled())。 - 计算逻辑 :基于自身数据计算(如
recalculateTotalAmount())。 - 状态变更 :改变自身状态的操作(如
addItem(),cancel())。
- 校验逻辑 :与自己状态相关的校验(如
-
放在 Domain Service 中的逻辑(需要外部协作):
- 跨聚合的操作 :如下单需要同时操作
Order和Inventory。 - 依赖外部资源:需要调用 Repository 或外部服务。
java@Service public class OrderDomainService { private final OrderRepository orderRepo; private final InventoryService invService; @Transactional public void placeOrder(Order order) { // 1. 调用 Order 自身逻辑 order.calculatePrice(); // 2. 跨聚合的协作(由 Domain Service 协调) for (OrderItem item : order.getItems()) { invService.deductStock(item.getProductId(), item.getQuantity()); } // 3. 持久化 orderRepo.save(order); } } - 跨聚合的操作 :如下单需要同时操作
3. "结构充血" vs "行为充血"
针对"模型因包含过多逻辑而臃肿"的问题,DDD 社区提出了更精细的实践:
- "行为充血"(不推荐) :将
placeOrder这样的复杂流程全放进Order对象,导致模型臃肿。 - "结构充血"(推荐) :将稳定性高的规则 (如
canBeCancelled)放在 Model 中;将易变的复杂流程 (如placeOrder)上浮到 Domain Service 或流程编排引擎中。Model 的核心价值在于提供清晰、表达业务语义的"领域语言"。
最佳实践总结 :采用充血模型时,要避免将 Model 变成万能类 。核心准则是:"我"自己的事(校验、计算、状态变更)在 Model 内解决;"我们"的事(跨对象、跨聚合的协作)交给 Domain Service 处理。
总结:MapStruct + 充血模型的协同
当采用充血模型时,Model 包含了业务逻辑。MapStruct 在其中的职责更加清晰和重要:
- 保护领域模型 :严禁 将
Model直接暴露给外部。Controller 接收的是Request DTO,返回的是VO,这些都需要通过 MapStruct 与Model进行转换。 - 防腐层 :MapStruct 在
Model和外部世界之间建立了一道"防腐层"。外部的变化(如 API 接口变更)只会影响 DTO/VO 和 Mapper,而不会污染核心的、包含业务逻辑的Model。