Nhu cầu ánh xạ đối tượng
Bối cảnh sử dụng
Trong kiến trúc phân tầng Java, ánh xạ thuộc tính giữa các đối tượng xuất hiện phổ biến:
- Entity ↔ DTO: Tách biệt dữ liệu giữa tầng lưu trữ và nghiệp vụ
- DTO ↔ VO: Lược bỏ trường không cần thiết giữa dịch vụ và giao diện
- API ↔ Mô hình nội bộ: Cách ly mô hình bên ngoài và bên trong
So sánh giải pháp
| Phương pháp | Công cụ | Hiệu năng | An toàn kiểu |
|---|---|---|---|
| Getter/Setter thủ công | — | ★★★★★ | ★★★★★ |
| Công cụ phản xạ | BeanUtils | ★★ | ★ |
| Biên dịch động | Dozer | ★★★ | ★★★ |
| Sinh mã thời gian biên dịch | MapStruct | ★★★★★ | ★★★★★ |
Bản chất MapStruct
Framework sinh mã sử dụng JSR 269 Annotation Processor với ưu điểm:
- Không phản xạ: Mã sinh tương đương viết tay
- Kiểm tra kiểu tại compile-time
- Không phụ thuộc runtime
Cơ chế hoạt động
Quy trình biên dịch
Giao diện @Mapper │ ▼ Bộ xử lý MapStruct │ ▼ Sinh file UserConverterImpl.java │ ▼ Biên dịch thành bytecode
Lớp sinh ra chứa các lệnh target.setXxx(source.getXxx()) tương tự mã viết tay.
Vị trí mã sinh
target/generated-sources/annotations/<package>/XxxConverterImpl.java
Thực hành cơ bản
Cấu hình Maven
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3</version>
</dependency>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</path>
</annotationProcessorPaths>
Ví dụ chuyển đổi
@Data
public class Account {
private Long id;
private String loginId;
private String contact;
}
@Data
public class AccountView {
private Long id;
private String login;
private String contactInfo;
}
@Mapper(componentModel = "spring")
public interface AccountConverter {
@Mapping(source = "loginId", target = "login")
@Mapping(source = "contact", target = "contactInfo")
AccountView toView(Account source);
}
Chú giải cốt lõi
| Chú giải | Chức năng |
|---|---|
@Mapper(componentModel = "spring") | Khai báo interface |
@Mapping(source/target) | Ánh xạ trường |
@Mapping(dateFormat) | Định dạng ngày |
@Named | Đánh dấu phương thức tùy chỉnh |
@BeanMapping(ignoreByDefault) | Bỏ qua tất cả trường mặc định |
Kịch bản ánh xạ
Tùy biến logic
@Mapper
public interface StatusConverter {
@Mapping(target = "state", source = "code", qualifiedByName = "stateText")
StatusView convert(StatusModel source);
@Named("stateText")
default String toText(int code) {
return switch(code) {
case 1 -> "Hoạt động";
case 2 -> "Tạm khóa";
default -> "Không xác định";
};
}
}
Gộp nhiều nguồn
@Mapper
public interface OrderConverter {
@Mapping(source = "header.id", target = "orderId")
@Mapping(source = "customer.name", target = "clientName")
@Mapping(target = "timestamp", expression = "java(java.time.Instant.now())")
OrderSummary merge(OrderHeader header, Customer customer);
}
Xử lý tập hợp
@Mapper
public interface CatalogConverter {
ProductView itemToView(Product item);
default List<ProductView> convertList(List<Product> items) {
return items.stream()
.filter(Product::isVisible)
.map(this::itemToView)
.toList();
}
}
Thực tiễn sản xuất
Cấu hình toàn cục
@MapperConfig(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface GlobalConfig {}
@Mapper(config = GlobalConfig.class)
public interface ClientConverter {
ClientView toView(Client source);
}
Chính sách kiểm soát
| Chính sách | Mục đích |
|---|---|
unmappedTargetPolicy=ERROR | Báo lỗi trường đích chưa ánh xạ |
nullValueCheckStrategy=ALWAYS | Luôn kiểm tra giá trị null |
Cạm bẫy thường gặp
- Thứ tự xử lý Lombok: Đảm bảo Lombok chạy trước MapStruct
- componentModel="spring": Không dùng cùng
Mappers.getMapper() - Bỏ qua trường không chủ ý: Kích hoạt
unmappedTargetPolicy=ERROR
Tổng kết
MapStruct giải quyết ánh xạ đối tượng với:
- Hiệu năng tương đương mã viết tay
- Kiểm tra kiểu tại thời gian biên dịch
- Hỗ trợ kịch bản phức tạp