mapStruct实体类属性映射工具实现

1. 概述

MapStruct 是一个 Java Bean mapper,用于Java Bean 之间的转换。MapStruct 基于约定优于配置的设计思想,相较于常用的 BeanUtils.copyProperties 它更高效、优雅。
mapStruct 官方网站
官方文档

对比 BeanUtils.copyProperties

  1. 性能优势​
    使用 MapStruct,我们只需要定义映射接口,该库在编译时会自动生成具体实现代码, 生成的代码和手动编写的属性拷贝代码效率一致,​​适合高频调用场景​​(如高并发、大数据量)。。
  2. 类型安全与编译时检查​
  • ​​MapStruct​​:
    ​​ 强类型映射​​:在编译时检查属性名和类型是否匹配,​​避免运行时错误​​
    ​​ 自动提示错误​​:如果源对象或目标对象的属性名/类型不匹配,编译时会直接报错。
  • BeanUtils.copyProperties()​​:
    ​​ 弱类型检查​​:属性名匹配但类型不兼容时(如 String 复制到 Integer),​​运行时抛出异常​​,增加调试成本。
  1. 复杂映射支持​
  • ​​MapStruct​​:
    • ​​自定义转换逻辑​​:支持通过注解或自定义方法实现复杂类型转换(如 Date 转 String、嵌套对象映射)。
    • 多源参数映射​​:支持从多个源对象合并属性到目标对象。
    • 条件映射​​:可配置条件判断是否复制某个属性。

场景对比

​​场景​​​​MapStruct​​​​BeanUtils.copyProperties()​​
高频调用✅ 适合(高性能)❌ 不适用(反射开销大)
简单属性复制✅ 适合(但需要定义接口)✅ 适合(代码简洁)
复杂类型转换✅ 适合(支持自定义逻辑)❌ 不适用
需要编译时检查✅ 适合❌ 不适用
快速原型开发❌ 略繁琐(需定义接口)✅ 适合(快速实现)

快速入门

lombok + mapStruct

maven 依赖

  1. 映入lombok、mapStruct 依赖
  2. 必须搭配lombok-mapstruct-binding插件,以及mapStrust-process
  3. 加入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 );
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值