理解 @Mapper 注解的核心功能

@Mapper 注解是现代 Java 开发中用于简化对象转换的重要工具,最著名的实现是 MapStruct 框架,它是 Java 中最流行的对象映射框架。它的设计理念是通过编译时代码生成来替代传统的反射机制,从而实现高性能的对象映射转换。

所以,我们从 MapStruct 使用来讲 @Mapper 注解的使用及其对象转换方案。

MapStruct 是一个代码生成器,它的主要功能是在编译时根据注解配置自动生成高性能的映射实现代码,避免了手写转换代码的繁琐和运行时反射的性能损耗。这个工具基于“约定优于配置”的原则,极大地简化了 Java Bean 类型之间的映射实现过程。

在多层架构的应用中,经常需要在不同的对象模型之间进行转换,例如在持久层的实体和传输层的 DTO(Data Transfer Object,数据传输对象)之间。手动编写这种映射代码是一项繁琐且容易出错的任务。MapStruct通过自动化的方式解决了这个问题,它可以在编译时生成映射代码,从而保证了高性能、快速的开发反馈以及严格的错误检查。

@Mapper 注解用于标记一个 接口或抽象类,声明它是一个对象转换器(Mapper),其核心功能是:自动生成代码,将一种对象(如 DTO、VO、Entity)的字段值映射到另一种对象,避免手动编写重复的 setter/getter 逻辑。

1
2
3
4
5
6
7
8
9
// 1. 定义Mapper接口
@Mapper
public interface UserMapper {
UserDTO toDto(UserEntity entity); // Entity → DTO
UserEntity toEntity(UserDTO dto); // DTO → Entity
}

// 2. 编译后,MapStruct自动生成实现类(如UserMapperImpl)
UserDTO userDTO = userMapper.toDto(userEntity); // 自动映射同名字段

具体来说,使用MapStruct时,开发者只需要定义一个接口,并在接口中定义转换方法。然后,MapStruct会自动生成实现这些方法的代码。这些生成的代码使用纯方法调用,因此速度快、类型安全且易于理解。

不同对象之间都是什么意思

相信看到这个文章的应该都知道,但是我还是说一下,我记得我写过全面的文章

  • POJO(Plian Ordinary Java Object):简单普通的Java对象,就是最简单的Java对象,最基本的Java Bean只是在属性上加上 get 和 set 方法,POJO可转化为以下的PO、DTO、VO等,比如说:在service中传递的Java Bean就叫DTO。

  • Entity:Entity就是模型类,通常定义在 model 层里面,相当于MVC的M层,属于数据模型层,一个 Entity 实体类代表一个数据库的一张表。其中的属性定义数据表中的字段,实体类的字段数量 >= 数据库表中需要操作的字段数量,其中,也有另一种叫法PO(Persistent Object) 持久对象,本质上并无差别

  • DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象。属于一种比较底层基础得操作,具体到对某个表得增删改查,换句话说,某个dao一定是和数据库中的某一张表一一对应的,而且其中也只是封装了增删改查得方法。

  • VO(View Object):VO有人理解为Value Object,也有人理解为View Object,我是理解为后者,因为更偏向与表达的意思是表现层对象,用于业务层之间的数据传递,但是是在前端页面层展示被封装的VO数据。

  • DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。

  • DAO(Data Assess Object):数据访问对象,相当于DAO层,mapper 层直接与数据库打交道(执行SQL语句),接口提供给 service 层。一般写在业务层,当业务逻辑比较复杂时,可能会用到比较多的业务对象,这样就可以将多个PO、VO封装成一个BO对象用于数据传递。

核心特性与原理

自动字段映射

  • 同名匹配:默认按字段名自动映射(如 entity.namedto.name)。

  • 类型兼容:支持基本类型、包装类、String、集合等常见类型的转换。

  • 嵌套对象:可递归映射嵌套对象(如 UserDTO.department ←→ UserEntity.department)。

编译时生成代码

  • 原理:MapStruct 在编译期(非运行时反射)生成高效的 Java 映射代码,运行时无需通过反射进行属性拷贝,性能接近手写 setter/getter
  • 优势
    • 零运行时开销:生成的代码直接调用字段赋值,无反射或代理。

    • 易调试:生成的代码可见,便于排查问题。

类型安全

  • MapStruct 在编译时生成映射代码并进行类型检查,如果源对象和目标对象的属性不匹配,会在编译阶段就报错。

  • 而且MapStruct不依赖于任何第三方库,可以很容易地集成到任何项目中。

  • 支持 Spring:通过 @Mapper(componentModel = "spring") 生成 Spring Bean。

  • 组合其他工具:可结合 Lombok、JPA 等使用。

基本工作原理

MapStruct 的核心工作流程是:

  1. 定义接口并使用 @Mapper 注解标记
  2. 在接口中声明映射方法
  3. 在编译时,MapStruct 处理器会生成实现这些方法的具体类
  4. 这些生成的类执行实际的对象转换操作

MapStruct 生成的代码高效且类型安全,通常比手动编写的代码更高效。

MapStruct 的类型转换

MapStruct 支持多种类型转换方式:

  1. 内置转换:自动处理基本类型及其包装类之间的转换
  2. 字符串转换:自动处理字符串与基本类型之间的转换
  3. 枚举转换:自动处理名称相同的枚举值转换
  4. 日期转换:通过 @MappingdateFormat 属性指定格式
  5. 自定义转换:通过 @Named 注解定义自定义转换方法
  6. 使用其他映射器:通过 uses 属性引入其他映射器

使用

导入依赖

在你的pom.xml文件中添加MapStruct的依赖:

  • org.mapstruct:mapstruct:包含了一些必要的注解,例如@Mapping。若我们使用的JDK版本高于1.8,当我们在pom里面导入依赖时候,建议使用坐标是:org.mapstruct:mapstruct-jdk8,这可以帮助我们利用一些Java8的新特性。
  • org.mapstruct:mapstruct-processor:注解处理器,根据注解自动生成mapper的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--mapstruct核心-->
<dependency>
<groupId>org.mapstruct</groupId>
<!-- jdk8以下就使用mapstruct -->
<artifactId>mapstruct-jdk8</artifactId>
<version>1.5.5.Final</version>
</dependency>
<!--mapstruct编译-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</dependency>

一些配置

其中注解的基本使用

@Mapper 基础接口的定义

@Mapper 是 MapStruct 的核心注解,用于标记接口或抽象类作为映射器。MapStruct 会在编译时为这些接口生成实现类,自动生成实现类代码,支持配置全局映射策略。它会定义所有映射方法的入口,适用于任何需要对象转换的场景。

关键属性

  • componentModel:指定组件模型(如 springcdi),用于依赖注入。
  • uses:引入其他映射器或工具类。
  • unmappedTargetPolicy:未映射字段的处理策略(如 IGNOREERROR)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 用户实体类 - 通常对应数据库表结构
public class User {
private Long id;
private String username;
private String email;
private String phone;
private Date createTime;
private Boolean isActive;

// 构造方法、getter、setter 省略...
}

// 用户DTO类 - 用于数据传输,通常会隐藏一些敏感字段
public class UserDto {
private Long id;
private String username;
private String email;
private String phone;
private String createTime; // 注意:这里是String类型,与实体类不同
private Boolean isActive;

// 构造方法、getter、setter 省略...
}

// 定义映射器接口
@Mapper
public interface UserMapper {
// 将User实体转换为UserDto
// MapStruct会自动匹配同名字段进行映射
UserDto toDto(User user);

// 将UserDto转换为User实体
// 支持双向转换,方便在不同层之间传递数据
User toEntity(UserDto userDto);

// 批量转换:将User列表转换为UserDto列表
// MapStruct自动支持集合类型的转换
List<UserDto> toDtoList(List<User> users);

// 批量转换:将UserDto列表转换为User列表
List<User> toEntityList(List<UserDto> dtos);
}

这是最基本的使用方式,MapStruct 会根据字段名自动匹配并生成转换代码。当源对象和目标对象的字段名完全一致时,会自动进行映射。

映射出的代码示例如下,相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserMapperImpl implements UserMapper {
private final AddressMapper addressMapper = new AddressMapperImpl();

@Override
public UserDto toDto(User user) {
if (user == null) {
return null;
}

UserDto userDto = new UserDto();
userDto.setId(user.getId());
userDto.setUsername(user.getUsername());
userDto.setPhone(user.getPhone());
userDto.setEmail(user.getEmail());
userDto.setCreateTime(user.getCreateTime());
userDto.setIsActive(user.getIsActive());

return userDto;
}

// 其他方法实现...
}

在生成的方法实现中,源类型(例如Person)的所有可读属性都将被复制到目标类型(例如PersonDto)的相应属性中:

  • 当一个属性与其目标实体对应的名称相同时,它将被隐式映射。
  • 当属性在目标实体中具有不同的名称时,可以通过@Mapping注释指定其名称。

默认方式获取映射器实例

1

@Mapping 基础接口的定义

当源对象和目标对象的字段名不一致,或者需要特殊处理时,使用 @Mapping 注解。它只会定义单个字段的映射规则,支持属性名转换、表达式、常量等。在字段名不一致、类型转换、动态赋值时使用

关键属性

  • source:源对象属性名(支持嵌套路径,如 user.address.city)。
  • target:目标对象属性名。
  • expression:自定义转换逻辑(如调用方法)。
  • constant:固定值映射。
  • ignore:忽略该字段。
1
2
3
4
5
6
7
8
9
@Mapper
public interface UserMapper {
@Mapping(source = "userName", target = "name")
@Mapping(source = "userAddress", target = "address")
@Mapping(target = "createTime", expression = "java(new java.util.Date())")
@Mapping(target = "status", constant = "ACTIVE")
@Mapping(target = "password", ignore = true)
UserDTO toUserDTO(User user);
}

上述代码的映射相当于如下代码的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public UserDTO toUserDTO(User user) {
if (user == null) {
return null;
}

UserDTO userDTO = new UserDTO();
userDTO.setName(user.getUserName()); // 字段名映射
userDTO.setAddress(user.getUserAddress()); // 字段名映射
userDTO.setCreateTime(new java.util.Date()); // 表达式映射
userDTO.setStatus("ACTIVE"); // 常量映射
// password 字段被忽略,不会被映射

return userDTO;
}

这个注解可以使用@Mappings进行多个 @Mapping 注解的组合,用于定义多个字段映射规则。当需要 需要同时配置多个字段的映射关系时,这么写比较整洁

1
2
3
4
5
6
7
8
9
@Mapper
public interface UserMapper {
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "email", target = "contactInfo"),
@Mapping(source = "birthDate", target = "age", qualifiedByName = "BirthDateToAge")
})
UserDTO toUserDTO(User user);
}

@BeanMapping

用于配置整个方法的映射行为。如忽略未映射字段或空值处理。

关键属性

  • ignoreByDefault:忽略所有未明确映射的字段
  • nullValuePropertyMappingStrategy:处理 null 值的策略
  • nullValueCheckStrategy:null 值检查策略
1
2
3
4
5
6
7
8
9
10
@Mapper
public interface UserMapper {
@BeanMapping(ignoreByDefault = true) // 忽略所有未明确映射的字段
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "fullName")
UserDTO toUserDTO(User user);

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateUserFromDto(UserDTO dto, @MappingTarget User user); // 更新现有对象
}

@BeforeMapping / @AfterMapping

允许在映射前后执行自定义逻辑。在动态修改源/目标对象、填充额外字段、调用外部服务时候使用,一般用于格式化日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Mapper
public interface UserMapper {
@BeforeMapping
default void mapBefore(User user, @MappingTarget UserDTO dto) {
// 在映射前执行
if (user.getStatus() == UserStatus.INACTIVE) {
dto.setInactive(true);
}
}

@AfterMapping
default void mapAfter(User user, @MappingTarget UserDTO dto) {
// 在映射后执行
if (dto.getAge() > 18) {
dto.setAdult(true);
}
}

UserDTO toUserDTO(User user);
}

@IterableMapping

定义集合类型(如 ListSet)元素的映射规则。集合元素类型转换或批量映射时候进行使用

关键属性

  • elementTargetType:目标元素类型。
  • dateFormat:日期格式化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper
public interface UserMapper {
@IterableMapping(qualifiedByName = "userToDto")
List<UserDTO> toDtoList(List<User> users);

@Named("userToDto")
default UserDTO userToDto(User user) {
// 自定义映射逻辑
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setName(user.getName() + " (DTO)");
return dto;
}
}

@MapMapping

用于配置 Map 类型的映射。使用起来和上述@IterableMapping并无明显差距

关键属性:

  • keyDateFormat / valueDateFormat:键或值的日期格式。
  • mapNullToEmpty:空 Map 处理。
1
2
3
4
5
6
7
8
@Mapper
public interface UserMapper {
@MapMapping(keyDateFormat = "yyyy-MM-dd", valueDateFormat = "HH:mm:ss")
Map<String, String> mapToStringMap(Map<Date, Date> dateMap);

@MapMapping(valueTransformation = "toString")
Map<String, String> toStringMap(Map<String, Integer> intMap);
}

@MapperConfig

定义可重用的映射配置,供多个映射器继承。便于统一配置公共策略(如日期格式、空值处理)。

1
2
3
4
5
6
7
8
9
10
11
@MapperConfig(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,
uses = {AddressMapper.class}
)
public interface MyMapperConfig {}

@Mapper(config = MyMapperConfig.class)
public interface UserMapper {
// 使用共享配置的映射器
}

@InheritConfiguration / @InheritInverseConfiguration

重用现有映射配置。继承正向或逆向映射规则,避免重复配置。

  • @InheritConfiguration:正向转换,原mapper → 转换后类型的mapper
  • @InheritInverseConfiguration:逆向转换,转换后类型的mapper → 原mapper
1
2
3
4
5
6
7
8
9
10
11
12
@Mapper
public interface UserMapper {
@Mapping(source = "id", target = "userId")
@Mapping(source = "email", target = "contactInfo")
UserDTO toUserDTO(User user);

@InheritConfiguration(name = "toUserDTO")
List<UserDTO> toUserDTOList(List<User> users);

@InheritInverseConfiguration(name = "toUserDTO")
User toUser(UserDTO dto);
}

@ValueMapping

配置枚举类型的映射。

1
2
3
4
5
6
7
8
9
@Mapper
public interface StatusMapper {
@ValueMapping(source = "INACTIVE", target = "DISABLED")
@ValueMapping(source = "ACTIVE", target = "ENABLED")
@ValueMapping(source = "PENDING", target = "ON_HOLD")
@ValueMapping(source = "DELETED", target = "NULL")
@ValueMapping(source = "NULL", target = "DEFAULT")
UserStatusDto toDto(UserStatus status);
}

此外,MapStruct 还提供了特殊的源/目标值 NULL 和 ANY,可以用于处理源枚举值为 null 或未映射的情况。

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper
public interface EnumMapper {
@ValueMappings({
@ValueMapping(source = "TYPE_A", target = "TYPE_X"),
@ValueMapping(source = "TYPE_B", target = "TYPE_Y"),
@ValueMapping(source = "TYPE_C", target = "TYPE_Z"),
@ValueMapping(source = "NULL", target = "TYPE_Z"),
@ValueMapping(source = "ANY", target = "TYPE_X")
})
TargetEnum sourceToTarget(SourceEnum sourceEnum);
}

@Context

在映射过程中传递上下文信息。 需要外部数据参与映射逻辑(如权限校验)时候使用。

1
2
3
4
5
6
7
8
9
@Mapper
public interface UserMapper {
UserDTO toDto(User user, @Context Locale locale);

default String formatDate(Date date, @Context Locale locale) {
// 使用上下文信息进行格式化
return new SimpleDateFormat("yyyy-MM-dd", locale).format(date);
}
}

@Named

为映射方法命名,便于在其他地方引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Mapper
public interface UserMapper {
@Mapping(source = "birthDate", target = "age", qualifiedByName = "BirthDateToAge")
UserDTO toDto(User user);

@Named("BirthDateToAge")
default Integer birthDateToAge(Date birthDate) {
// 计算年龄的逻辑
if (birthDate == null) {
return null;
}
Calendar birth = Calendar.getInstance();
birth.setTime(birthDate);
Calendar now = Calendar.getInstance();
int age = now.get(Calendar.YEAR) - birth.get(Calendar.YEAR);
if (now.get(Calendar.DAY_OF_YEAR) < birth.get(Calendar.DAY_OF_YEAR)) {
age--;
}
return age;
}
}

属性讲解

componentModel 属性

@Mapper 注解的 componentModel 属性用于指定自动生成的接口实现类的组件类型,这个属性支持四个值:

  • default: 这是默认的情况,mapstruct 不使用任何组件类型, 可以通过Mappers.getMapper(Class)方式获取自动生成的实例对象。

  • spring: 生成的实现类上面会自动添加一个@Component注解,可以通过Spring的 @Autowired或者@Resource方式进行注入

    1
    2
    3
    4
    5
    6
    7
    @Mapper(componentModel = "spring")
    public interface PersonMapper {

    @Mapping(source = "name", target = "fullName")
    PersonDto personToPersonDto(Person person);

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Service
    public class PersonService {

    @Autowired
    private PersonMapper personMapper;

    public PersonDto convert(Person person){
    PersonDto dto = personMapper.personToPersonDto(person);
    return dto;
    }

    }
  • jsr330: 生成的实现类上会添加@javax.inject.Named@Singleton注解,可以通过 @Inject注解获取

nullValueCheckStrategy 属性

是否在生成的实现类中,对每一个属性进行null检查,可选值有两个:ON_IMPLICIT_CONVERSION(默认值)和 ALWAYS

ALWAYS

ALWAYS 表示在赋值之前,对每一个属性进行!= null的检查。

ON_IMPLICIT_CONVERSION

ON_IMPLICIT_CONVERSION则表示直接进行赋值,不进行 != null 判断。

nullValuePropertyMappingStrategy属性

指定当源属性为null或者不存在时目标属性生成值的策略,可选值有:

  • SET_TO_NULL(默认值)

    当源属性为null或者不存在时,设置目标属性的值为null。

  • SET_TO_DEFAULT

    根据源属性的类型,设置指定的默认值。

    • 源属性是List,则默认值是ArrayList;
    • 源属性是String,则默认值是”“;
    • 源属性是基本类型,则默认值是0或者false;
    • 源属性是Map,则默认值是LinkedHashMap;
    • 源属性是数组,则默认值是空数组。
  • IGNORE

    如果源属性是null或者不存在,则不会将null赋值给目标属性,目标属性是什么值就保持什么值。

指定默认值

@Mapper接口类里面的转换方法上添加@Mapping注解的时候,如果需要指定默认值

target() 必须添加,source()可以不添加,则直接使用defaultValue

MapStruct也支持默认值映射,你可以使用@Mapping注解的defaultValue参数来实现这一点:

1
2
3
@Mapping(target = "describe", defaultValue = "默认值")
PersonDTO conver(Person person);

自定义映射

在某些情况下,可能需要自定义字段映射。可以通过在@Mapping注解中使用expressionqualifiedByName参数来实现这一点。

qualifiedByName这个参数允许你引用一个具有@Named注解的方法作为自定义的映射逻辑

1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface OrderMapper {

@Mapping(target = "customerName", source = "customer", qualifiedByName = "fullName")
OrderDto orderToOrderDto(Order order);

@Named("fullName")
default String customerToString(Customer customer) {
return customer.getFirstName() + " " + customer.getLastName();
}
}

在这个例子中,orderToOrderDto方法将Ordercustomer字段(类型为Customer)转换为OrderDtocustomerName字段(类型为String),并且使用了customerToString方法来获取全名。

experssion

expression这个参数允许你使用Java表达式来定义字段映射。这在源和目标字段之间需要一些特定逻辑时非常有用。

注意: 这个属性不能与source()defaultValue()defaultExpression()qualifiedBy()qualifiedByName()constant()一起使用。

1
2
3
4
5
@Mapper
public interface OrderMapper {
@Mapping(target = "orderDate", expression = "java(new java.text.SimpleDateFormat(\"yyyy-MM-dd\").format(order.getCreationDate()))")
OrderDto orderToOrderDto(Order order);
}

在这个例子中,orderToOrderDto方法将OrdercreationDate字段(类型为Date)转换为OrderDto的orderDate字段(类型为String),并且使用了特定的日期格式。

@BeanMapping在映射方法级别提供更详细的配置

从MapStruct 1.5开始,可以使用@BeanMapping注解在MapStruct中用于在映射方法级别提供更详细的配置。这个注解有许多参数可以使用。

  • resultType: 这个参数允许你指定映射方法的返回类型。这在目标类型可以是多个实现类时非常有用。 如果目标类型有多个实现类,并且你希望在映射时使用特定的实现类。通过指定resultType,你可以确保生成的映射代码使用正确的目标类型

    1
    2
    3
    @BeanMapping(resultType = CarDto.class)
    CarDto map(Car car);

  • qualifiedByqualifiedByName: 这两个参数允许你引用一个具有@Qualifier@Named注解的方法作为自定义的映射逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    @BeanMapping(qualifiedByName = "fullName")
    PersonDto personToPersonDto(Person person);

    @Named("fullName")
    default String customerToString(Customer customer) {
    return customer.getFirstName() + " " + customer.getLastName();
    }

  • ignoreByDefault: 这个参数允许你忽略所有未明确映射的属性。然后,你可以使用@Mapping注解来明确需要映射的属性。

    1
    2
    3
    4
    @BeanMapping(ignoreByDefault = true)
    @Mapping(target = "name", source = "fullName")
    PersonDto personToPersonDto(Person person);

  • nullValuePropertyMappingStrategy: 这个参数允许你指定当源属性为null时应如何处理目标属性。例如,你可以选择是否在源属性为null时调用目标的setter方法。

    1
    2
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    PersonDto personToPersonDto(Person person);

使用装饰器增强Mapper

你可以使用装饰器来增强你的Mapper。

MapStruct 装饰器是一种强大的机制,允许你在生成的映射代码基础上添加自定义逻辑,而不需要完全重写映射方法。这在需要处理复杂业务逻辑、添加额外验证或执行副作用操作时特别有用。

装饰器本质上是一个抽象类或接口,它实现了你的映射器接口,并在映射方法执行前后添加自定义逻辑。MapStruct 会在生成的代码中自动调用这些装饰器方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public abstract class UserMapperDecorator implements UserMapper {
// 注入原始的映射器实现
private final UserMapper delegate;

public UserMapperDecorator(UserMapper delegate) {
this.delegate = delegate;
}

// 重写需要增强的方法
@Override
public UserDto toDto(User user) {
// 映射前的逻辑
System.out.println("开始映射用户: " + (user != null ? user.getId() : "null"));

// 调用委托实现进行实际映射
UserDto dto = delegate.toDto(user);

// 映射后的逻辑
if (dto != null) {
// 增强映射结果
dto.setDisplayName(dto.getName() + " (已映射)");
// 可以添加业务逻辑,如计算派生字段
dto.setAge(calculateAge(user.getBirthDate()));
}

return dto;
}

// 自定义方法
private int calculateAge(Date birthDate) {
// 计算年龄的逻辑
if (birthDate == null) return 0;

Calendar birth = Calendar.getInstance();
birth.setTime(birthDate);
Calendar now = Calendar.getInstance();

int age = now.get(Calendar.YEAR) - birth.get(Calendar.YEAR);
if (now.get(Calendar.DAY_OF_YEAR) < birth.get(Calendar.DAY_OF_YEAR)) {
age--;
}

return age;
}
}

当 MapStruct 生成代码时,它会创建一个 UserMapper 的实现类,这个实现类会委托给 UserMapperDecorator 处理。装饰器类中没有重写的方法会由 MapStruct 自动生成。

1
2
3
4
5
6
@Mapper(componentModel = "spring")
@DecoratedWith(UserMapperDecorator.class) // 指定装饰器
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserDto dto);
}

常量映射

@Mapping注解constant属性可以用于将源对象的某个固定值映射到目标对象的属性:

1
2
3
4
5
@Mapper
public interface CarMapper {
@Mapping(target = "carType", constant = "SEDAN")
CarDto carToCarDto(Car car);
}

在这个例子中,carToCarDto方法将会把CarDtocarType字段设置为SEDAN,无论Car对象的实际内容如何。

转换情景与规则

默认方式获取映射器实例

在实现类的时候, 如果属性名称相同, 则会进行对应的转化。通过此种方式, 我们可以快速的编写出转换的方法。、

适用于 Source 和 Target 需要转化的属性是完全相同的,也就是说转换前后不会出现不同的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 用户实体类 - 通常对应数据库表结构
public class User {
private Long id;
private String username;
private String email;
private String phone;
private Date createTime;
private Boolean isActive;

// 构造方法、getter、setter 省略...
}

// 用户DTO类 - 用于数据传输,通常会隐藏一些敏感字段
public class UserDto {
private Long id;
private String username;
private String email;
private String phone;
private String createTime; // 注意:这里是String类型,与实体类不同
private Boolean isActive;

// 构造方法、getter、setter 省略...
}

// 定义映射器接口
@Mapper
public interface UserMapper {
// 将User实体转换为UserDto
// MapStruct会自动匹配同名字段进行映射
UserDto toDto(User user);

// 将UserDto转换为User实体
// 支持双向转换,方便在不同层之间传递数据
User toEntity(UserDto userDto);

// 批量转换:将User列表转换为UserDto列表
// MapStruct自动支持集合类型的转换
List<UserDto> toDtoList(List<User> users);

// 批量转换:将UserDto列表转换为User列表
List<User> toEntityList(List<UserDto> dtos);
}

在需要进行类型转换的地方,使用在@Mapper映射器接口中定义的方法就会自动进行转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class UserService {

// 通过Mappers.getMapper()获取自动生成的实例
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);

public UserDto getUserById(Long id) {
// 假设从数据库查询到User实体
User user = userRepository.findById(id);

// 使用映射器将实体转换为DTO
// 编译后生成的代码会直接调用setter/getter方法,性能接近手写代码
return userMapper.toDto(user);
}

public List<UserDto> getAllUsers() {
List<User> users = userRepository.findAll();

// 批量转换,MapStruct会自动处理集合中的每个元素
return userMapper.toDtoList(users);
}
}

编译后,MapStruct 会生成类似以下的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 这是MapStruct自动生成的实现类,开发者无需编写
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-01-01T10:00:00+0800",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 11"
)
public class UserMapperImpl implements UserMapper {

@Override
public UserDto toDto(User user) {
if (user == null) {
return null; // 自动进行null检查
}

UserDto userDto = new UserDto();

// 直接调用getter/setter方法,无反射开销
userDto.setId(user.getId());
userDto.setUsername(user.getUsername());
userDto.setEmail(user.getEmail());
userDto.setPhone(user.getPhone());
userDto.setIsActive(user.getIsActive());

// 对于Date到String的转换,MapStruct会自动处理
if (user.getCreateTime() != null) {
userDto.setCreateTime(user.getCreateTime().toString());
}

return userDto;
}

@Override
public User toEntity(UserDto userDto) {
if (userDto == null) {
return null;
}

User user = new User();

user.setId(userDto.getId());
user.setUsername(userDto.getUsername());
user.setEmail(userDto.getEmail());
user.setPhone(userDto.getPhone());
user.setIsActive(userDto.getIsActive());

// String到Date的转换需要特殊处理(后面会介绍如何配置)
// 这里简化处理...

return user;
}

@Override
public List<UserDto> toDtoList(List<User> users) {
if (users == null) {
return null;
}

List<UserDto> list = new ArrayList<UserDto>(users.size());
// 遍历集合,逐个转换
for (User user : users) {
list.add(toDto(user));
}

return list;
}

// toEntityList方法实现类似...
}

字段映射配置

属性名不相同, 在需要进行互相转化的时候, 则我们可以通过 @Mapping 注解来进行转化.

这适用于当源对象和目标对象的字段名不一致,或者需要特殊的转换逻辑时

1
2
3
4
5
6
7
@Mapper
public interface ProductMapper {
@Mapping(source = "productName", target = "name")
@Mapping(source = "productPrice", target = "price")
@Mapping(source = "createdTime", target = "createDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
ProductDto toDto(Product product);
}

有时候我们不希望某些字段参与映射,可以使用 ignore = true 来忽略它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 订单实体类
public class Order {
private Long orderId;
private String orderNumber;
private BigDecimal totalAmount;
private Date createTime;
private String internalNotes; // 内部备注,不对外暴露
private Integer version; // 版本号,乐观锁字段
private String auditLog; // 审计日志,敏感信息

// getter、setter省略...
}

// 订单DTO类
public class OrderDto {
private Long orderId;
private String orderNumber;
private BigDecimal totalAmount;
private String createTime;
// 注意:没有internalNotes、version、auditLog字段

// getter、setter省略...
}

@Mapper
public interface OrderMapper {

// 忽略敏感字段和内部字段
@Mapping(target = "internalNotes", ignore = true) // 忽略内部备注
@Mapping(target = "version", ignore = true) // 忽略版本字段
@Mapping(target = "auditLog", ignore = true) // 忽略审计日志
@Mapping(source = "createTime", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
OrderDto toDto(Order order);

// DTO转实体时,被忽略的字段不会被设置,保持默认值或null
@Mapping(source = "createTime", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
Order toEntity(OrderDto dto);
}

嵌套对象映射

这是比较复杂的映射场景,MapStruct 支持嵌套对象的自动映射,当对象包含其他对象时,会递归进行映射:

当源对象和目标对象都包含嵌套结构时,MapStruct 会自动递归处理这些嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 部门实体
public class Department {
private Long deptId;
private String deptName;
private String location;

// getter、setter省略...
}

// 部门DTO
public class DepartmentDto {
private Long deptId;
private String deptName;
private String location;

// getter、setter省略...
}

// 员工实体 - 包含部门对象
public class Employee {
private Long empId;
private String empName;
private String email;
private Department department; // 嵌套的部门对象

// getter、setter省略...
}

// 员工DTO - 同样包含部门DTO对象
public class EmployeeDto {
private Long empId;
private String empName;
private String email;
private DepartmentDto department; // 嵌套的部门DTO对象

// getter、setter省略...
}

// 需要先定义部门映射器
@Mapper
public interface DepartmentMapper {
DepartmentDto toDto(Department department);
Department toEntity(DepartmentDto dto);
}

// 员工映射器 - 会自动处理嵌套对象
@Mapper(uses = DepartmentMapper.class) // 指定依赖的映射器
public interface EmployeeMapper {

// MapStruct会自动检测到department字段是嵌套对象
// 并使用DepartmentMapper来进行转换
EmployeeDto toDto(Employee employee);

Employee toEntity(EmployeeDto dto);
}

此时嵌套映射的工作原理

  1. MapStruct 会自动检测到 Employee 中的 department 字段是一个 Department 对象
  2. 它会查找是否有可用的 DepartmentDepartmentDto 的映射方法
  3. 如果找到了(通过 uses 属性指定的 DepartmentMapper),就会使用该映射器处理嵌套对象
  4. 如果没找到,会尝试自动生成映射逻辑

如果其中遇到了更复杂的嵌套处理路径,我们可以使用 @Mapping(source = "嵌套字段", target = "目标字段")

1
2
3
4
5
@Mapper
public interface OrderMapper {
@Mapping(source = "customer.address.city", target = "deliveryCity")
OrderDto toDto(Order order);
}

在这个例子中,toDto方法将customeraddress.city属性映射到OrderDtodeliveryCity属性。

使用自定义的转换

有时候,对于某些类型, 无法通过代码生成器的形式来进行处理。 那么, 就需要自定义的方法来进行转换。或者当MapStruct的默认转换逻辑无法满足复杂业务需求时,我们也可以在映射器中定义自定义方法:

这时候, 我们可以在接口(同一个接口, 后续还有调用别的 Mapper 的方法)中定义默认方法(Java8及之后)。

自定义转换方法可以是:

  1. 同一映射器接口中的默认方法
  2. 其他映射器中的方法(通过 uses 属性引用)
  3. 静态工具类中的方法(通过 uses 属性引用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Mapper
public interface UserMapper {

// 基本映射方法,引用自定义转换
@Mapping(source = "createTime", target = "createDate", dateFormat = "yyyy-MM-dd")
@Mapping(source = "status", target = "statusText", qualifiedByName = "statusToText")
@Mapping(target = "fullName", expression = "java(formatFullName(user.getFirstName(), user.getLastName()))")
UserDto toDto(User user);

// 自定义转换方法:将状态码转换为文本描述
@Named("statusToText") // 指定方法名,供@Mapping使用
default String statusToText(Integer status) {
if (status == null) {
return "未知";
}
switch (status) {
case 1:
return "激活";
case 0:
return "禁用";
case -1:
return "删除";
default:
return "未知状态";
}
}

// 自定义方法:处理复杂的业务逻辑
default String formatFullName(String firstName, String lastName) {
if (firstName == null && lastName == null) {
return null;
}
if (firstName == null) {
return lastName;
}
if (lastName == null) {
return firstName;
}
return firstName + " " + lastName;
}
}

使用外部工具类进行自定义转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 工具类
public class DateFormatters {
public static String formatDate(Date date) {
if (date == null) return null;
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return format.format(date);
}
}

// 映射器
@Mapper(uses = DateFormatters.class)
public interface UserMapper {
@Mapping(source = "birthDate", target = "formattedBirthDate", qualifiedByName = "formatDate")
UserDto toDto(User user);
}

也可以使用 @Context 传递上下文信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper
public interface UserMapper {
UserDto toDto(User user, @Context Locale locale);

default String formatDate(Date date, @Context Locale locale) {
if (date == null) return null;
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", locale);
return format.format(date);
}
}

// 使用示例
Locale locale = Locale.CHINA;
UserDto dto = userMapper.toDto(user, locale);

多转一

我们在实际的业务中少不了将多个对象转换成一个的场景。 MapStruct 当然也支持多转一的操作。

  • 将多个实体对象合并为一个 DTO
  • 将表单数据和查询参数合并为一个业务对象
  • 将配置信息和运行时数据合并为一个完整的上下文对象

MapStruct 通过方法参数实现多对象映射,只需要在映射方法中声明多个源对象参数即可

1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface CarMapper {
// 多个源对象映射到一个目标对象
@Mapping(source = "car.make", target = "manufacturer")
@Mapping(source = "car.numberOfSeats", target = "seatCount")
@Mapping(source = "owner.name", target = "ownerName")
CarDto carAndOwnerToCarDto(Car car, Person owner);

// 支持更多源对象
CarFullInfoDto carAndOwnerAndConfigToCarFullInfoDto(Car car, Person owner, CarConfig config);
}

字段冲突处理

当多个源对象包含同名属性时,MapStruct 默认使用最后一个参数中的值:

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface ConflictMapper {
// 如果Source1和Source2都有name属性,将使用Source2中的值
Target mergeSources(Source1 source1, Source2 source2);

// 显式指定使用哪个源对象的属性
@Mapping(source = "source1.name", target = "nameFromSource1")
@Mapping(source = "source2.name", target = "nameFromSource2")
TargetWithBothNames mergeSourcesWithBothNames(Source1 source1, Source2 source2);
}

使用 @MappingTarget 处理已存在的目标对象

1
2
3
4
5
@Mapper
public interface UpdateMapper {
// 将source1和source2的属性合并到已存在的target对象中
void updateTarget(@MappingTarget Target target, Source1 source1, Source2 source2);
}

更新现有 bean 对象

在实际应用中,我们经常需要更新一个已存在的对象,而不是创建一个新对象。MapStruct 提供了专门的机制来处理这种情况。

使用 @MappingTarget 注解

1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface UserMapper {
// 将DTO中的属性更新到已存在的实体对象中
void updateUserFromDto(UserDto userDto, @MappingTarget User user);

// 支持多个源对象更新
void updateUserFromDtoAndConfig(UserDto userDto, UserConfig config, @MappingTarget User user);

// 集合更新
void updateUserListFromDtoList(List<UserDto> userDtos, @MappingTarget List<User> users);
}

可以使用 @BeanMapping 进行更详细的配置更新策略

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper
public interface CarMapper {
// 忽略null值更新
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);

// 忽略未映射的属性
@BeanMapping(ignoreByDefault = true)
@Mapping(source = "name", target = "name")
@Mapping(source = "price", target = "price")
void updateCarWithSelectedFields(CarDto carDto, @MappingTarget Car car);
}