mapStruct实体类属性映射实现
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 {
@Test
public 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{
}
测试类
@Autowired
private SourceTargetMapper sourceTargetMapper;
@Test
public 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 {
@Autowired
protected 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 {
@Override
public SimpleDestination sourceToDestination(SimpleSource source) {
SimpleDestination destination = new SimpleDestination();
// 关键行:调用 simpleService 加工 name
destination.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 = "employeeId", source = "entity.id")
@Mapping(target = "employeeName", source = "entity.name")
EmployeeDTO employeeToEmployeeDTO(Employee entity);
/**
*
* 如果写 Did 编译无法通过
*/
@Mapping(target = "did",source = "id")
DivisionDTO toDivisionDTO( Division s );
}
这个还是有点小问题的,如果写 Did 编译无法通过
3. 数据类型映射
MapStruct还提供了一些开箱即用的隐式类型转换。本例中,我们希望将 String 类型的日期转换为 Date 对象。
有关隐式类型转换的更多详细信息,请查看 MapStruct 官方文档
public class Employee {
// other fields
private Date startDt;
// getters and setters
}
public class EmployeeDTO {
// other fields
private 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 );