[Java] MapStruct:Java数据对象的映射框架

概述:MapStruct :Java数据对象的映射框架(Entity/PO <==> DTO / Vo / ...)

MapStruct 的作用

  • MapStruct 是一个代码生成器,它简化了 Java 应用程序对象之间的映射/转换

为什么使用 MapStruct?

  • 在开发中你可曾遇到如下这样的问题?

MyBtatis从数据库中查询的数据行映射到domain的实体类上,然后有时候需要将domain的实体类映射给前端的VO类,用于展示。
如下所示,假如Studentdomain,而给前端展示的为StudentVO

  • 有没有什么优雅的解决方式呢?
  • 可能你的第一反应就是使用SpringBeanUtils.copyProperties(),但BeanUtils.copyProperties() 只能转换类中字段名字一样类型一样的字段
  • 由于BeanUtils.copyProperties() 底层是基于反射机制,实际上当高频重复调用效率是比较低的。

实际测试实际测试SpringBeanUtils在生成次数为100 0000时需要1.6秒,而使用MapStruct仅需要69毫秒)。

MapStruct 原理分析

  • MapStruct 的原理其实比较简单,就是基于 APT(Annotation Processing Tool)。
  • APTJava的一个工具,它可以在编译时扫描和处理注解
  • MapStruct就是使用APT来处理@Mapper注解,并生成相应的映射代码。
  • 当你在接口抽象类上使用@Mapper注解,并且编译你的项目时,APT会调用MapStruct的注解处理器。然后,MapStruct注解处理器会分析这个接口或抽象类,找出所有的映射方法,并为每个映射方法生成实现代码。

  • 这种在编译时生成代码的方式,使得MapStruct运行效率非常高,因为所有的映射逻辑都已经在编译时确定,运行时不需要进行任何反射动态代理

如在上文的例子中,MapStruct 生成了一个 mapper 接口的实现类:

Maven坐标

  • mapstruct.version : 1.5.5.Final
  • mapstruct-plus.version : 1.4.6 (经项目验证过的配套版本,了解即可)
  • spring-boot : 3.3.5 (经项目验证过的配套版本,了解即可)
  • spring : 6.1.14 (经项目验证过的配套版本,了解即可)
        <!-- data object convert | start -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        <!-- mapstruct-processor | 避免报错: Caused by: java.lang.ClassNotFoundException: Cannot find implementation for com.daq.sdk.pojo.mapstruct.convert.CustomerBeanMapper -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>

<!--        <dependency>-->
<!--          <groupId>io.github.linpeilie</groupId>-->
<!--          <artifactId>mapstruct-plus</artifactId>-->
<!--          <version>${mapstruct-plus.version}</version>-->
<!--        </dependency>-->

<!--        <dependency>-->
<!--            <groupId>io.github.linpeilie</groupId>-->
<!--            <artifactId>mapstruct-plus-object-convert</artifactId>-->
<!--            <version>${mapstruct-plus.version}</version>-->
<!--        </dependency>-->
        <!-- data object convert | end -->

同类竞品: Java Bean 对象转换工具

性能对比

工具 原理与运行机制 性能
参考数据1(调用10000次)
单位:ms
备注说明
手写Bean转换逻辑 运行前/编译 性能最高
Pure get/set 未知 10ms
Cglib Beancopier 使用了 asm 生成一个包含所有 setter/getter 代码的代理类 14ms
Mapstruct 运行前/编译(基于注解) 10ms
MapstructPlus 运行前/编译(基于注解) 10ms
Apache Commons BeanUtils 运行时/反射/浅复制 249ms
Apache PropertieyUtils 运行时/反射/浅复制 130ms
Spring BeanUtils 运行时/反射/浅复制 96ms
Dozer 未知 770ms
  • 参考数据1
数据量 Apache Spring MapStruct BeanCopier
100w 391ms 250ms 45ms 57ms
10w 82ms 34ms 8ms 10ms
1w 30ms 19ms 2ms 7ms
1k 15ms 6ms 1ms 5ms
100 5ms 3ms 1ms 4ms
10 2ms 1ms 1ms 4ms
  • 参考数据2

Apache Commons BeanUtils#copyProperties
Spring BeanUtils#copyProperties
Cglib BeanCopier#copy

Cglib BeanCopier

  • 推荐文献
  • 依赖引入
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>
  • 基本使用
  • 案例1
public static BeanCopier create(Class source, Class target, boolean useConverter) {
    Generator gen = new Generator();
    gen.setSource(source);
    gen.setTarget(target);
    gen.setUseConverter(useConverter);
    return gen.create();
}
  • 案例2
User user = new User();
user.setUsername("vinjcent");
user.setPassword("123456");
UserDTO userDTO = BeanCopierUtils.copy(user, UserDTO.class);

System.out.println("源对象===>" + user);
System.out.println("拷贝对象===>" + userDTO);

案例实践

案例:存在嵌套对象的Entity转Dto

  • java : 17

引入依赖

	<!-- data object convert | start -->
	<dependency>
		<groupId>org.mapstruct</groupId>
		<artifactId>mapstruct</artifactId>
		<version>${mapstruct.version}</version>
		<scope>test</scope>
	</dependency>
	<!-- mapstruct-processor | 避免报错: Caused by: java.lang.ClassNotFoundException: Cannot find implementation for com.daq.sdk.pojo.mapstruct.convert.CustomerBeanMapper -->
	<dependency>
		<groupId>org.mapstruct</groupId>
		<artifactId>mapstruct-processor</artifactId>
		<version>${mapstruct.version}</version>
		<scope>test</scope>
	</dependency>
<!--        <dependency>-->
<!--            <groupId>io.github.linpeilie</groupId>-->
<!--            <artifactId>mapstruct-plus-object-convert</artifactId>-->
<!--            <version>${mapstruct-plus.version}</version>-->
<!--        </dependency>-->
	<!-- data object convert | end -->

Customer / Address : Entity 对象

  • Customer
package com.xxx.sdk.pojo.mapstruct.entity;

import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class Customer {
    private String name;
    private Address address;
    // getters and setters
}
  • Address
package com.xxx.sdk.pojo.mapstruct.entity;

import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class Address {
    private String street;
    private String city;
    // getters and setters
}

CustomerDto : DTO 对象

  • CustomerDto
package com.xxx.sdk.pojo.mapstruct.dto;

import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class CustomerDto {
    private String name;
    private String street;
    private String city;
    // getters and setters
}

CustomerBeanMapper(CustomerBeanConverter) : 基于 MapStruct 的对象转换器

  • CustomerBeanMapper (也可命名为: CustomerBeanConverter)
package com.xxx.sdk.pojo.mapstruct.convert;


import com.xxx.sdk.pojo.mapstruct.dto.CustomerDto;
import com.xxx.sdk.pojo.mapstruct.entity.Customer;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

@Mapper
public interface CustomerBeanMapper {
    //映射器工厂 : MapStruct 框架 为开发者提供的映射工厂,指定接口类型后自动帮我们创建接口的实现,且保证是【线程安全】的【单例】,无需自己手动创建
    CustomerBeanMapper INSTANCE = Mappers.getMapper(CustomerBeanMapper.class);

    /**
     * 必须使用 @Mappings/Mapping 注解的情况:
     * CASE1. 来源Bean 与 目标Bean 的 属性【字段名称有不同】时 (参见 本方法)
     * CASE2. 来源Bean 与 目标Bean 的 属性有【嵌套关系】时 (参见 本方法)
     * CASE3. 【多个来源Bean】 转 目标Bean 的属性时 (参见 toCustomerDto 方法)
     */
    @Mappings({
        @Mapping(source = "address.street", target = "street"),
        @Mapping(source = "address.city", target = "city"),
        @Mapping(source = "name", target = "name", qualifiedByName = "toUpperCase") // qualifiedByName: 这个参数允许你引用一个具有@Named注解的方法作为自定义的映射逻辑。
    })
    CustomerDto customerToCustomerDTO(Customer customer);

    
    @Mappings({
        @Mapping(source = "vipUser.vipId", target = "vipNo"),
        @Mapping(source = "account.username", target = "username")
    })
    CustomerDto toCustomerDto(VipUser vipUser, Account account);


    @Named("toUpperCase")
    default String toUpperCase(String name) {
        return name.toUpperCase();
    }
}
  • qualifiedByName: 这个参数允许你引用一个具有@Named注解的方法作为自定义的映射逻辑

应用场景1:正常使用(Entity to Dto) | CustomBeanMapperTest

package com.xxx.sdk.pojo.mapstruct.test;

import com.xxx.sdk.pojo.mapstruct.convert.CustomerBeanMapper;
import com.xxx.sdk.pojo.mapstruct.dto.CustomerDto;
import com.xxx.sdk.pojo.mapstruct.entity.Address;
import com.xxx.sdk.pojo.mapstruct.entity.Customer;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

/**
 * entity to dto
 */
@Slf4j
public class CustomBeanMapperTest {
    @Test
    public void entityToDtoTest(){
        //entity
        Customer customer = new Customer();
        customer.setName("John Doe");
        Address address = new Address();
        address.setStreet("123 Main St");
        address.setCity("Springfield");
        customer.setAddress(address);

        //to dto
        CustomerDto customerDto = CustomerBeanMapper.INSTANCE.customerToCustomerDTO(customer);
        log.info("customerDto:{}", customerDto);//customerDto:CustomerDto(name=JOHN DOE, street=123 Main St, city=Springfield)
    }
}

延申-应用场景2 : 基于spring,依赖注入

  • 某些时候尤其是在做项目时,我们用到了 Sping,希望映射后的新实例是交给Spring管理。

这时候就需要进行依赖注入了。

  • 只需要在MapStruct框架的Mapper接口中的@Mapper注解中加入componentModel = "spring"即可

  • 案例1 : StudentMapper

  • 案例2

MapStruct 同时支持 Spring 和 CDI

    1. 修改 Mapper

对于Spring应用,修改@Mapper注解,设置componentModel属性值为spring,如果是 CDI,则将其值设置为 cdi

@Mapper(componentModel = "spring")
public interface SimpleBeanMapper
    1. 注入 Spring 组件到 Mapper 中

反过来,我们需要在mapper中引用Spring容器中的组件该如何实现? 这种情况下,我们需要改用抽象类而非接口

@Mapper(componentModel = "spring")
public abstract class SimpleMapperUsingInjectedService
    1. 添加我们熟知的 @Autowired 注入依赖:
@Mapper(componentModel = "spring")
public abstract class SimpleMapperUsingInjectedService {
    @Autowired
    protected SimpleBeanMapper simpleBeanMapper;

    @Mapping(target = "name", expression = "java(simpleService.enrichName(sourceBean.getName()))")
    public abstract SimpleDestinationBean sourceToDestination(SimpleSourceBean sourceBean);
}

    //可在业务方法中直接调用 simpleBeanMapper 实例对象
    public void businessMethod(){
        //...
        XxxxVo = simpleBeanMapper.toXxxxVo(sourceBean);
        //...
    }

切记不要将注入的 bean 设置为private ,因为 MapStruct 需要在生成的实现类中访问该对象

延申-应用场景3 : 数据类型转换

  • 映射属性源对象目标对象中具有相同的类型,这种情况不全有

例如,属性在源bean中可以是Int类型,但在目标bean中可以是Long类型。
另一个例子是对其他对象的引用,这些对象应该映射到目标模型中的相应类型
例如:Teachr类可能有一个Wife类型的属性wife,在映射VO对象时需要将其转换为StudentVO对象。

  • 在许多情况下,MapStruct自动处理类型转换

例如,如果属性在源bean中的类型为int,但在目标bean中的类型为String,则:

生成的代码将分别通过调用String.valueOf(int)Integer.parseInt(String)来透明地执行转换。

  • 案例

通过案例来实现: 从int转换为String;从BigDecimalString的转换;及从DateString的转换

  • Student
@Data //lombok 注解
@AllArgsConstructor //lombok 注解
public class Student {
    private String name;
    private int age;
    private BigDecimal cash;
    private Date dateOfBirth;
}
  • StudentVo
@Data //lombok 注解
@AllArgsConstructor //lombok 注解
public class StudentVo {
    private String name;
    private String age;
    private String cash;
    private String dateOfBirth;
}
  • StudentBeanMapper
@Mapper // mapstruct 注解
public interface StudentBeanMapper {
    StudentBeanMapper INSTANCE = Mappers.getMapper(StudentBeanMapper.class);

    @Mappings({
        @Mapping(source="name", target="name"),
        @Mapping(source="age", target="age", numberFormat="#.00"),
        @Mapping(source="cash", target="cash", numberFormat="#.#E0"),
        @Mapping(source="dateOfBirth", target="dateOfBirth", dateFormat="yyyy-MM-dd")
    })
    StudentVo toStudentVo(Student student);

}
  • Client
public class Test {
    public static void main(String [] args) {
        BigDecimal bigDecimal = new BigDecimal("0.3");
        Date date = new Date();
        Student student = new Student("小旺", 23, bigDecimal, date);
        StudentVo studentVo = StudentBeanMapper.INSTANCE.toStudentVo(student);
        System.out.println(studentVo);
    }
}

out

StudentVo(name=小旺, age=33.00, cash=3E-1, dateOfBirth=2025-10-24)

延申-应用场景4:映射集合

  • 映射集合的时候,我们同样可以进行类型之间的转换

如下所示使用@MapMapping注解指定输出类型即可。

  • MapBeanMapper
@Mapper // mapstruct 注解
public interface MapBeanMapper {
    MapBeanMapper INSTANCE = Mappers.getMapper(StuMapBeanMapper entBeanMapper.class);

    @Mapping(valueDateFormat="yyyy-MM-dd")
    Map<String, String> toMap(Map<String, Date> map);

}
  • Client
public class Test {
    public static void main(String [] args) {
        Map map = new HashMap();
        map.put("张三", new Date());
        map.put("李四", new Date());
        Map newMap = MapBeanMapper.INSTANCE.toMap(map);
        System.out.println(newMap);
    }
}

out

{张三=2025-10-24, 李四=2025-10-24}

当然,MapStruct也支持其他各种类型的集合映射,上面只是举例了Map的映射

延申-应用场景5:映射枚举

  • MapStruct支持生成将一个Java枚举类型映射到另一个Java枚举类型的方法。

默认情况下,源枚举中的每个常量都映射到目标枚举类型中具有相同名称的常量
如果需要,可以使用@ValueMapping注解将【源枚举】中的常量映射到【目标枚举】具有其他名称的常量
源枚举中的几个常量可以映射到目标类型中的相同常量。

  • 案例

Student中是SexEnum枚举,而StudentVO中是Sex2Enum,且枚举中的值是一致时,我们需要将Student中的映射到StudentVO中。
此时,只需要使用@Mapping来指定映射源和目标源的名称即可

  • SexEnum
@Getter // lombok 注解
public enum SexEnum {
    // 保密
    SECRECY(desc: "保密"),
    // 男
    MAN(desc: "男"),
    // 女
    WOMAN(desc: "女");

    private String desc;

    SexEnum(String desc) {
        this.desc = desc;
    }
}
  • Sex2Enum
@Getter // lombok 注解
public enum Sex2Enum {
    // 保密
    SECRECY(desc: "保密"),
    // 男
    MAN(desc: "男"),
    // 女
    WOMAN(desc: "女");

    private String desc;

    Sex2Enum(String desc) {
        this.desc = desc;
    }
}
  • Student
@Data //lombok 注解
@AllArgsConstructor
public class Student {
    private String name;
    private Integer age;
    private SexEnum sexEnum;
}
  • StudentVo
@Data //lombok 注解
@AllArgsConstructor
public class StudentVo {
    private String name;
    private Integer age;
    private Sex2Enum sex2Enum;
}

枚举值一样时,直接使用@Mapping来指定映射源和目标源的名称即可

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

    @Mapping(source = "sexEnum", target= "sex2Enum")
    StudentVo toStudentVo(Student student);
}

枚举值不一致时,使用@ValueMapping注解

  • Sex2Enum
@Getter // lombok 注解
public enum Sex2Enum {
    // 保密
    SECRECY2(desc: "保密"),
    // 男
    MAN2(desc: "男"),
    // 女
    WOMAN2(desc: "女");

    private String desc;

    Sex2Enum(String desc) {
        this.desc = desc;
    }
}
  • EnumMapper
@Mapper
public interface EnumMapper {

    EnumMapper INSTANCE = Mappers.getMapper(EnumMapper.class);

    @Mapping(source = "sexEnum", target = "sex2Enum")
    StudentVO toStudentVO(Student student);

    @ValueMappings({
        @ValueMapping(source = "SECRECY", target = "SECRECY2"),
        @ValueMapping(source = "MAN", target = "MAN2"),
        @ValueMapping(source = "WOMAN", target = "WOMAN2"),
    })
    Sex2Enum toSex2Enum(SexEnum sexEnum);
}

延申-应用场景6:对象工厂(针对私有构造器场景)| 工厂方法模式

  • 有时候由于目标实例的构造方法被私有化后,我们使用原来的方式没办法进行。

原因: MapStruct会在编译时去帮你实现,其中包含了调用构造方法
所以,此时我们可以定义工厂的形式来生成实例,而让MapStruct去调用工厂方法来生成初始的实例对象,而不再使用默认的构造方法

  • 案例:
  • 有我们私有化了StudentVO构造方法,如果直接使用MapStruct进行映射是会报错的。
  • StudentVO
@Data
public class StudentVO {
    private String name;
    private Integer age;

    private StudentVO() {//私有化方法
    }

    public static StudentVO getInstance(){
        return new StudentVO();
    }
}
  • StudentFactory

指定工厂,同时在Mapper接口中的@Mapper注解上加入工厂的class

public class StudentFactory {
    public class createStudentVo(){
        return StudentVo.getInstance();
    }
}
  • StudentMapper
@Mapper(uses = {StudentFactory.class})
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    StudentVO toStudentVO(Student student);
}
  • Client
public static void main(String[] args) {
    Student student = new Student(name: "张三", age: 12);
    StudentVO studentVO = StudentMapper.INSTANCE.toStudentVO(student);
    System.out.println(studentVO);
}

out

StudentVo(name=张三,age=12)

延申-应用场景6:自定义映射 | 装饰器模式

  • 在某些情况下,可能需要定制生成的映射方法,在目标对象中设置了一个无法由MapStruct生成的方法实现时,可以使用自定义映射来完成。

  • 案例

  • 假如我们的StudentVO中的age是无法生成的。
  • 首先定义类,然后实现Mapper接口,在重写的方法中写上需要的逻辑,且在Mapper接口中加入@DecorateWith注解,指定自定义映射的class
  • StudentMapperDecorator
public class StudentMapperDecorator implements StudentMapper {
    private final StudentMapper studentMapper;

    public StudentMapperDecorator(StudentMapper studentMapper) {
        this.studentMapper = studentMapper;
    }

    @Override
    public StudentVO toStudentVO(Student student) {
        StudentVO studentVO = studentMapper.toStudentVO(student);
        studentVO.setAge("100");
        return studentVO;
    }
}
  • StudentMapper
@Mapper(uses = {StudentFactory.class})
@DecoratedWith(StudentMapperDecorator.class)
public interface StudentMapper {

    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    StudentVO toStudentVO(Student student);
}
  • Client
public static void main(String[] args) {
    Student student = new Student(name: "张三", age: 0);
    StudentVO studentVO = StudentMapper.INSTANCE.toStudentVO(student);
    System.out.println(studentVO);
}

可以看到先给age值为0,最后输出为100.

out

StudentVo(name=张三, age=100)

K FAQ

Q: 缓存问题: 使用 MapStruct 在 IDEA 社区版中老是出现类找不到、编译问题(比较坑)

Q: lombok 兼容的问题

lombok 兼容的问题 : Lombok 1.18.16 引入了重大更改(更改日志)。必须添加附加注释处理器 lombok-mapstruct-binding (Maven),否则 MapStruct 将停止与 Lombok 配合使用。

<path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-mapstruct-binding</artifactId>
    <version>0.2.0</version>
</path>

Q:被不同版本编译?

Caused by: java.lang.UnsupportedClassVersionError: com/example/mapper/AddressMapperImpl has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0

  • 手动删除 target重新启动应用即可。或者 mvn clean

Q:IDEA 插件-MapStruct-Support

  • target、source、expression 中的代码补全
  • 重构支持
  • ......

插件地址: https://plugins.jetbrains.com/plugin/10036-mapstruct-support

Y 推荐文献

  • MapStruct

X 参考文献

个人不认同其观点:

  • "微不足道的性能差异"。MapStruct的性能表现出色,与直接使用set/get方法相比几乎没有差距; Spring的BeanUtils虽然稍慢,但这种微小的差距对系统运行影响微乎其微。然而,正是这种微不足道的性能差异,导致许多人选择使用MapStruct。

个人认为,并非微不足道,而是差距明显。

  • 过早优化:为了解决一个可能并不那么重要的性能问题,我们反而使架构变得更复杂。

个人认为,并非过早优化

个人补充: MapStruct 的优异之处,并不仅限于性能,还在于其提供了灵活的字段级映射配置能力。

在 Spring 中使用 MapStruct (@Mapper(componentModel = "spring"))

lombok 兼容的问题 / 被不同版本编译 / IDEA的缓存问题 / IDEA 插件-MapStruct-Support / ...

posted @ 2025-04-10 12:54  千千寰宇  阅读(152)  评论(0)    收藏  举报