目录
“没有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+个字段...
}
致命问题:
- 暴露了数据库表结构细节
- 传输了不需要的敏感字段(如内部状态码)
- 返回了前端无法直接解析的复杂结构
- 序列化/反序列化性能堪忧
二、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);
}
}
六、单元测试妙招
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系统,如何在保证类型安全的同时实现灵活配置?欢迎在评论区晒出你的设计方案!