mapStruct实体类属性映射工具实现
mapStruct实体类属性映射实现
- 1. 概述
- 对比 BeanUtils.copyProperties
- 场景对比
- 快速入门
- lombok + mapStruct
- maven 依赖
- 代码实现
- 1. 定义两个要转换的实体类
- 2. 定义转化接口
- 测试
- mapStruct + Spring
- 1. 注册成bean
- 需要使用bean
- 转化规则
- 1. 成员变量名不同时
- 2. 子对象映射
- 3. 数据类型映射
- 自定义映射
- 4. 多对象映射
- 5. 原对象改造
1. 概述
MapStruct
是一个 Java Bean mapper,用于Java Bean 之间的转换。MapStruct 基于约定优于配置的设计思想,相较于常用的 BeanUtils.copyProperties
它更高效、优雅。
mapStruct 官方网站
官方文档
对比 BeanUtils.copyProperties
- 性能优势
使用 MapStruct,我们只需要定义映射接口,该库在编译时会自动生成具体实现代码, 生成的代码和手动编写的属性拷贝代码效率一致,适合高频调用场景(如高并发、大数据量)。。 - 类型安全与编译时检查
- MapStruct:
强类型映射:在编译时检查属性名和类型是否匹配,避免运行时错误。
自动提示错误:如果源对象或目标对象的属性名/类型不匹配,编译时会直接报错。 - BeanUtils.copyProperties():
弱类型检查:属性名匹配但类型不兼容时(如 String 复制到 Integer),运行时抛出异常,增加调试成本。
- 复杂映射支持
- MapStruct:
- 自定义转换逻辑:支持通过注解或自定义方法实现复杂类型转换(如 Date 转 String、嵌套对象映射)。
- 多源参数映射:支持从多个源对象合并属性到目标对象。
- 条件映射:可配置条件判断是否复制某个属性。
场景对比
场景 | MapStruct | BeanUtils.copyProperties() |
---|---|---|
高频调用 | ✅ 适合(高性能) | ❌ 不适用(反射开销大) |
简单属性复制 | ✅ 适合(但需要定义接口) | ✅ 适合(代码简洁) |
复杂类型转换 | ✅ 适合(支持自定义逻辑) | ❌ 不适用 |
需要编译时检查 | ✅ 适合 | ❌ 不适用 |
快速原型开发 | ❌ 略繁琐(需定义接口) | ✅ 适合(快速实现) |
快速入门
lombok + mapStruct
maven 依赖
- 映入lombok、mapStruct 依赖
- 必须搭配lombok-mapstruct-binding插件,以及mapStrust-process
- 加入path配置打包工具
<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><org.mapstruct.version>1.6.3</org.mapstruct.version><org.projectlombok.version>1.18.30</org.projectlombok.version><lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties><dependencies><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${org.mapstruct.version}</version></dependency><!-- lombok dependencies should not end up on classpath --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${org.projectlombok.version}</version><scope>provided</scope></dependency><!-- IntelliJ pre 2018.1.1 requires the mapstruct processor to be present as provided dependency -->
<!-- <dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${org.mapstruct.version}</version><scope>provided</scope></dependency>--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope><version>4.13.1</version></dependency></dependencies><build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><!-- See https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html --><!-- Classpath elements to supply as annotation processor path. If specified, the compiler --><!-- will detect annotation processors only in those classpath elements. If omitted, the --><!-- default classpath is used to detect annotation processors. The detection itself depends --><!-- on the configuration of annotationProcessors. --><!-- --><!-- According to this documentation, the provided dependency processor is not considered! --><annotationProcessorPaths><path><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${org.mapstruct.version}</version></path><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${org.projectlombok.version}</version></path><path><groupId>org.projectlombok</groupId><artifactId>lombok-mapstruct-binding</artifactId><version>${lombok-mapstruct-binding.version}</version></path></annotationProcessorPaths></configuration></plugin></plugins></pluginManagement></build>
代码实现
1. 定义两个要转换的实体类
@Data
public class SourceBo {private String test;
}@Data
public class TargetBo {private Long testing;}
2. 定义转化接口
import com.example.webapp.demos.entity.mapStruct.SourceBo;
import com.example.webapp.demos.entity.mapStruct.TargetBo;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;/*** @Description: 类型转换。* 注意:并不是只能有一个转换的方法,而是我这只写了一个。*/
@Mapper
public interface SourceTargetMapper {SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );@Mapping( target = "testing", source = "test" )TargetBo toTarget( SourceBo s );
}
测试
import com.example.webapp.demos.Config.SourceTargetMapper;
import com.example.webapp.demos.entity.mapStruct.SourceBo;
import com.example.webapp.demos.entity.mapStruct.TargetBo;
import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.assertEquals;/**
* @ClassName : SimpleTest
* @Description :
* @Date: 2025/6/2 13:27
*/public class SimpleTest {@Testpublic void testMapping() {SourceBo s = new SourceBo();s.setTest( "5" );TargetBo t = SourceTargetMapper.MAPPER.toTarget( s );assertEquals( 5, (long) t.getTesting() );}
}
嗯,没错,这样就可以了,是不是so easy!
实现原理呢,同学们可以关注一个 target
目录下生成的.class
文件,它自动生成了我们定义接口的实现类。
如果你的代码出现和lombok的冲突问题。 官网解决方案
mapStruct + Spring
或许你会觉得使用 SourceTargetMapper.MAPPER.xxxx()
不够优雅或者与你使用spring 的bean风格有点融洽,那么你也可以将 SourceTargetMapper 注入spring 容器,来实现代码风格的统一。
1. 注册成bean
那么改动也很简单,修改@Mapper注解,设置componentModel属性值为spring。
@Mapper(componentModel = "spring")
public interface SourceTargetMapper{}
测试类
@Autowiredprivate SourceTargetMapper sourceTargetMapper;@Testpublic void testMapping() {SourceBo s = new SourceBo();s.setTest( "5" );TargetBo t = sourceTargetMapper.toTarget( s );assertEquals( 5, (long) t.getTesting() );}
需要使用bean
反过来,我们需要在mapper中引用Spring容器中的组件该如何实现? 这种情况下,我们需要改用抽象类而非接口了:
@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {@Autowiredprotected SimpleService simpleService;@Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")public abstract SimpleDestination sourceToDestination(SimpleSource source);
}
切记不要将注入的 bean 设置为private
,因为 MapStruct 需要在生成的实现类中访问该对象
编译后,代码就相当于于
@Component // 因为 componentModel = "spring"
public class SimpleDestinationMapperUsingInjectedServiceImpl extends SimpleDestinationMapperUsingInjectedService {@Overridepublic SimpleDestination sourceToDestination(SimpleSource source) {SimpleDestination destination = new SimpleDestination();// 关键行:调用 simpleService 加工 namedestination.setName(simpleService.enrichName(source.getName()));return destination;}
}
转化规则
1. 成员变量名不同时
public class EmployeeDTO {private int employeeId;private String employeeName;// getters and setters
}public class Employee {private int id;private String name;// getters and setters
}@Mapper
public interface EmployeeMapper {@Mapping(target = "employeeId", source = "entity.id")@Mapping(target = "employeeName", source = "entity.name")EmployeeDTO employeeToEmployeeDTO(Employee entity);@Mapping(target = "id", source = "dto.employeeId")@Mapping(target = "name", source = "dto.employeeName")Employee employeeDTOtoEmployee(EmployeeDTO dto);
}
2. 子对象映射
这里我们需要为 Division 与 DivisionDTO 之间的转换添加方法。如果MapStruct检测到需要转换对象类型,并且要转换的方法存在于同一个类中,它将自动使用它。
public class EmployeeDTO {private int employeeId;private String employeeName;private DivisionDTO division;// getters and setters omitted
}public class Employee {private int id;private String name;private Division division;// getters and setters omitted
}
public class Division {private int id;private String name;// default constructor, getters and setters omitted
}public class DivisionDTO {private int Did;private String name;// default constructor, getters and setters omitted
}
public interface SourceTargetMapper {SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );@Mapping( target = "testing", source = "test" )TargetBo toTarget( SourceBo s );/*** * 如果写 Did 编译无法通过*/@Mapping(target = "did",source = "id")DivisionDTO toDivisionDTO( Division s );
}
这个还是有点小问题的,如果写 Did 编译无法通过
3. 数据类型映射
MapStruct还提供了一些开箱即用的隐式类型转换。本例中,我们希望将 String 类型的日期转换为 Date 对象。
有关隐式类型转换的更多详细信息,请查看 MapStruct 官方文档
public class Employee {// other fieldsprivate Date startDt;// getters and setters
}
public class EmployeeDTO {// other fieldsprivate String employeeStartDt;// getters and setters
}@Mapping(target="employeeId", source = "entity.id")
@Mapping(target="employeeName", source = "entity.name")
@Mapping(target="employeeStartDt", source = "entity.startDt",dateFormat = "dd-MM-yyyy HH:mm:ss")
EmployeeDTO employeeToEmployeeDTO(Employee entity);@Mapping(target="id", source="dto.employeeId")
@Mapping(target="name", source="dto.employeeName")
@Mapping(target="startDt", source="dto.employeeStartDt",dateFormat="dd-MM-yyyy HH:mm:ss")
Employee employeeDTOtoEmployee(EmployeeDTO dto);
自定义映射
一些特殊场景 @Mapping 无法满足时,我们需要定制化开发,同时希望保留MapStruct自动生成代码的能力。那我们可以通过创建抽象类实现。
@Mapper
abstract class TransactionMapper {public TransactionDTO toTransactionDTO(Transaction transaction) {TransactionDTO transactionDTO = new TransactionDTO();transactionDTO.setUuid(transaction.getUuid());// BigDecimal 与 long 的自定义转化transactionDTO.setTotalInCents(transaction.getTotal().multiply(new BigDecimal("100")).longValue());return transactionDTO;}public abstract List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions);
}
Collection 与 List 类型之间的转换,仍然交给 MapStruct 完成,我们只需定义接口。
又或者
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ProductMapper {@Mapping(source = "length",target = "length",qualifiedByName = "cmToM")@Mapping(source = "width",target = "width",qualifiedByName = "cmToM")@Mapping(source = "high",target = "high",qualifiedByName = "cmToM")Product productVOToPrduct(ProductVO productVO);@Named("cmToM")default BigDecimal cmToM (BigDecimal oldValue){if (oldValue == null) {return BigDecimal.ZERO;}return oldValue.divide(new BigDecimal("100"));}
}
4. 多对象映射
@Mappings({@Mapping(source ="userVO.name",target = "username"),@Mapping(source ="score.score",target = "score")
})
User userVOToUser(UserVO userVO, Score score);
5. 原对象改造
@Mapping( target = "t.testing", source = "s.test" )void toTarget( SourceBo s,@MappingTarget TargetBo t );