mapstruct-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MapStruct Patterns for Jakarta EE

适用于Jakarta EE的MapStruct模式

Best practices for using MapStruct with constructor-based mapping to achieve compile-time safety. When constructors change, mappings fail to compile — no runtime surprises.
使用MapStruct进行基于构造函数的映射以实现编译时安全性的最佳实践。当构造函数发生变更时,映射会编译失败——不会出现运行时意外问题。

Core Philosophy

核心理念

Use constructors, not setters. This gives you compile-time errors when fields change.
Records naturally enforce this. For mutable entities, use the
@Default
annotation.
使用构造函数,而非setter方法。这样当字段发生变更时,你会得到编译时错误。
Records(Java记录类)天然支持这一点。对于可变实体,可使用
@Default
注解。

CDI Setup

CDI配置

java
@Mapper(componentModel = "cdi")  // CDI injection
public interface OrderMapper {
    OrderResponse toResponse(Order order);
}
java
@Mapper(componentModel = "cdi")  // CDI注入
public interface OrderMapper {
    OrderResponse toResponse(Order order);
}

The @Default Annotation Trick

@Default注解技巧

MapStruct uses any annotation named
@Default
to select the constructor. Create your own:
java
package com.example.mapstruct;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.CLASS)
public @interface Default {
}
MapStruct会使用任何名为
@Default
的注解来选择构造函数。你可以自定义一个:
java
package com.example.mapstruct;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.CLASS)
public @interface Default {
}

Usage on Mutable Entities

在可变实体上的使用

java
@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;
    private String customerId;
    private BigDecimal total;
    private OrderStatus status;

    // JPA needs this
    protected Order() {}

    // MapStruct uses this - CHANGE HERE = COMPILER ERROR in mapper
    @Default
    public Order(String customerId, BigDecimal total, OrderStatus status) {
        this.customerId = customerId;
        this.total = total;
        this.status = status;
    }
}
java
@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;
    private String customerId;
    private BigDecimal total;
    private OrderStatus status;

    // JPA需要此无参构造函数
    protected Order() {}

    // MapStruct会使用此构造函数——此处变更会导致映射器编译错误
    @Default
    public Order(String customerId, BigDecimal total, OrderStatus status) {
        this.customerId = customerId;
        this.total = total;
        this.status = status;
    }
}

Records (Ideal Case)

Records(理想场景)

Records automatically work with constructor mapping:
java
// No @Default needed - single constructor
public record OrderResponse(
    String orderId,
    String customerId,
    String total,
    String status
) {}

@Mapper(componentModel = "cdi")
public interface OrderMapper {

    @Mapping(target = "orderId", source = "id")
    @Mapping(target = "total", expression = "java(order.getTotal().toString())")
    OrderResponse toResponse(Order order);
}
Records会自动支持构造函数映射:
java
// 无需@Default注解——单个构造函数即可
public record OrderResponse(
    String orderId,
    String customerId,
    String total,
    String status
) {}

@Mapper(componentModel = "cdi")
public interface OrderMapper {

    @Mapping(target = "orderId", source = "id")
    @Mapping(target = "total", expression = "java(order.getTotal().toString())")
    OrderResponse toResponse(Order order);
}

Key Patterns

关键模式

1. Constructor-Based Mapping

1. 基于构造函数的映射

java
@Mapper(componentModel = "cdi")
public interface CustomerMapper {

    // MapStruct uses Customer constructor, fail if signature changes
    Customer toEntity(CreateCustomerRequest request);

    // MapStruct uses CustomerResponse constructor
    CustomerResponse toResponse(Customer customer);
}
java
@Mapper(componentModel = "cdi")
public interface CustomerMapper {

    // MapStruct会使用Customer的构造函数,若签名变更则会失败
    Customer toEntity(CreateCustomerRequest request);

    // MapStruct会使用CustomerResponse的构造函数
    CustomerResponse toResponse(Customer customer);
}

2. Custom @Default for Entities

2. 为实体自定义@Default注解

java
@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private BigDecimal price;
    private String category;

    protected Product() {}

    @Default  // Your custom annotation
    public Product(String name, BigDecimal price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
}
java
@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private BigDecimal price;
    private String category;

    protected Product() {}

    @Default  // 你的自定义注解
    public Product(String name, BigDecimal price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
}

Anti-Pattern: Setter-Based Mapping

反模式:基于Setter的映射

java
// ❌ Can add field to DTO, forget mapper, get null at runtime
public class OrderDTO {
    private String id;
    private String status;
    private String newField;  // Added later, no error!

    // Just setters...
}

// ✓ Add field to constructor = compiler error in mapper
public record OrderDTO(String id, String status, String newField) {}
java
// ❌ 可为DTO添加字段,却忘记更新映射器,导致运行时出现null值
public class OrderDTO {
    private String id;
    private String status;
    private String newField;  // 后续添加的字段,无错误提示!

    // 仅包含setter方法...
}

// ✓ 向构造函数添加字段会导致映射器编译错误
public record OrderDTO(String id, String status, String newField) {}

Compile-Time Safety Benefit

编译时安全性的优势

java
// Before: Record has 3 fields
public record OrderResponse(String id, String status, String total) {}

// After: Added customerName field
public record OrderResponse(String id, String status, String total, String customerName) {}

// Mapper now FAILS TO COMPILE until you add the mapping:
@Mapper(componentModel = "cdi")
public interface OrderMapper {
    @Mapping(target = "customerName", source = "customer.name")  // Must add this
    OrderResponse toResponse(Order order);
}
java
// 之前:Record包含3个字段
public record OrderResponse(String id, String status, String total) {}

// 之后:添加了customerName字段
public record OrderResponse(String id, String status, String total, String customerName) {}

// 此时映射器会编译失败,直到你添加对应的映射配置:
@Mapper(componentModel = "cdi")
public interface OrderMapper {
    @Mapping(target = "customerName", source = "customer.name")  // 必须添加此配置
    OrderResponse toResponse(Order order);
}

Cookbook Index

指南索引

Setup & Configuration

配置与设置

  • cdi-setup - CDI/MicroProfile setup
  • default-annotation - Custom @Default annotation
  • cdi-setup - CDI/MicroProfile配置
  • default-annotation - 自定义@Default注解

Mapping Patterns

映射模式

  • constructor-mapping - Constructor-based mapping
  • record-mapping - Java Records mapping
  • entity-mapping - JPA entity mapping
  • constructor-mapping - 基于构造函数的映射
  • record-mapping - Java Records映射
  • entity-mapping - JPA实体映射