Nguyên lý và Ứng dụng MapStruct trong Java

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ápCông cụHiệu năngAn toàn kiểu
Getter/Setter thủ công★★★★★★★★★★
Công cụ phản xạBeanUtils★★
Biên dịch độngDozer★★★★★★
Sinh mã thời gian biên dịchMapStruct★★★★★★★★★★

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ảiChứ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áchMục đích
unmappedTargetPolicy=ERRORBáo lỗi trường đích chưa ánh xạ
nullValueCheckStrategy=ALWAYSLuô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

Thẻ: MapStruct Java JSR-269 Bean-Mapping Lombok

Đăng vào ngày 7 tháng 6 lúc 00:24