🧭 MapStruct 教程笔记

—— 高效、安全的 Java Bean 映射工具


一、为什么选择 MapStruct?

  1. 性能优势
    MapStruct 在编译时生成纯 Java 方法调用的映射代码,避免了反射带来的性能损耗。例如,在处理大量对象映射时,MapStruct 的性能显著优于使用反射的工具,如 Spring 的 BeanUtils
  2. 类型安全
    编译时检查映射字段的类型和名称,避免运行时错误。例如,字段名或类型不匹配时,编译阶段会报错。
  3. 灵活性
    支持复杂映射逻辑,如多源映射、嵌套映射、自定义类型转换等,覆盖大多数业务场景。

二、基础配置

1. Maven 依赖配置

pom.xml 中添加以下依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bingbaihanji</groupId>
    <artifactId>MapStruct</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <org.mapstruct.version>1.6.3</org.mapstruct.version>
    </properties>
    <dependencies>
        <!-- MapStruct 核心库 -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
        <!-- MapStruct 注解处理器 -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${org.mapstruct.version}</version>
            <scope>provided</scope>
        </dependency>
        <!-- Lombok(可选) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.38</version>
        </dependency>
        <!-- Lombok 与 MapStruct 集成(可选) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>0.2.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven 编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.38</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意:若使用 Lombok,需确保 mapstruct-processor 在 Lombok 之后声明。

2. 与 Spring 集成

@Mapper 注解中指定 componentModel = "spring",生成的映射器将自动注册为 Spring Bean:

@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDto toDto(User user);
}

三、核心用法

1. 基础映射

同名字段自动映射:若源对象与目标对象字段名相同,无需额外配置。

定义两个实体类

package com.bingbaihanji.entity;

import lombok.Data;

@Data
public class SimpleSource {
    private String name;
    private String description;
}
//--------------------------------------------------------------------// 
package com.bingbaihanji.entity;

import lombok.Data;

@Data
public class SimpleDestination {
    private String name;
    private String description;
}

写mapper接口

package com.bingbaihanji.structmapper;

import com.bingbaihanji.entity.SimpleDestination;
import com.bingbaihanji.entity.SimpleSource;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface SimpleSourceDestinationMapper {
    // 使用 Mappers.getMapper(SimpleSourceDestinationMapper.class) 获取实例
    SimpleSourceDestinationMapper INSTANCE = Mappers.getMapper(SimpleSourceDestinationMapper.class);

    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

然后执行 mvn clean install 触发 MapStruct 插件自动生成代码,生成后的实现类在 /target/generated-sources/annotations/ 目录下

package com.bingbaihanji.structmapper;

import com.bingbaihanji.entity.SimpleDestination;
import com.bingbaihanji.entity.SimpleSource;
import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-21T12:00:31+0800",
    comments = "version: 1.6.3, compiler: javac, environment: Java 21.0.7 (Eclipse Adoptium)"
)
public class SimpleSourceDestinationMapperImpl implements SimpleSourceDestinationMapper {

    @Override
    public SimpleDestination sourceToDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }

        SimpleDestination simpleDestination = new SimpleDestination();

        simpleDestination.setName( source.getName() );
        simpleDestination.setDescription( source.getDescription() );

        return simpleDestination;
    }

    @Override
    public SimpleSource destinationToSource(SimpleDestination destination) {
        if ( destination == null ) {
            return null;
        }

        SimpleSource simpleSource = new SimpleSource();

        simpleSource.setName( destination.getName() );
        simpleSource.setDescription( destination.getDescription() );

        return simpleSource;
    }
}

测试用例

package com.bingbaihanji;


import com.bingbaihanji.entity.SimpleDestination;
import com.bingbaihanji.entity.SimpleSource;
import com.bingbaihanji.structmapper.SimpleSourceDestinationMapper;


public class Main {

    public static void main(String[] args) {
        // 创建源对象并赋值
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName("SourceName");
        simpleSource.setDescription("SourceDescription");
        // 使用SimpleSourceDestinationMapper将源对象转换为目标对象
        SimpleDestination simpleDestination = SimpleSourceDestinationMapper.INSTANCE.sourceToDestination(simpleSource);
        System.out.println(simpleSource);
        System.out.println("----------------------");
        System.out.println(simpleDestination);

    }
}

输出结果:

SimpleSource(name=SourceName, description=SourceDescription)
----------------------
SimpleDestination(name=SourceName, description=SourceDescription)
字段名不同:使用 @Mapping 注解指定映射关系。

Studnet类的age和name与StudentVO类的ageVO和nameVO对应不上时

package com.bingbaihanji.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author bingb
 * @date 2025-05-21 12:05:56
 * @description //TODO
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String name;
    private Integer age;
}

//==========================================================//
package com.bingbaihanji.entity.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentVO {
    // 换名字
    private String nameVO;
    // 将age换成String类型
    private String age;
}

mappper

package com.bingbaihanji.structmapper;

import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.vo.StudentVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

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


    // 使用 @Mappings 注解进行更复杂的映射
    // 在Mapper类中加入@Mapping的注解指定原对象的字段名和要被对应上的字段名。
    // 其中@Mappings表示多个字段需要对应,如果只是一个可以使用@Mappin
    @Mappings({
            @Mapping(source = "name", target = "nameVO"),
            @Mapping(source = "age", target = "age")
    })
    StudentVO studentToStudentVO(Student student);

}

测试

package com.bingbaihanji;


import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.vo.StudentVO;
import com.bingbaihanji.structmapper.StudentMapper;


public class Main {

    public static void main(String[] args) {
        Student student = new Student("张三", 14);
        StudentVO studentVO = StudentMapper.INSTANCE.studentToStudentVO(student);
        System.out.println(student);
        System.out.println("----------------------");
        System.out.println(studentVO);
    }
}    

结果

Student(name=张三, age=14)
----------------------
StudentVO(nameVO=张三, age=14)

2. 多源映射

支持从多个源对象合并字段到目标对象:

某些时候,我们的源不是一个,例如从数据库中查询出来了学生和老师,我们需要将老师的名字给VO的name字段,学生的年龄给VO的age字段时可以使用多参数源的映射方式。

package com.bingbaihanji.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author bingb
 * @date 2025-05-21 14:09:04
 * @description //TODO
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    private String teacherName;
    private String teacherAge;
}

mapper

package com.bingbaihanji.structmapper;

import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

@Mapper
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
    	// 老师的名字给VO的name字段,学生的年龄给VO的age字段
      @Mappings({
                @Mapping(source = "teacher.teacherName", target = "nameVO"),
                @Mapping(source = "student.age", target = "age")
        })
        StudentVO studentToStudentVO(Student student, Teacher teacher);

    }
}

测试:

package com.bingbaihanji;

import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import com.bingbaihanji.structmapper.StudentMapper;

public class Main {
    public static void main(String[] args) {
        Student student = new Student("猴子", 14);
        Teacher teacher = new Teacher("菩提祖师", 18);
        StudentVO studentVO = StudentMapper.INSTANCE.studentToStudentVO(student, teacher);
        System.out.println(student);
        System.out.println(teacher);
        System.out.println("----------------------");
        System.out.println(studentVO);
    }
}

输出结果:

Student(name=猴子, age=14)
Teacher(teacherName=菩提祖师, teacherAge=18)
----------------------
StudentVO(nameVO=菩提祖师, age=14)

3. 嵌套映射

通过 . 操作符访问嵌套对象的字段:

实体类:

package com.bingbaihanji.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author bingb
 * @date 2025-05-21 14:09:04
 * @description //TODO
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    private String teacherName;
    private Integer teacherAge;
    // 爱好
    private Hobby hobby;
}

//=======================================//
package com.bingbaihanji.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author bingb
 * @date 2025-05-21 14:19:59
 * @description //TODO
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Hobby {
    private String playingBasketball;
    private String readingBooks;
    private String swimming;
}
// ========================================//
package com.bingbaihanji.entity.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class StudentVO {
    // 换名字
    private String nameVO;
    // 将age换成String类型
    private String age;
    // 爱好
    private String hobby;

}

mapper

package com.bingbaihanji.structmapper;

import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

@Mapper
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
     @Mappings({
                @Mapping(source = "teacher.hobby.swimming", target = "hobby"),
                @Mapping(source = "teacher.teacherAge", target = "nameVO"),
                @Mapping(source = "student.age", target = "age")
        })
        StudentVO studentToStudentVO(Student student, Teacher teacher);
}

测试以及结果

package com.bingbaihanji;


import com.bingbaihanji.entity.Hobby;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import com.bingbaihanji.structmapper.StudentMapper;

public class Main {
    public static void main(String[] args) {
        Student student = new Student("猴子", 14);
        Teacher teacher = new Teacher("菩提祖师", 18, new Hobby("篮球", "看书", "游泳"));
        StudentVO studentVO = StudentMapper.INSTANCE.studentToStudentVO(student, teacher);
        System.out.println(student);
        System.out.println(teacher);
        System.out.println("----------------------");
        System.out.println(studentVO);
    }
}

结果

Student(name=猴子, age=14)
Teacher(teacherName=菩提祖师, teacherAge=18, hobby=Hobby(playingBasketball=篮球, readingBooks=看书, swimming=游泳))
----------------------
StudentVO(nameVO=18, age=14, hobby=游泳)

4. 集合映射

自动映射集合类型(如 ListSet):

List<CarDto> carsToCarDtos(List<Car> cars);

四、高级功能

1. 数据类型转换

  • 隐式转换:自动处理基本类型与包装类、StringDate 等常见类型的转换。

  • 显式格式化:使用 dateFormatnumberFormat 指定格式。

    @Mapping(target = "createTime", source = "createDate", dateFormat = "yyyy-MM-dd")
    UserDto userToUserDto(User user);
    

2. 自定义映射方法

通过 expression@AfterMapping 注入自定义逻辑:

@Mapping(target = "fullName", expression = "java(user.getFirstName() + ' ' + user.getLastName())")
UserDto userToUserDto(User user);

@AfterMapping
default void fillExtInfo(User user, @MappingTarget UserDto dto) {
    dto.setExtInfo(calculateExtInfo(user));
}

3. 逆向映射

使用 @InheritInverseConfiguration 自动生成反向映射:

@Mapping(target = "name", source = "userName")
UserDto userToUserDto(User user);

@InheritInverseConfiguration
User userDtoToUser(UserDto dto);

4. 更新现有对象

通过 @MappingTarget 更新目标对象而非创建新实例:

void updateUser(User user, @MappingTarget UserDto dto);

5. 使用抽象类自定义映射器

一些特殊场景 @Mapping 无法满足时,需要定制化开发,同时希望保留MapStruct自动生成代码的能力。

1 定义实体类
public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    //standard getters
}
DTO类:
public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

BigDecimal 类型的total美元金额,转换为 Long totalInCents (以美分表示的总金额),这部分我们将使用自定义方法实现。

2. Mapper 定义
@Mapper
abstract class TransactionMapper {
    public TransactionDTO toTransactionDTO(Transaction transaction) {
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setUuid(transaction.getUuid());
        transactionDTO.setTotalInCents(transaction.getTotal()
          .multiply(new BigDecimal("100")).longValue());
        return transactionDTO;
    }
    public abstract List<TransactionDTO> toTransactionDTO(
      Collection<Transaction> transactions);
}

6 @BeforeMapping 和 @AfterMapping 注解

可以通过 @BeforeMapping 和 @AfterMapping 注解定制化需求

@MappingTarget 是一个参数注释,在@BeforeMapping的情况下,在执行映射逻辑之前填充目标映射DTO,在@AfterMapping注释方法的情况下在执行之后填充。

@Mapper
public abstract class CarsMapper {
    @BeforeMapping
    protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
        if (car instanceof ElectricCar) {
            carDto.setFuelType(FuelType.ELECTRIC);
        }
        if (car instanceof BioDieselCar) { 
            carDto.setFuelType(FuelType.BIO_DIESEL);
        }
    }

    @AfterMapping
    protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
        carDto.setName(carDto.getName().toUpperCase());
    }

    public abstract CarDTO toCarDto(Car car);
}

五、整合spring框架

前面实例通过 Mappers.getMapper(YourClass.class) 获取 MapStruct Mapper实例。

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

注入到spring容器

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

注入 Spring 组件到 Mapper 中

我们需要在mapper中引用Spring容器中的组件,需要改用抽象类而非接口了:

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService

然后添加 @Autowired 注入依赖:

@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 需要在生成的实现类中访问该对象

六、常见问题与优化

1. Lombok 兼容性

Maven 配置中需确保 Lombok 处理器在 MapStruct 之前。

2. 枚举映射

使用 @ValueMapping 处理枚举值不一致的情况:

@ValueMapping(source = "MALE", target = "MAN")
GenderDto genderToGenderDto(Gender gender);

3. 空值处理

配置全局策略忽略未映射字段或空值:

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper { ... }

六、总结

MapStruct 通过编译时代码生成实现了高效、安全的 Bean 映射,适用于复杂业务场景。其核心优势在于:

  • 性能:基于直接方法调用,无反射开销。
  • 可维护性:编译时错误检查减少运行时问题。
  • 灵活性:支持自定义逻辑、多源映射等高级功能。

推荐场景:DTO 与 Entity 转换、微服务间模型适配、前后端数据封装等。


人生不作安期生,醉入东海骑长鲸