MapStruct 介绍与使用指南

什么是 MapStruct?

MapStruct 是一个 Java 注解处理器,用于生成类型安全的 bean 映射代码。它通过在编译时生成映射代码,避免了手动编写繁琐的对象转换代码,同时保证了高性能(因为生成的代码是普通的 Java 方法调用,没有反射开销)。

主要特点

  • 高性能:编译时生成代码,无反射开销
  • 类型安全:编译器会检查映射是否正确
  • 易于调试:生成的代码是普通 Java 代码,可以轻松调试
  • 配置灵活:支持自定义映射规则
  • 丰富的注解:提供多种注解满足不同映射需求
  • 支持多级映射

实践

1.引入依赖

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.5.Final</version> <!-- 使用最新版本 -->
    </dependency>
</dependencies>

<build>
    <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>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.简单类相互转换

1.两个user类
@Data
@I18nClassAnnotation
public class User {
    public String id;
    public String name;
    public String desc;
    public Integer age;
    public BigDecimal count;
    private List<String> a;
}

@Data
@Table(name = "`user`")
public class UserDTO {
    private int id;
    private String name;
    private Date createdTime;
    private BigDecimal number;
    private List<String> a;
}
2.添加UserMapperStruct
package com.example.demo.mapstruct;

import com.example.demo.domain.User;
import com.example.demo.domain.po.UserDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

import java.util.List;

/**
 * @author zhouxy
 * @date 2025年04月10日 16:34
 */
@Mapper
public interface UserMapperStruct {
    UserMapperStruct INSTANCE = Mappers.getMapper(UserMapperStruct.class);

    /**
     * 单个对象映射
     *
     * @author zhouxy
     * @date 2025/4/11 10:26
     */
    //属性转换
    @Mapping(source = "count", target = "number")
    //忽略某个属性赋值
    @Mapping(target = "id", ignore = true)
    UserDTO userToDTO(User user);

    /**
     * 集合映射
     *
     * @author zhouxy
     * @date 2025/4/10 17:27
     */
    //属性转换
    @Mapping(source = "count", target = "number")
    //忽略某个属性赋值
    @Mapping(target = "id", ignore = true)
    List<UserDTO> usersToDTOs(List<User> users);
}
3.测试
 @Test
 public void testMapping() {
     User user = new User();
     user.setId("3");
     user.setName("张三");
     user.setCount(BigDecimal.valueOf(2l));
     user.setA(Arrays.asList("2","李四"));

     UserDTO userDTO = UserMapperStruct.INSTANCE.userToDTO(user);
     log.info("{}", JSON.toJSONString(userDTO));
 }
 测试结果:{"a":["2","李四"],"id":0,"name":"张三","number":2}


 @Test
 public void testListMapping() {
     User user = new User();
     user.setId("3");
     user.setName("张三");
     user.setCount(BigDecimal.valueOf(2l));
     user.setA(Arrays.asList("2","李四"));

     List<User> users = new ArrayList<>();
     users.add(user);

     List<UserDTO> userDTOs = UserMapperStruct.INSTANCE.usersToDTOs(users);
     log.info("{}", JSON.toJSONString(userDTOs));
 }
 测试结果:[{"a":["2","李四"],"id":0,"name":"张三","number":2}]

3.复杂类相互转换

场景,代码中有一段是将各种机票改期数据整合出改期单的价格信息。如图:

//初始化价格详情
List<QueryChangeDetailResp.OrderPriceVo> prices = new ArrayList<>();
buildChangePrices(adtChangeDetails, "成人", currentChangeInfo, prices);
buildChangePrices(chdChangeDetails, "儿童", currentChangeInfo, prices);
buildChangePrices(infChangeDetails, "婴儿", currentChangeInfo, prices);
currentChangeInfo.setPrices(prices);
//参数说明

转换代码

private static void buildChangePrices(
List<TicketChangeDetail> changeDetails,
String type,
QueryChangeDetailResp.ChangeDetail currentChangeInfo,
List<QueryChangeDetailResp.OrderPriceVo> prices) {
        if (CollectionUtils.isNotEmpty(changeDetails)) {
            StringBuilder prdType = new StringBuilder();
            prdType.append(type);
            prdType.append("(");
            prdType.append(currentChangeInfo.getOrgTrip().getTripType());
            prdType.append("/");
            prdType.append(currentChangeInfo.getOrgTrip().getTripIndex());
            prdType.append(")");

            QueryChangeDetailResp.OrderPriceVo adtPrice = new QueryChangeDetailResp.OrderPriceVo();
            adtPrice.setPrdType(prdType.toString());
            adtPrice.setDiffPrice(changeDetails.get(0).getDiffPrice());
            adtPrice.setSize(changeDetails.size());
            adtPrice.setTotalPrice(changeDetails.get(0).getPrice());
            adtPrice.setChangeFee(changeDetails.get(0).getChangeFee());
            prices.add(adtPrice);
        }
    }

prices的数据效果如下:

[{"changeFee":3,"diffPrice":2,"prdType":"成人(去程/第1程) ","size":2,"totalPrice":5}]

现在使用MapStruct实现上述代码,
首先,需要添加转换器ChangePriceMapperStruct

@Mapper
public interface ChangePriceMapperStruct {
    ChangePriceMapperStruct INSTANCE = Mappers.getMapper(ChangePriceMapperStruct.class);

    /**
     * 复杂场景-@Mapping用法
     * expression 和 source不能混用
     *
     * @author zhouxy
     * @date 2025/4/10 18:32
     */
    @Mapping(target = "prdType", expression = "java(type + \"(\" +  trip.getTripType() + \"/第\" +  trip.getTripIndex() + \"程) \")")
    @Mapping(target = "size", expression = "java(changeDetails.size())")
    @Mapping(target = "changeFee", expression = "java(changeDetails.get(0).getChangeFee())")
    @Mapping(target = "diffPrice", expression = "java(changeDetails.get(0).getDiffPrice())")
    @Mapping(target = "totalPrice", expression = "java(changeDetails.get(0).getPrice())")
    QueryChangeDetailResp.OrderPriceVo toOrderPrice(List<TicketChangeDetail> changeDetails,
                                                    String type,
                                                    QueryChangeDetailResp.Trip trip,
                                                    QueryChangeDetailResp.ChangeDetail currentChangeInfo);

}

添加测试类

    @Test
    public void complexityMapping() {
        List<TicketChangeDetail> changeDetails = new ArrayList<>();
        TicketChangeDetail changeDetail1 = new TicketChangeDetail();
        changeDetail1.setChangeFee(BigDecimal.valueOf(3));
        changeDetail1.setDiffPrice(BigDecimal.valueOf(2));
        changeDetail1.setPrice(BigDecimal.valueOf(5));
        changeDetails.add(changeDetail1);

        TicketChangeDetail changeDetail2 = new TicketChangeDetail();
        changeDetail2.setChangeFee(BigDecimal.valueOf(3));
        changeDetail2.setDiffPrice(BigDecimal.valueOf(2));
        changeDetail2.setPrice(BigDecimal.valueOf(5));
        changeDetails.add(changeDetail2);
        String type = "成人";

        QueryChangeDetailResp.Trip trip = new QueryChangeDetailResp.Trip();
        trip.setTripIndex(1);
        trip.setTripType("去程");

        log.info("{}", JSON.toJSONString(ChangePriceMapperStruct.INSTANCE.toOrderPrice(changeDetails, type, trip, new QueryChangeDetailResp.ChangeDetail())));
        //返回结果:{"changeFee":3,"diffPrice":2,"prdType":"成人","size":2,"totalPrice":5}
    }

效果:

{"changeFee":3,"diffPrice":2,"prdType":"成人(去程/第1程) ","size":2,"totalPrice":5}

从结果可以看出,使用MapStruct返回的对象的结果,和使用java代码转换返回的集合中的对象的结果是一样的。

4.MapStruct和BeanUtils比较

前面两个例子只是将Java的属性赋值换了个写法,两者各有各的好处,在实际应用中就是萝卜白菜各有所爱了。但是与BeanUtils通过反射进行赋值的方式比较,MapStruct的性能优势就凸显出来了。

1. 核心机制对比

特性MapStructBeanUtils (Apache/Spring)
实现原理编译时生成Java映射代码运行时反射
代码可见性生成可调试的Java类黑箱操作,无可见代码
性能接近手写代码的性能(无反射开销)反射调用,性能较差(尤其批量操作)
类型安全编译时检查类型匹配运行时可能抛出异常

针对代码可见性/性能/类型安全简单举个例子:前面的两个MapStruct,比如ChangePriceMapperStruct,其实编译的时候,就会生成一个实现类,如下图所示。
在这里插入图片描述
转换为java代码,也就是运行时实际上跑的是上图的Java代码,如果编译时生成实现类代码有问题,就会直接报编译错误。

另附:原始接口方法

   /**
     * 复杂场景-@Mapping用法
     * expression 和 source不能混用
     *
     * @author zhouxy
     * @date 2025/4/10 18:32
     */
    @Mapping(target = "prdType", expression = "java(type + \"(\" +  trip.getTripType() + \"/第\" +  trip.getTripIndex() + \"程) \")")
    @Mapping(target = "size", expression = "java(changeDetails.size())")
    @Mapping(target = "changeFee", expression = "java(changeDetails.get(0).getChangeFee())")
    @Mapping(target = "diffPrice", expression = "java(changeDetails.get(0).getDiffPrice())")
    @Mapping(target = "totalPrice", expression = "java(changeDetails.get(0).getPrice())")
    QueryChangeDetailResp.OrderPriceVo toOrderPrice(List<TicketChangeDetail> changeDetails,
                                                    String type,
                                                    QueryChangeDetailResp.Trip trip,
                                                    QueryChangeDetailResp.ChangeDetail currentChangeInfo);

2. 性能对比,deepseek示例

基准测试示例(100万次映射)
// MapStruct 平均耗时:~50ms
CarDto dto = CarMapper.INSTANCE.carToDto(car); 

// BeanUtils.copyProperties 平均耗时:~500ms 
CarDto dto = new CarDto();
BeanUtils.copyProperties(car, dto); 

MapStruct 快 5-10 倍,差距在批量操作中更明显。


3.总结

  • 选MapStruct:追求性能、类型安全、复杂映射
  • 选BeanUtils:快速原型开发、简单POJO拷贝
  • 混合使用:非关键路径用BeanUtils,核心业务用MapStruct

常见注解

注解说明
@Mapper标记接口为映射器
@Mapping定义字段映射规则
@Mappings包含多个@Mapping注解
@MappingTarget标记目标对象用于更新操作
@BeforeMapping在映射前执行的方法
@AfterMapping在映射后执行的方法
@Named为映射方法命名,用于qualifiedByName

缺点

  • 无法在MapperStruct的接口类中使用@Slf4j
  • @BeforeMapping设置前提条件,不支持拦截,只能通过报错或者上下文处理。

问题

问题一:引入mapStruct依赖之后,导致@Data注解不生效,如何解决

当 MapStruct 和 Lombok 一起使用时,可能会出现 @Data 注解不生效的情况,这是因为两个注解处理器的执行顺序问题导致的。可以使用下述方案解决:

  1. 正确配置注解处理器顺序(推荐)
<build>
    <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>
                <annotationProcessorPaths>
                    <!-- Lombok 必须在前 -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <!-- 然后是 MapStruct -->
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.添加lombok以及mapStruct依赖

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
</dependencies>

lombok和mapStruct的版本适配,lombok推荐使用1.18.30+

MapStruct 版本推荐的 Lombok 版本说明
1.4.x.Final1.18.16+基本兼容
1.5.x.Final1.18.24+最佳兼容
1.6.x.Beta1.18.30+支持新特性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值