理解 @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 @Mapper public interface UserMapper { UserDTO toDto (UserEntity entity) ; UserEntity toEntity (UserDTO dto) ; } 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.name
→ dto.name
)。
类型兼容 :支持基本类型、包装类、String、集合等常见类型的转换。
嵌套对象 :可递归映射嵌套对象(如
UserDTO.department
←→
UserEntity.department
)。
编译时生成代码
原理 :MapStruct 在编译期(非运行时反射)生成高效的
Java 映射代码,运行时无需通过反射进行属性拷贝,性能接近手写
setter/getter
。
优势 :
类型安全
MapStruct
在编译时生成映射代码并进行类型检查,如果源对象和目标对象的属性不匹配,会在编译阶段就报错。
而且MapStruct不依赖于任何第三方库,可以很容易地集成到任何项目中。
支持 Spring :通过
@Mapper(componentModel = "spring")
生成 Spring
Bean。
组合其他工具 :可结合 Lombok、JPA
等使用。
基本工作原理
MapStruct 的核心工作流程是:
定义接口并使用 @Mapper
注解标记
在接口中声明映射方法
在编译时,MapStruct 处理器会生成实现这些方法的具体类
这些生成的类执行实际的对象转换操作
MapStruct 生成的代码高效且类型安全,通常比手动编写的代码更高效。
MapStruct 的类型转换
MapStruct 支持多种类型转换方式:
内置转换 :自动处理基本类型及其包装类之间的转换
字符串转换 :自动处理字符串与基本类型之间的转换
枚举转换 :自动处理名称相同的枚举值转换
日期转换 :通过 @Mapping
的
dateFormat
属性指定格式
自定义转换 :通过 @Named
注解定义自定义转换方法
使用其他映射器 :通过 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 <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-jdk8</artifactId > <version > 1.5.5.Final</version > </dependency > <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct-processor</artifactId > <version > 1.5.5.Final</version > </dependency >
一些配置
其中注解的基本使用
@Mapper 基础接口的定义
@Mapper
是 MapStruct
的核心注解,用于标记接口或抽象类作为映射器。MapStruct
会在编译时为这些接口生成实现类,自动生成实现类代码,支持配置全局映射策略。它会定义所有映射方法的入口,适用于任何需要对象转换的场景。
关键属性 :
componentModel
:指定组件模型(如
spring
、cdi
),用于依赖注入。
uses
:引入其他映射器或工具类。
unmappedTargetPolicy
:未映射字段的处理策略(如
IGNORE
或 ERROR
)。
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; } public class UserDto { private Long id; private String username; private String email; private String phone; private String createTime; private Boolean isActive; } @Mapper public interface UserMapper { UserDto toDto (User user) ; User toEntity (UserDto userDto) ; List<UserDto> toDtoList (List<User> users) ; 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注释指定其名称。
默认方式获取映射器实例
@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" ); 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
定义集合类型(如
List
、Set
)元素的映射规则。集合元素类型转换或批量映射时候进行使用
关键属性 :
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或者不存在时目标属性生成值的策略,可选值有:
指定默认值
在@Mapper
接口类里面的转换方法上添加@Mapping
注解的时候,如果需要指定默认值
target()
必须添加,source()
可以不添加,则直接使用defaultValue
MapStruct
也支持默认值映射,你可以使用@Mapping
注解的defaultValue
参数来实现这一点:
1 2 3 @Mapping(target = "describe", defaultValue = "默认值") PersonDTO conver (Person person) ;
自定义映射
在某些情况下,可能需要自定义字段映射。可以通过在@Mapping注解中使用expression
或qualifiedByName
参数来实现这一点。
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
方法将Order
的customer
字段(类型为Customer
)转换为OrderDto
的customerName
字段(类型为String
),并且使用了customerToString
方法来获取全名。
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
方法将Order
的creationDate
字段(类型为Date)转换为OrderDto的orderDate
字段(类型为String),并且使用了特定的日期格式。
@BeanMapping
在映射方法级别提供更详细的配置
从MapStruct
1.5开始,可以使用@BeanMapping
注解在MapStruct中用于在映射方法级别提供更详细的配置。这个注解有许多参数可以使用。
resultType
:
这个参数允许你指定映射方法的返回类型。这在目标类型可以是多个实现类时非常有用。
如果目标类型有多个实现类,并且你希望在映射时使用特定的实现类。通过指定resultType,你可以确保生成的映射代码使用正确的目标类型
1 2 3 @BeanMapping(resultType = CarDto.class) CarDto map (Car car) ;
qualifiedBy
和qualifiedByName
:
这两个参数允许你引用一个具有@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
方法将会把CarDto
的carType
字段设置为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; } public class UserDto { private Long id; private String username; private String email; private String phone; private String createTime; private Boolean isActive; } @Mapper public interface UserMapper { UserDto toDto (User user) ; User toEntity (UserDto userDto) ; List<UserDto> toDtoList (List<User> users) ; 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 { private final UserMapper userMapper = Mappers.getMapper(UserMapper.class); public UserDto getUserById (Long id) { User user = userRepository.findById(id); return userMapper.toDto(user); } public List<UserDto> getAllUsers () { List<User> users = userRepository.findAll(); 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 @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 ; } UserDto userDto = new UserDto (); userDto.setId(user.getId()); userDto.setUsername(user.getUsername()); userDto.setEmail(user.getEmail()); userDto.setPhone(user.getPhone()); userDto.setIsActive(user.getIsActive()); 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()); 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; } }
字段映射配置
属性名不相同, 在需要进行互相转化的时候, 则我们可以通过
@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; } public class OrderDto { private Long orderId; private String orderNumber; private BigDecimal totalAmount; private String createTime; } @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) ; @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; } public class DepartmentDto { private Long deptId; private String deptName; private String location; } public class Employee { private Long empId; private String empName; private String email; private Department department; } public class EmployeeDto { private Long empId; private String empName; private String email; private DepartmentDto department; } @Mapper public interface DepartmentMapper { DepartmentDto toDto (Department department) ; Department toEntity (DepartmentDto dto) ; } @Mapper(uses = DepartmentMapper.class) public interface EmployeeMapper { EmployeeDto toDto (Employee employee) ; Employee toEntity (EmployeeDto dto) ; }
此时嵌套映射的工作原理
MapStruct 会自动检测到 Employee
中的
department
字段是一个 Department
对象
它会查找是否有可用的 Department
到
DepartmentDto
的映射方法
如果找到了(通过 uses
属性指定的
DepartmentMapper
),就会使用该映射器处理嵌套对象
如果没找到,会尝试自动生成映射逻辑
如果其中遇到了更复杂的嵌套处理路径,我们可以使用
@Mapping(source = "嵌套字段", target = "目标字段")
1 2 3 4 5 @Mapper public interface OrderMapper { @Mapping(source = "customer.address.city", target = "deliveryCity") OrderDto toDto (Order order) ; }
在这个例子中,toDto
方法将customer
的address.city
属性映射到OrderDto
的deliveryCity
属性。
使用自定义的转换
有时候,对于某些类型, 无法通过代码生成器的形式来进行处理。 那么,
就需要自定义的方法来进行转换。或者当MapStruct的默认转换逻辑无法满足复杂业务需求时,我们也可以在映射器中定义自定义方法:
这时候, 我们可以在接口(同一个接口, 后续还有调用别的 Mapper
的方法)中定义默认方法(Java8及之后)。
自定义转换方法可以是:
同一映射器接口中的默认方法
其他映射器中的方法(通过 uses
属性引用)
静态工具类中的方法(通过 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") 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 { 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 { 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 { 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 { @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) ; }