[Java] MapStruct:Java数据对象的映射框架
概述:MapStruct :Java数据对象的映射框架(Entity/PO <==> DTO / Vo / ...)
MapStruct 的作用
- MapStruct是一个代码生成器,它简化了- Java应用程序中对象之间的映射/转换。
为什么使用 MapStruct?
- 在开发中你可曾遇到如下这样的问题?
MyBtatis从数据库中查询的数据行映射到domain的实体类上,然后有时候需要将domain的实体类映射给前端的VO类,用于展示。
如下所示,假如Student是domain,而给前端展示的为StudentVO。

- 有没有什么优雅的解决方式呢?
- 可能你的第一反应就是使用
Spring的BeanUtils.copyProperties(),但BeanUtils.copyProperties()只能转换类中字段名字一样且类型一样的字段。- 由于
BeanUtils.copyProperties()底层是基于反射机制,实际上当高频重复调用时效率是比较低的。实际测试实际测试
Spring的BeanUtils在生成次数为100 0000时需要1.6秒,而使用MapStruct仅需要69毫秒)。
MapStruct 原理分析
- MapStruct的原理其实比较简单,就是基于- APT(Annotation Processing Tool)。
APT是Java的一个工具,它可以在编译时扫描和处理注解。
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
- 修改 Mapper
对于
Spring应用,修改@Mapper注解,设置componentModel属性值为spring,如果是CDI,则将其值设置为cdi。
@Mapper(componentModel = "spring")
public interface SimpleBeanMapper
- 注入 Spring 组件到 Mapper 中
反过来,我们需要在
mapper中引用Spring容器中的组件该如何实现? 这种情况下,我们需要改用抽象类而非接口了
@Mapper(componentModel = "spring")
public abstract class SimpleMapperUsingInjectedService
- 添加我们熟知的
@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;从BigDecimal到String的转换;及从Date到String的转换
- 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
- [Java EE]辨析: POJO(PO / DTO / VO) | BO/DO | DAO - 博客园/千千寰宇
- MapStruct最详细的使用教程,别在用BeanUtils.copyProperties () - CSDN
- 设计模式之总述 - 博客园/千千寰宇
X 参考文献
个人不认同其观点:
- "微不足道的性能差异"。MapStruct的性能表现出色,与直接使用set/get方法相比几乎没有差距; Spring的BeanUtils虽然稍慢,但这种微小的差距对系统运行影响微乎其微。然而,正是这种微不足道的性能差异,导致许多人选择使用MapStruct。
个人认为,并非微不足道,而是差距明显。
- 过早优化:为了解决一个可能并不那么重要的性能问题,我们反而使架构变得更复杂。
个人认为,并非过早优化
个人补充: MapStruct 的优异之处,并不仅限于性能,还在于其提供了灵活的字段级映射配置能力。
在 Spring 中使用 MapStruct (
@Mapper(componentModel = "spring"))
lombok 兼容的问题 / 被不同版本编译 / IDEA的缓存问题 / IDEA 插件-MapStruct-Support / ...
 
    本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号