26.DTO模式:企业级系统的“数据传输特快专递“

“没有DTO的系统就像用敞篷卡车运咖啡豆,不仅效率低下,还可能撒得到处都是” —— 某电商平台架构师的深夜吐槽

一、从订单导出功能说起

假设我们需要实现订单导出功能,典型的错误实现可能是这样的:

// 直接暴露实体对象给表现层
@RestController
public class OrderController {
    
    @Autowired
    private OrderRepository repository;

    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        return repository.findById(id).orElseThrow();
    }
}

// JPA实体直接绑定数据库表结构
@Entity
public class Order {
    @Id
    private Long id;
    private String customerId;
    @Lob
    private String itemsJson;  // 订单项JSON
    private String paymentStatus;
    // 20+个字段...
}

致命问题

  1. 暴露了数据库表结构细节
  2. 传输了不需要的敏感字段(如内部状态码)
  3. 返回了前端无法直接解析的复杂结构
  4. 序列化/反序列化性能堪忧

二、DTO模式四重奏

2.1 基础DTO(手动挡模式)

// 定制化响应结构
public class OrderDTO {
    private String orderNumber;
    private LocalDateTime createTime;
    private List<OrderItemDTO> items;
    private String formattedAmount;
    
    // 手动转换方法
    public static OrderDTO fromEntity(Order order) {
        OrderDTO dto = new OrderDTO();
        dto.setOrderNumber(order.getOrderCode());
        dto.setCreateTime(order.getCreatedAt());
        dto.setItems(convertItems(order.getItems()));
        dto.setFormattedAmount(formatCurrency(order.getTotalAmount()));
        return dto;
    }
    
    // Getter/Setter省略...
}

// 使用示例
@GetMapping("/orders/{id}")
public OrderDTO getOrder(@PathVariable Long id) {
    Order order = repository.findById(id).orElseThrow();
    return OrderDTO.fromEntity(order);
}

2.2 Builder模式强化版(带校验的智能快递箱)

@Builder
@Validated
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20)
    private String username;
    
    @Email
    private String email;
    
    @Pattern(regexp = "1\\d{10}")
    private String mobile;
    
    // 类型转换Builder
    public static UserDTOBuilder fromEntity(User user) {
        return builder()
            .username(user.getLoginName())
            .email(user.getContactEmail())
            .mobile(user.getPhoneNumber());
    }
}

// 使用示例
UserDTO dto = UserDTO.fromEntity(user)
    .email("new@example.com")
    .build();

2.3 MapStruct自动化(DTO流水线)

@Mapper
public interface OrderMapper {
    OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
    
    @Mapping(source = "orderCode", target = "orderNumber")
    @Mapping(source = "items", target = "items")
    @Mapping(target = "formattedAmount", expression = "java(formatAmount(order.getTotalAmount()))")
    OrderDTO toDTO(Order order);

    default String formatAmount(BigDecimal amount) {
        return NumberFormat.getCurrencyInstance(Locale.CHINA).format(amount);
    }
}

// 使用示例
OrderDTO dto = OrderMapper.INSTANCE.toDTO(order);

2.4 动态DTO(灵活的快递包装)

// 使用Map实现动态DTO
public class DynamicDTO extends HashMap<String, Object> {
    
    public DynamicDTO add(String key, Object value) {
        put(key, value);
        return this;
    }
}

// 使用示例
DynamicDTO response = new DynamicDTO()
    .add("orderNumber", order.getOrderCode())
    .add("createTime", DateTimeFormatter.ISO_DATE_TIME.format(order.getCreatedAt()))
    .add("warning", needsAttention ? "该订单需要人工审核" : null);

三、高级场景实战

3.1 多态DTO(处理异构数据)

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type")
@JsonSubTypes({
    @Type(value = BookProductDTO.class, name = "book"),
    @Type(value = DigitalProductDTO.class, name = "digital") 
})
public abstract class ProductDTO {
    private String name;
    private BigDecimal price;
}

public class BookProductDTO extends ProductDTO {
    private String isbn;
    private String author;
}

public class DigitalProductDTO extends ProductDTO {
    private String downloadUrl;
    private LocalDateTime expireTime;
}

3.2 Reactive DTO(响应式数据流)

// WebFlux中的DTO转换
public class OrderReactiveService {
    
    public Flux<OrderDTO> getRecentOrders() {
        return repository.findRecentOrders()
            .map(OrderMapper.INSTANCE::toDTO)
            .delayElements(Duration.ofMillis(100));
    }
}

// 带缓存的响应式DTO
public class CachedProductDTO {
    private static final Map<Long, ProductDTO> cache = new ConcurrentHashMap<>();
    
    public static Mono<ProductDTO> getById(Long id) {
        return Mono.fromSupplier(() -> cache.computeIfAbsent(id, 
            key -> repository.findById(key)
                .map(ProductMapper.INSTANCE::toDTO)
                .block()));
    }
}

四、框架集成秘籍

4.1 Spring Boot + Lombok全家福

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AddressDTO {
    @NotBlank
    private String province;
    private String city;
    private String district;
    @JsonProperty("full_address")
    private String fullAddress;
    
    @JsonIgnore
    public boolean isValid() {
        return StringUtils.hasText(province) && 
               StringUtils.hasText(city);
    }
}

4.2 GraphQL适配方案

// GraphQL DTO定义
@Data
public class ProductGraphQLDTO {
    @GraphQLField
    private String sku;
    
    @GraphQLField
    private String displayName;
    
    @GraphQLField
    @GraphQLNonNull
    private BigDecimal price;
}

// 类型转换器
public class ProductGraphQLMapper {
    public static ProductGraphQLDTO toGraphQL(Product product) {
        return new ProductGraphQLDTO()
            .setSku(product.getStockCode())
            .setDisplayName(product.getDisplayName())
            .setPrice(product.getCurrentPrice());
    }
}

五、性能优化指南

5.1 对象池技术

public class DtoPool {
    private static final int MAX_POOL_SIZE = 100;
    private static final Queue<OrderDTO> pool = new ConcurrentLinkedQueue<>();
    
    public static OrderDTO borrowObject() {
        OrderDTO dto = pool.poll();
        return dto != null ? dto : new OrderDTO();
    }
    
    public static void returnObject(OrderDTO dto) {
        if (pool.size() < MAX_POOL_SIZE) {
            reset(dto);  // 重置对象状态
            pool.offer(dto);
        }
    }
    
    private static void reset(OrderDTO dto) {
        dto.setOrderNumber(null);
        dto.setItems(null);
        // 其他字段重置...
    }
}

5.2 零拷贝序列化

// 使用二进制协议
public class OrderMsgDTO implements Serializable {
    private static final Proto proto = new Proto(OrderMsgDTO.class);
    
    // 字段定义...
    
    public byte[] toBytes() {
        return proto.serialize(this);
    }
    
    public static OrderMsgDTO fromBytes(byte[] data) {
        return proto.deserialize(data);
    }
}
调用
调用
返回
转换
Controller
+getOrder()
Service
+getOrder()
Repository
+findById()
Order
-id: Long
-orderCode: String
-totalAmount: BigDecimal
OrderDTO
-orderNumber: String
-formattedAmount: String
+fromEntity()

六、单元测试妙招

class DtoMappingTest {
    
    @Test
    void shouldCorrectlyMapOrderToDTO() {
        // 准备测试数据
        Order order = new Order()
            .setOrderCode("20230809-001")
            .setTotalAmount(new BigDecimal("199.99"));
        
        // 执行转换
        OrderDTO dto = OrderMapper.INSTANCE.toDTO(order);
        
        // 验证结果
        assertThat(dto.getOrderNumber()).isEqualTo("20230809-001");
        assertThat(dto.getFormattedAmount()).isEqualTo("¥199.99");
    }
    
    @Test
    void shouldHandleNullGracefully() {
        OrderDTO dto = OrderMapper.INSTANCE.toDTO(null);
        assertThat(dto).isNull();
    }
}

终极挑战:如果要设计一个支持动态字段扩展(用户可自定义显示字段)的DTO系统,如何在保证类型安全的同时实现灵活配置?欢迎在评论区晒出你的设计方案!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhysunny

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值