一.尽量不要使用BeanUtils来拷贝属性
1. BeanUtils 的实现原理
BeanUtils
是 Spring 提供的一个工具类,用于将源对象的属性值拷贝到目标对象。它主要通过 反射 实现属性拷贝,即通过获取源对象和目标对象的类类型,并使用反射获取字段的类型和属性值,然后进行复制。
反射是一种动态查询类信息并操作对象的技术,它提供了灵活性,但也带来了一些性能问题。因为每次使用反射时,JVM都需要在运行时查找和操作对象的属性,这个过程比直接通过 getter
和 setter
访问属性要慢得多。
2. MapStruct 的优势
相比 BeanUtils
,MapStruct
是一个专门用于 JavaBean 转换的库,提供了更高效的属性拷贝方式。
MapStruct 的实现原理:
-
编译时生成代码:MapStruct 不像
BeanUtils
依赖于反射,而是在编译时通过注解生成源代码(生成实现类),并使用普通的getter
和setter
方法进行属性拷贝。这意味着属性的拷贝是通过直接调用方法实现的,不需要反射。 -
更高效:因为 MapStruct 生成的代码直接调用
getter
和setter
方法,而不是通过反射查找字段,所以它的性能要高得多。 -
类型安全:MapStruct 在编译时会进行类型检查,因此转换时会捕获类型错误,而反射无法做到这一点。
MapStruct vs BeanUtils 的性能差异:
-
MapStruct 通过直接的方法调用生成代码,避免了反射,执行效率高;
-
BeanUtils 通过反射来操作对象,性能较差,尤其是在大规模对象映射时可能会有明显的性能瓶颈。
性能对比总结:
工具 | 性能 | 特点 | 适用场景 |
---|---|---|---|
MapStruct | 非常快 | 编译时生成映射代码,避免反射开销 | 高性能、大型项目、类型安全 |
Apache BeanUtils | 较慢 | 使用反射进行属性拷贝,性能开销较大 | 简单场景、不关注性能的小项目 |
Spring BeanUtils | 中等 | 基于反射,但有优化 | Spring 项目,简易的对象映射 |
BeanCopier | 很快 | 使用字节码生成,避免了反射的性能瓶颈 | 高性能、大量数据拷贝 |
二.简单使用MapStruct
0.下载插件( mapstruct support )
具有代码生成、代码提示、用法搜索的作用。
具体怎么使用,没看懂,嘿嘿嘿嘿嘿
1. 引入 MapStruct 依赖
MapStruct 是一个高效的 Java Bean 映射库,常用于将一个对象的属性映射到另一个对象中。它在编译时生成代码,避免了反射的性能问题。接下来,我将通过一个详细的例子,展示如何使用 MapStruct 来进行对象属性的转换。
首先,你需要在项目中添加 MapStruct 的依赖。在 Maven 项目中,可以在 pom.xml
文件中加入以下内容:
<dependencies>
<!-- MapStruct 主库 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version> <!-- 请根据需要使用最新版本 -->
</dependency>
<!-- MapStruct 编译时注解处理器 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version> <!-- 请根据需要使用最新版本 -->
<scope>provided</scope>
</dependency>
</dependencies>
2. 创建简单的 Java Bean 类
我们将创建几个简单的 Java Bean 类,Student和 StudentDTO、Grade、Hobby等实体类。
Student类:
package com.example.study.entity.thread;
import com.example.study.entity.Hobby;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class Student {
@ApiModelProperty(value = "id")
private String id;
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "性别")
private String gender;
@ApiModelProperty(value = "创建时间")
private Date createTime;
@ApiModelProperty(value = "爱好列表")
private List<Hobby> hobbyList;
}
StudentDTO 类:
package com.example.study.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.example.study.entity.Hobby;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class StudentDTO {
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "年龄")
private String ageString;
@ApiModelProperty(value = "性别")
private String gender;
@ApiModelProperty(value = "创建时间")
private String createTime;
@ApiModelProperty(value = "爱好列表")
private List<Hobby> hobbyList;
}
Hobby类
package com.example.study.entity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class Hobby {
@ApiModelProperty(value = "爱好名称")
private String name;
@ApiModelProperty(value = "爱好详情")
private String describe;
}
3. 定义 MapStruct 映射接口
我们接下来创建一个接口 PersonMapper
,通过 @Mapper
注解来标记这个接口,表示这是一个 MapStruct 映射器。
package com.example.study.mapper;
import com.example.study.dto.StudentDTO;
import com.example.study.entity.Grade;
import com.example.study.entity.thread.Student;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper; //注意不是mybatis的那个Mapper
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@Mapper //注意不是mybatis的那个Mapper
public interface DTOMapper {
// 加上componentModel = "spring"后,Spring 会自动将 DTOMapper 的实现类注入到你的服务或控制器中,而你不再需要手动创建 INSTANCE。
DTOMapper INSTANCE = Mappers.getMapper(DTOMapper.class);
// 将 Student 转换为 StudentDTO
// 用于指定在映射时,源对象中的 age 字段应该映射到目标对象中的 ageString 字段。这个注解主要在源和目标对象的字段名称不一致时使用,帮助 MapStruct 正确地进行字段的映射。
@Mapping(source = "age", target = "ageString")
// target() 必须添加,source()可以不添加,则直接使用defaultValue指定默认值
@Mapping(target = "gender", source = "gender", defaultValue = "男")
// 指定日期字段的格式,使得源对象中的 createTime 日期字段能够按指定的格式(如 yyyy-MM-dd)转换为目标对象中的 String 类型字段。需要将日期字段从 Date 或 LocalDate 转换为 String。
@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
StudentDTO studentToStudentDTO(Student student);
}
4. 使用 MapStruct 进行对象转换
在你的应用程序中,你可以使用 DTOMapper
接口来实现对象的转换。
使用例子:
package com.example.study.controller;
import com.example.study.dto.StudentDTO;
import com.example.study.entity.Grade;
import com.example.study.entity.Hobby;
import com.example.study.entity.thread.Student;
import com.example.study.mapper.DTOMapper;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Date;
@Api(tags = {"MapStruct映射学习"})
@RestController
@RequestMapping("mapstruct")
public class MapStructController {
@GetMapping("/mapstructStudy")
public void mapstructStudy() {
Student student = new Student();
student.setId("1");
student.setName("lili");
student.setAge(23);
student.setCreateTime(new Date());
Hobby hobby = new Hobby();
hobby.setName("游戏");
hobby.setDescribe("喜欢玩竞技游戏、以及智力游戏");
Hobby hobby1 = new Hobby();
hobby1.setName("运动");
hobby1.setDescribe("跑步、散步、打球、骑行");
ArrayList<Hobby> list = new ArrayList<>();
list.add(hobby);
list.add(hobby1);
student.setHobbyList(list);
System.out.println("初始数据:" + student.toString());
System.out.println("------------");
StudentDTO studentDTO = DTOMapper.INSTANCE.studentToStudentDTO(student);
System.out.println("studentToStudentDTO结果:" + studentDTO.toString());
}
}
运行结果:
初始数据:Student(id=1, name=lili, age=23, gender=null, createTime=Tue Feb 11 14:35:21 CST 2025, hobbyList=[Hobby(name=游戏, describe=喜欢玩竞技游戏、以及智力游戏), Hobby(name=运动, describe=跑步、散步、打球、骑行)]) ------------ studentToStudentDTO结果:StudentDTO(name=lili, ageString=23, gender=男, createTime=2025-02-11, course=null, score=null, hobbyList=[Hobby(name=游戏, describe=喜欢玩竞技游戏、以及智力游戏), Hobby(name=运动, describe=跑步、散步、打球、骑行)])
5. 生成映射代码
MapStruct 会在编译时自动生成实现类,通常生成的类位于 target/generated-sources/annotations
目录下。请确保该目录中存在生成的类,该类会包含实际的转换代码。你不需要手动编写这些转换代码,MapStruct 会在编译时自动生成。
生成的代码(大致结构):
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 1.8.0_191 (Oracle Corporation)"
)
public class DTOMapperImpl implements DTOMapper {
@Override
public StudentDTO studentToStudentDTO(Student student) {
.....
}
}
三. @Mapping 属性介绍
@Mapping
注解是 MapStruct 框架中的一个核心注解,用于定义源对象与目标对象之间的字段映射关系。它通常用于方法级别,标识源字段和目标字段之间的对应关系,允许开发者在映射时进行详细的配置和自定义。
1.源/目标字段映射相关
1. source
-
类型:
String
-
描述: 指定源字段的名称。可以为空,表示使用默认的映射规则(即源字段名与目标字段名相同)。
字段名不同的映射
假设 Student
类中的Integer
类型的 age
字段需要映射到 StudentDTO
类中的String
类型的 ageString
字段。可以通过 @Mapping
注解指定字段映射关系。
// 用于指定在映射时,源对象中的 age 字段应该映射到目标对象中的 ageString 字段。这个注解主要在源和目标对象的字段名称不一致时使用,帮助 MapStruct 正确地进行字段的映射。
@Mapping(source = "age", target = "ageString")
2. target
-
类型:
String
-
描述: 指定目标字段的名称。必须设置这个值,用于指定目标字段映射。
@Mapping(source = "age", target = "ageString")
2.默认值
1. defaultValue
如果源字段为 null
,则使用 defaultValue
设置的默认值填充目标字段。
简单的常量值提供的默认值(如 String
、int
等),适用于不需要复杂计算的场景
-
类型:
String
-
描述:
defaultValue
用于指定一个简单的常量值作为目标字段的默认值。当源字段为null
时,会使用这个常量值填充目标字段。 -
适用场景: 用于为目标字段提供一个固定的、简单的默认值,常用于简单的类型(如
String
、int
、boolean
等)。
@Mapping(target = "gender", source = "gender", defaultValue = "男")
2.defaultExpression
适用于需要复杂计算或动态生成默认值的场景,可以使用 Java 表达式。
-
类型:
String
-
描述:
defaultExpression
用于指定一个 Java 表达式,如果源字段为null
时,MapStruct 将执行该表达式并将结果赋给目标字段。defaultExpression
可以包含更复杂的逻辑,如调用方法、条件判断、甚至创建对象等。 -
适用场景: 当你需要根据某些条件生成默认值,或者需要执行更复杂的计算时,使用
defaultExpression
更为合适。
//defaultExpression 是 java(18),这意味着如果源字段 age 为 null,目标字段 age 将被赋值为 18,不过这时 18 是一个 Java 表达式。
@Mapping(target = "age", source = "age", defaultExpression = "java(18)")
//defaultExpression 包含一个 Java 条件表达式:如果源对象的 age 为 null,则使用 18,否则使用源对象的实际 age 值。
@Mapping(target = "age", source = "age", defaultExpression = "java(person.getAge() == null ? 18 : person.getAge())")
3. constant
-
类型:
String
-
描述: 如果需要将源字段映射为一个常量值,可以使用这个属性来设置常量值。
// UserDTO 类
public class UserDTO {
private String name;
private String email;
private String status; // 这个字段的值不影响最终的目标对象
// getter 和 setter
}
// UserEntity 类
public class UserEntity {
private String name;
private String email;
private String status; // 我们希望将这个字段的值设置为固定的 "ACTIVE"
// getter 和 setter
}
// Mapper 接口
@Mapper
public interface UserMapper {
// 即使在 UserDTO 中 status 字段的值是 "INACTIVE",通过 @Mapping(target = "status", constant = "ACTIVE") 配置,MapStruct 会把 UserEntity 中的 status 字段设置为 "ACTIVE",而不是直接映射 UserDTO 中 的status 字段。
@Mapping(target = "status", constant = "ACTIVE") // 设置 status 字段为 "ACTIVE"
UserEntity toEntity(UserDTO userDTO);
}
3.格式化
1. numberFormat
指定数值类型字段(如 BigDecimal
、Double
)的格式。例如,格式化为带有两位小数的字符串。
-
类型:
String
-
描述: 用于指定数字字段的格式化模式。用于将数字格式化为特定的字符串形式。
@Mapping(target = "score", source = "grade.score", numberFormat = "#0.00")
2. dateFormat
指定日期类型字段(如 Date
、LocalDate
、LocalDateTime
)的格式。
-
类型:
String
-
描述: 用于指定日期字段的格式化模式。通常在日期类型字段映射时使用。
@Mapping(target = "dateOfBirth", source = "birthDate", dateFormat = "yyyy-MM-dd")
4.映射控制
1. qualifiedByName
指定使用自定义转换方法的名称。通过该属性可以调用 @Named
注解的方法来进行自定义转换。
-
类型:
String[]
-
描述: 与
qualifiedBy
一样,指定方法的名称,来精确匹配特定的映射方法。
@Mapping(target = "createTime", source = "createTime", qualifiedByName = "customDateFormat")
StudentDTO studentToStudentDTO(Student student);
// 自定义方法customDateFormat进行复杂的字段处理
@Named("customDateFormat")
default String customDateFormat(Date date) {
if (date != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
return null;
}
2. qualifiedBy
指定使用自定义转换方法的 @Qualifier
注解的类或方法。这与 qualifiedByName
类似,但 qualifiedBy
是基于自定义的 @Qualifier
类来定位转换方法。
-
类型:
Class[]
-
描述: 允许指定自定义的注解,用于精确选择用于映射的方法
// StatusConverter 类
import org.mapstruct.Qualifier;
@Qualifier
public @interface StatusQualifier {
}
// StatusConverter 转换器类
public class StatusConverter {
@StatusQualifier
public String convert(int statusCode) {
switch (statusCode) {
case 1:
return "ACTIVE";
case 2:
return "INACTIVE";
default:
return "UNKNOWN";
}
}
}
// UserMapper 接口:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(uses = StatusConverter.class) // 引用 StatusConverter
public interface UserMapper {
// 根据StatusQualifier的StatusConverter中的convert方法定义,UserDTO的int类型的statusCode会转成相应的String类型的status
@Mapping(target = "status", source = "statusCode", qualifiedBy = StatusQualifier.class)
UserEntity toEntity(UserDTO userDTO);
}
uses使用
@Mapper(uses = StatusConverter.class) // 引用 StatusConverter
// @Mapper(uses = ...) 可以使用多个类。你只需要将多个类传递给 uses 属性,MapStruct 会使用这些类进行转换。如果你有多个转换器或辅助类,可以按以下方式进行:
@Mapper(uses = {StatusConverter.class, AnotherConverter.class, ThirdConverter.class})
3.conditionQualifiedBy-待完善
好奇怪,我设置了,但是没有生效,没有测试成功,就不加上代码了。。。。
-
类型: Class<? extends Annotation>[]
-
描述: 指定条件注解,只有满足指定条件时,映射才会执行。
4.conditionQualifiedByName-待完善
-
类型: String[]
-
描述: 与 conditionQualifiedBy 类似,指定条件映射方法的名称。
conditionQualifiedByName
指定一个 静态方法,返回 boolean
类型,来判断是否要进行映射。使用正确的路径,MapStruct 会根据条件方法的签名和方法所在的上下文来识别该方法,因此你需要确保方法签名和配置的参数类型都一致。
5.conditionExpression-待完善
-
类型: String
-
描述: 使用表达式指定映射的条件,只有当这个表达式为 true 时,字段映射才会执行。
6.mappingControl-待完善
-
类型: Class<? extends Annotation>
-
描述: 用于指定映射控制的注解类。默认是 MappingControl.class,可以通过自定义映射控制来改变映射过程中的行为。
5.忽略字段
1. ignore
指示 MapStruct 忽略目标字段的映射。通常用于目标字段不需要映射或需要排除某个字段时。
-
类型:
boolean
-
描述: 如果为
true
,表示忽略源字段的映射,目标字段不会受到影响。这对于一些不需要映射的字段非常有用。
@Mapping(target = "password", ignore = true)
6.自定义表达式
1. expression
指定一个 Java 表达式进行转换。这对于自定义的逻辑非常有用,例如连接字符串、调用方法等。
-
类型:
String
-
描述: 使用 Java 表达式进行自定义映射。通过此属性可以编写复杂的表达式来映射源字段到目标字段。
source
和 expression
不能同时使用,只能选择其中一种
// 将 firstName 和 lastName 拼接为 fullName
@Mapping(target = "fullName", expression = "java(person.getFirstName() + \" \" + person.getLastName())")
7.处理空值(null)的映射行为
1. nullValueCheckStrategy
nullValueCheckStrategy
属性用于控制在进行类型转换时是否检查源对象的字段是否为 null
。这可以影响映射过程中的空值处理。
可选的策略:
-
ON_IMPLICIT_CONVERSION
: 这是默认策略,表示只有在需要类型转换时才会检查源字段是否为null
。如果没有需要转换的字段,则不会检查。 -
ALWAYS
: 无论是否需要进行类型转换,都会检查源字段是否为null
。 -
NEVER
: 不会检查源字段是否为null
,即使源字段为null
,目标字段也会进行映射。
@Mapper
public interface PersonMapper {
@Mapping(target = "age", source = "age", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
PersonDTO toPersonDTO(Person person);
}
在这个例子中,nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
表示无论 age
字段是否需要类型转换,都会检查 age
是否为 null
。
2. nullValuePropertyMappingStrategy
nullValuePropertyMappingStrategy
属性用于指定当源字段为 null
时,目标对象的属性应如何处理。它主要控制源字段为空时,目标字段的行为,具体可选的策略有:
(1).SET_TO_NULL
: 这是默认值,表示当源字段为 null
时,目标字段将被设置为 null
。
@Mapper
public interface PersonMapper {
// 表示当源对象的 phoneNumber 字段为 null 时,目标对象的 phoneNumber 字段将被设置为 null。
@Mapping(target = "phoneNumber", source = "phoneNumber", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL)
PersonDTO toPersonDTO(Person person);
}
(2).IGNORE
: 如果源字段为 null
,则目标字段将保持不变,即忽略空值的映射。
-
源对象的
address
字段为null
,目标对象的address
字段保持不变,如果目标对象的address
字段之前已经是某个非null
值,它将继续保持该值。 -
源对象的
address
字段有值,目标对象的address
字段会被更新为源对象中的值。
//假设 personDTO 最初的 address 字段为 "Old Address"
// person 的 address 字段为null
//输出 "Old Address",不会被设置为 null
@Mapping(target = "address", source = "address", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
PersonDTO toPersonDTO(Person person);
8.结果类型和依赖关系
1.targetType
指定目标字段的类型。通常用于源字段和目标字段类型不匹配的情况。
// UserDTO 类
public class UserDTO {
private String createdAt; // String 类型
// 其他字段和 getter/setter
}
// UserEntity 类
public class UserEntity {
private Date createdAt; // Date 类型
// 其他字段和 getter/setter
}
// Mapper 接口
@Mapper
public interface UserMapper {
// 将 UserDTO 中的 createdAt(一个字符串)转换为 UserEntity 中的 createdAt(一个日期对象)。由于类型不同,我们可以使用 targetType 来进行类型转换。
@Mapping(target = "createdAt", source = "createdAt", targetType = Date.class)
UserEntity toEntity(UserDTO userDTO);
}
MapStruct 支持一些高级功能,例如自定义映射、不同字段名之间的映射、集合的映射等。以下是一些常见的使用场景。
2.resultType
-
类型: Class<?>
-
描述: 指定结果类型。可以用于明确映射的目标类型,如果没有特别指定,默认使用目标类型。
在 MapStruct 中,resultType
属性用于指定返回类型(即方法的返回类型)。它的默认值为 void.class
,这意味着在没有明确指定时,返回类型默认是 void
。
假设我们有一个源类和目标类,并且我们希望通过 MapStruct 进行字段映射。
// 源类
public class Source {
private String name;
private int age;
// Getters and Setters
}
// 目标类
public class Target {
private String fullName;
private int age;
// Getters and Setters
}
在 MapStruct 中,我们使用接口来定义映射规则。这里是一个简单的 Mapper 示例:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper
public interface PersonMapper {
// 映射规则: 将 Source 的 name 映射到 Target 的 fullName
@Mapping(source = "name", target = "fullName")
Target sourceToTarget(Source source);
// 如果需要自定义返回类型,使用 resultType 来指定
@Mapping(source = "name", target = "fullName", resultType = String.class)
String customMapping(Source source);
}
在上面的代码中,resultType = String.class
表明方法 customMapping
将返回 String
类型,而不是 Target
类型。通常,resultType
主要用于更复杂的场景中,指定映射的结果类型。
使用 MapStruct 时,MapStruct 会自动生成映射的实现类。在这种情况下,它会生成一个实现了 PersonMapper
接口的类,并提供 sourceToTarget
和 customMapping
方法的实现。
resultType
在以下情况下有用:
-
自定义返回类型:如果你想通过映射方法返回一个特定的类型,而不是目标对象本身。
-
更灵活的转换:当你需要将映射后的结果转换成一个不同类型的值时,例如从源对象提取特定的字段,并返回一个计算结果。
3.dependsOn
-
类型: String[]
-
描述: 指定该字段依赖的其他字段。只有在依赖的字段映射完成后,才会进行当前字段的映射。
//dependsOn = {"ageString", "gender"} 表示在计算 name 字段的值之前,ageString 和 gender 字段需要先被映射。dependsOn中的ageString,gender是目标字段,不是源字段!!!
@Mapping(target = "name", expression = "java(student.getName() + \" \" + student.getAge()+ \" \" + student.getGender())",dependsOn = {"ageString","gender"})
四.@Mapper的属性
@Mapper
是 MapStruct 中用于标记接口为映射器接口的注解。通过这个注解,MapStruct 可以在编译时生成相应的实现类,完成对象之间的属性映射。这个注解提供了多个可配置的属性,允许你定制映射行为。
1.引入类
1.uses
-
自动帮你用。
uses
引入的类,MapStruct 会自动帮你去调用类里面的方法来进行字段转换。比如你有个类里面有一些复杂的转换逻辑,MapStruct 会知道并自动使用它来完成转换。例子:如果你有个类负责日期格式转换,通过
uses
引入这个类,MapStruct 会自动使用它来处理日期的转换。
@Mapper
注解中的 uses
属性用于指定辅助类(例如工具类或其他映射器)以供 MapStruct 在生成映射代码时使用。当你有一些通用的转换逻辑,或想在映射过程中使用某些外部的方法时,uses
就非常有用。
// 单个引入
@Mapper(uses = DateMapper.class)
// 当有多个时
@Mapper(uses = {AgeUtils.class, ConditionUtils.class})
2.imports
-
只是引入,不会自动用。
imports
引入的类,只是为了让你在代码中能够使用它们,但是 MapStruct 不会自动帮你调用。你需要自己在映射方法里面手动去调用这个类的方法。例子:如果你有一个工具类,它提供了一些帮助方法(但这些方法不是转换逻辑),你用
imports
引入它之后,你就能在自己的代码里手动调用它。
用于引入额外的类,这些类可以在映射方法中使用。
@Mapper(imports = {DateUtils.class})
和传统的 import com.example.DateUtils;
语句不一样吗?都是引入类而已!!!为什么还要使用@Mapper(imports = {DateUtils.class})
呢,以下是解释:
-
局部作用域:
@Mapper(imports = {DateUtils.class})
主要影响的是 生成的映射器实现代码。虽然你在 Java 类中有import
语句,但是 MapStruct 生成的代码可能需要显式知道某些类。例如,在映射方法中使用自定义的工具类方法时,@Mapper(imports = {...})
会确保 MapStruct 在其生成的代码中能够正确地引用这些类,而不会因为无法识别这些类而导致编译错误。 -
特定用途:你可能会在
@Mapper
的实现中需要访问一些不属于当前类的工具类或第三方库的类,而这些类并不直接在你的映射方法的源代码中可见。因此,@Mapper(imports = {...})
显式地告诉 MapStruct 哪些类需要被引入生成的代码中。 -
减少冲突和明确范围:在一些复杂的项目中,你可能会有多个类的
import
,在这种情况下,使用@Mapper(imports = {...})
可以确保 MapStruct 只引入你真正需要的类,而不是所有的import
都进入生成的映射代码中。这有助于避免不必要的类被引入,保持代码的简洁性和清晰性。
// 工具类
public class DateUtils {
public static String formatDateToString(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
}
@Mapper(imports = {DateUtils.class}) // 显式导入 DateUtils 类
public interface PersonMapper {
@Mapping(source = "dateOfBirth", target = "dob", qualifiedByName = "formatDate")
PersonDTO personToPersonDTO(Person person);
// 自定义映射方法,调用工具类
@Named("formatDate")
default String formatDate(Date date) {
return DateUtils.formatDateToString(date);
}
}
2.控制字段映射行为
1.unmappedSourcePolicy/unmappedTargetPolicy
unmappedSourcePolicy
和 unmappedTargetPolicy
是 MapStruct 中用来控制字段映射行为的两个属性,特别是在源字段或目标字段没有明确的映射关系时。它们通常用于处理 源字段 或 目标字段 与目标对象/源对象中没有直接映射的情况。
-
unmappedSourcePolicy
:用于控制源字段没有映射关系时的行为(源字段有字段,但目标字段没有对应字段时)。unmappedSourcePolicy
的默认值是ReportingPolicy.IGNORE
。 -
unmappedTargetPolicy
:用于控制目标字段没有映射关系时的行为(目标字段有字段,但源字段没有对应字段时)。unmappedTargetPolicy
的默认值是ReportingPolicy.ERROR
。
这两个属性的值通常是 ReportingPolicy.IGNORE
或 ReportingPolicy.WARN
:
-
ReportingPolicy.IGNORE
:表示忽略没有映射的字段。 -
ReportingPolicy.WARN
:表示当某些字段没有映射时,发出警告。
假设我们有两个类:Person
和 Employee
,它们有一些相似的字段,但并不是所有字段都对应。
现在,我们希望用 MapStruct 生成一个映射器,将 Person
转换为 Employee
,但 Person
中的 address
字段并不直接映射到 Employee
中。
-
当你执行这个映射时:
-
name
和age
会被正常映射。 -
address
字段会被忽略,因为它没有映射到Employee
的字段,但由于unmappedSourcePolicy
设置为WARN
,你会在编译期间看到类似于以下的警告:
Warning: Unmapped source property: "address" in source type "Person".
-
-
如果
Employee
中添加了一个字段,比如salary
,而Person
中没有对应的字段:-
salary
会被忽略,因为unmappedTargetPolicy
设置为IGNORE
,并且 MapStruct 不会发出警告或错误。
-
// 源类
public class Person {
private String name;
private int age;
private String address; // 假设没有映射到 Employee 的字段
// getters and setters
}
// 目标类
public class Employee {
private String name;
private int age;
// getters and setters
}
// 映射接口
// unmappedSourcePolicy = ReportingPolicy.WARN:如果 `Person` 中有字段没有映射到 `Employee`(例如 `address` 字段),MapStruct 会发出警告,告诉你存在源字段没有映射的情况。
// unmappedTargetPolicy = ReportingPolicy.IGNORE:如果 `Employee` 中有字段没有对应的源字段(假设我们对 `Employee` 添加了额外的字段),MapStruct 会忽略这些目标字段,而不会报错或警告。
@Mapper(unmappedSourcePolicy = ReportingPolicy.WARN, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface PersonToEmployeeMapper {
Employee personToEmployee(Person person);
}
2.mappingControl
mappingControl
指定一个自定义的控制映射行为的注解。默认是 MappingControl.class
。
mappingControl
是 MapStruct 中的一个属性,允许你指定一个自定义的注解类来控制映射过程的行为。通过自定义映射控制类,你可以在映射时定义一些额外的逻辑或规则。
在 MapStruct 中,映射控制类通常用于在映射操作时执行额外的逻辑,例如,条件检查、字段值转换等。
使用 mappingControl
的基本步骤
-
创建自定义的映射控制注解类:你需要定义一个实现了
MappingControl
接口的类,并在映射器上通过mappingControl
属性来引用这个控制类。 -
应用映射控制:然后在你的 Mapper 接口中使用该自定义注解,MapStruct 会在映射时应用你定义的控制行为。
1. 定义自定义映射控制类
首先,定义一个自定义的映射控制类,它实现了 MapStruct 的 MappingControl
接口。你可以在这个类中指定映射行为,比如可以修改某些字段的转换规则或添加条件判断。
import org.mapstruct.MappingControl;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mapper;
// 需要实现MappingControl类
public class MyCustomMappingControl implements MappingControl {
@Override
public <S, D> void controlMapping(S source, @MappingTarget D target) {
// 在这里执行自定义的映射行为,例如添加一些条件判断
if (source != null && target != null) {
// 你可以在这里做一些检查或者修改目标对象的属性
System.out.println("Custom mapping logic is applied!");
}
}
}
2. 在 Mapper 接口中应用自定义的映射控制
接下来,你在 Mapper 接口上使用 mappingControl
属性引用自定义的控制类。
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(mappingControl = MyCustomMappingControl.class)
public interface MyMapper {
@Mapping(source = "sourceField", target = "targetField")
Target map(Source source);
}
在这个例子中,MyCustomMappingControl
将在 map
方法执行时被调用,你可以在自定义的控制类中对源对象和目标对象之间的映射进行任何额外的处理。
3.CollectionMappingStrategy
如果你不显式定义 collectionMappingStrategy
,MapStruct 会使用 默认策略。默认情况下,MapStruct 会使用 SETTER_PREFERRED
策略。
这意味着,当映射集合时,MapStruct 首先会尝试使用目标类中的 set
方法来进行映射,如果目标类没有 set
方法,那么才会尝试使用 add
方法(如果存在)。
用一个通俗易懂的例子来说明这四个 CollectionMappingStrategy
策略的意义和使用场景。
假设我们有以下两个类:
class Person {
private String name;
private List<String> hobbies;
// 省略构造方法和 getter/setter
}
class PersonDTO {
private String name;
private List<String> hobbies;
// 省略构造方法和 getter/setter
}
现在,我们希望将 Person
类中的 hobbies
集合映射到 PersonDTO
类中的 hobbies
集合,MapStruct 就可以帮助我们完成这个任务。
1. ACCESSOR_ONLY
这个策略告诉 MapStruct,只能通过 get
和 set
方法来映射集合。
假设 PersonDTO
类没有 setHobbies
方法,只有 addHobby
方法。如果你选择了 ACCESSOR_ONLY,那么 MapStruct 会发现 PersonDTO
没有 set
方法可用,结果无法完成映射。
使用场景:当目标类的集合字段只能通过 getter 和 setter 方法进行访问,而没有其他特殊的方法(例如 add
方法)时,选择这个策略。
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ACCESSOR_ONLY)
public interface PersonMapper {
PersonDTO personToPersonDTO(Person person);
}
2. SETTER_PREFERRED
这个策略优先使用 set
方法来映射集合。如果没有 set
方法,才会尝试使用 add
方法。
例如,PersonDTO
类有一个 setHobbies
方法,MapStruct 会优先使用 setHobbies
来替代直接通过 add
方法填充集合。
使用场景:当目标类拥有 set
方法来设置集合,并且你希望优先使用这些 set
方法进行集合的映射时,选择此策略。
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.SETTER_PREFERRED)
public interface PersonMapper {
PersonDTO personToPersonDTO(Person person);
}
举例:
-
如果
PersonDTO
有setHobbies
方法,它会直接使用这个方法。 -
如果
PersonDTO
没有setHobbies
方法,但有addHobby
方法,MapStruct 会使用addHobby
方法。
3. ADDER_PREFERRED
这个策略优先使用 add
方法来填充集合。如果没有 add
方法,则回退到使用 set
方法。
假设 PersonDTO
类只有 addHobby
方法没有 setHobbies
,在选择了 ADDER_PREFERRED 后,MapStruct 会使用 addHobby
方法将每个爱好添加到 PersonDTO
的 hobbies
集合中。
使用场景:当目标类的集合只能通过 add
方法来操作时,选择此策略。
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface PersonMapper {
PersonDTO personToPersonDTO(Person person);
}
4. TARGET_IMMUTABLE
这个策略适用于不可变目标类。MapStruct 不会使用 set
或 add
方法,而是通过构造函数来填充目标类中的集合。
假设 PersonDTO
类的 hobbies
集合是通过构造函数进行设置的,MapStruct 会通过构造函数来映射这个集合,而不会使用 set
或 add
方法。
使用场景:当目标类是不可变的(例如使用 final
修饰字段或没有 set
和 add
方法),并且只能通过构造函数来设置集合时,选择此策略。
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE)
public interface PersonMapper {
PersonDTO personToPersonDTO(Person person);
}
总结:
-
ACCESSOR_ONLY:只通过
get
和set
方法来映射集合。如果没有set
方法,映射就失败。 -
SETTER_PREFERRED:优先使用
set
方法,如果没有set
方法,才尝试使用add
方法。 -
ADDER_PREFERRED:优先使用
add
方法,如果没有add
方法,才尝试使用set
方法。 -
TARGET_IMMUTABLE:用于不可变目标类,通过构造函数来设置集合,没有
set
或add
方法。
这些策略的选择通常取决于你希望如何管理目标类的集合以及如何映射数据,特别是你目标类的设计和访问方法。
3.组件模型
1.componentModel
用于指定生成的映射器的组件模型。常见的选项有 "default"
、"spring"
(Spring Bean)等。
-
componentModel = "spring"
使得 MapStruct 生成的映射器类成为 Spring Bean,你可以通过 Spring 的依赖注入自动获取映射器实例。Spring 会自动为你管理映射器的生命周期,使用依赖注入 (DI) 进行注入。 -
你不再需要显式调用
Mappers.getMapper(DTOMapper.class)
来获取映射器,Spring 会帮你注入。
1. 启动类和配置:
假设你的项目是一个 Spring Boot 应用,使用 @MapperScan
注解来扫描你的映射器接口,并且使用 componentModel = "spring"
配置来告诉 MapStruct 将映射器注册为 Spring Bean。
启动类 (@SpringBootApplication
) 示例:
@SpringBootApplication
@MapperScan("com.example.study.mapper") // 扫描所有的 Mapper 接口
public class StudyApplication {
public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}
-
@MapperScan("com.example.study.mapper")
:这行代码告诉 Spring Boot 扫描com.example.study.mapper
包下的所有映射器接口,并将它们注册为 Spring Bean。
2. 映射器接口配置:
在你的映射器接口上,使用 @Mapper(componentModel = "spring")
注解来指定 MapStruct 生成的映射器是一个 Spring Bean。
package com.example.study.mapper;
import org.mapstruct.Mapper;
import com.example.study.dto.SourceDTO;
import com.example.study.dto.TargetDTO;
@Mapper(componentModel = "spring")
public interface DTOMapper {
TargetDTO sourceToTarget(SourceDTO source);
}
-
这里的
componentModel = "spring"
会使 MapStruct 自动将DTOMapper
接口实现为 Spring Bean,你可以直接在 Spring 的上下文中注入它。
3. 使用映射器:
使用自动注入的方式:
在 Spring 的组件中,你可以通过 依赖注入 (DI) 获取 DTOMapper
实例,而不需要手动调用 Mappers.getMapper()
。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyServiceImpl {
// 注入DTOMapper映射接口
private final DTOMapper dtoMapper;
@Autowired
public MyServiceImpl(DTOMapper dtoMapper) {
this.dtoMapper = dtoMapper;
}
public TargetDTO convert(SourceDTO source) {
// 不需要这样使用了
// TargetDTO targetDTO = DTOMapper.INSTANCE.sourceToTarget(source);
// 进行使用
return dtoMapper.sourceToTarget(source);
}
}
在上面的例子中,DTOMapper
被自动注入到 MyService
中,因为它已经是一个 Spring Bean 了。你不再需要通过 Mappers.getMapper(DTOMapper.class)
来获取映射器实例。
4. 通过构造函数注入:
当你使用 Spring 的构造函数注入时,Spring 会自动注入 DTOMapper
。你可以省略显式的 @Autowired
注解,Spring 会自动检测并注入需要的依赖。
5. 不需要手动获取映射器:
当你使用 componentModel = "spring"
时,DTOMapper中你不需要再使用 Mappers.getMapper(DTOMapper.class)
来获取映射器实例。Spring 会自动处理。
// 这个方法不再需要了
DTOMapper dtoMapper = Mappers.getMapper(DTOMapper.class);
报错,试了很多方法,没解决!!!!!!!!!有人也有这个问题吗。。。
Parameter 0 of constructor in com.example.study.service.impl.MapStructServiceImpl required a single bean, but 2 were found:
- DTOMapperImpl: defined in file [D:\project\studey-test\target\classes\com\example\study\mapper\DTOMapperImpl.class]
- DTOMapper: defined in file [D:\project\studey-test\target\classes\com\example\study\mapper\DTOMapper.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
4.指定生成实现类的名称和包名
1.implementationName` /
implementationName
用于指定生成的实现类的名称。默认情况下,MapStruct 会根据接口名生成实现类名,但你可以通过配置 implementationName
来修改生成的实现类的名称。
假设你有一个接口 DTOMapper
,你希望 MapStruct 生成的实现类的名称为 CustomDTOMapperImpl
,而不是默认的 DTOMapperImpl
。
@Mapper(implementationName = "CustomDTOMapperImpl", componentModel = "spring")
public interface DTOMapper {
TargetDTO sourceToTarget(SourceDTO source);
}
2.implementationPackage
implementationPackage
用于指定生成的实现类的包名。MapStruct 默认会将生成的实现类放在与接口相同的包下,你可以通过 implementationPackage
来改变它
生成类的位置:
-
默认情况下: MapStruct 会将实现类放在
target/generated-sources/annotations
目录下,而不是放在源码的src
目录中(与接口的源文件包名相同的位置)。这是因为这些生成的类是编译时生成的,通常不会直接在源码中看到。它们会被编译器自动处理,并加入到最终的构建中。 -
使用
implementationPackage
配置: 如果你想改变生成实现类的包名,implementationPackage
属性会控制生成类的包名,但仍然会把生成的类放在target/generated-sources/annotations
目录中(除非你自定义构建配置,改变了生成源文件的目录结构)。@Mapper(implementationPackage = "com.example.mapper.impl", componentModel = "spring") public interface DTOMapper { TargetDTO sourceToTarget(SourceDTO source); }
这种情况下,MapStruct 会将生成的实现类放到
target/generated-sources/annotations/com/example/mapper/impl
目录下,而不是源码目录中的com/example/mapper/impl
包下。
5.指定在遇到意外值时抛出的异常类型
1.unexpectedValueMappingException-待完善
unexpectedValueMappingException
: 用于指定在遇到意外值时抛出的异常类型,默认为 IllegalArgumentException.class
。
unexpectedValueMappingException
是 MapStruct 中的一种配置选项,用于指定在映射过程中遇到意外或不支持的值时抛出的异常类型。默认情况下,当 MapStruct 遇到无法映射的值时,会抛出 IllegalArgumentException
。通过使用 unexpectedValueMappingException
,你可以自定义该异常类型,选择抛出一个自定义的异常类。
6.是否抑制时间戳
1.suppressTimestampInGenerated
suppressTimestampInGenerated
: 是否在生成的代码中抑制时间戳。默认是 false
(保留时间戳)。
false
(包含时间戳)
true
(不包含时间戳)
suppressTimestampInGenerated
是 MapStruct 中的一个配置选项,主要用于控制生成的映射代码中是否包含时间戳。这个选项在一些代码生成工具或框架中使用,以决定是否在生成的 Java 文件头部注释中包含时间戳信息。默认情况下,MapStruct 会在生成的源代码顶部添加生成时间戳的信息。
这个选项允许你控制是否在生成的代码中抑制时间戳信息,默认是保留的。
当 suppressTimestampInGenerated = true
时,MapStruct 在生成的代码中不会包含时间戳,而是保持清洁的代码头部。
举个例子:
假设你有一个使用 MapStruct 的映射接口,如下所示:
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(suppressTimestampInGenerated = true)
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDTO carToCarDTO(Car car);
}
生成的代码:
如果 suppressTimestampInGenerated
设置为 false
(默认值),生成的代码可能会在文件顶部包含类似如下的注释:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-02-12T17:02:25+0800",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 1.8.0_191 (Oracle Corporation)"
)
public class CarMapperImpl implements CarMapper {
.......
}
但是,如果你将 suppressTimestampInGenerated
设置为 true
,生成的代码头部将不会包含时间戳,可能就只会是:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 1.8.0_191 (Oracle Corporation)"
)
public class CarMapperImpl implements CarMapper {
.......
}
适用场景:
-
清晰的代码:有时生成的时间戳可能会让代码显得杂乱,特别是在多次生成时,你不希望文件中充斥时间信息。此时,使用
suppressTimestampInGenerated = true
可以避免这一点。 -
符合代码风格要求:一些项目要求生成的代码头部不包含任何注释或外部元数据,
suppressTimestampInGenerated
提供了一个简单的方法来关闭这一功能。
五.多个源对象映射
1. 多个源对象映射到一个目标对象
如果你有多个源对象,想将它们映射到一个目标对象,可以通过使用 @Mapping
注解并指定多个源字段来实现。例如,你可以通过构造方法将多个字段映射到目标类,或者通过使用 MapStruct 的 @Mapping
来在方法中指定源对象的多个属性
假设你有两个源对象:Student
和 Course
,并且想将这两个源对象的字段映射到目标对象 StudentCourseDTO
。
// 源对象 Student
public class Student {
private String name;
private Integer age;
// getters and setters
}
// 源对象 Course
public class Course {
private String courseName;
private String instructor;
// getters and setters
}
// 目标对象 StudentCourseDTO
public class StudentCourseDTO {
private String name;
private Integer age;
private String courseName;
private String instructor;
// getters and setters
}
你可以使用 MapStruct 来同时映射这两个源对象:
@Mapper(componentModel = "spring")
public interface StudentCourseMapper {
// 使用多个 @Mapping 来指定不同源对象的字段映射到目标对象
// 一定要在字段前面,标明对应的实体!!!!!!,否则会报错
@Mappings({
@Mapping(source = "student.name", target = "name"), //标明来自student
@Mapping(source = "student.age", target = "age"),
@Mapping(source = "course.courseName", target = "courseName"), //标明来自course
@Mapping(source = "course.instructor", target = "instructor")
})
StudentCourseDTO toStudentCourseDTO(Student student, Course course);
}
在这个例子中,toStudentCourseDTO
方法接受两个源对象 Student
和 Course
,并将它们的属性映射到目标对象 StudentCourseDTO
中。
Student student = new Student();
student.setName("李华");
student.setAge(25);
Course course = new Course();
course.setCourseName("数学");
course.setInstructor("陈教练");
StudentCourseDTO dto = studentCourseMapper.toStudentCourseDTO(student, course);
System.out.println(dto.getName()); // 李华
System.out.println(dto.getAge()); // 25
System.out.println(dto.getCourseName()); // 数学
System.out.println(dto.getInstructor()); // 陈教练
2. 一个源对象映射到多个目标对象
如果你想将一个源对象映射到多个目标对象,你可以创建多个不同的映射方法,或者通过在映射方法中返回多个不同类型的目标对象。
假设你有一个 Student
对象,想将它映射为 StudentDTO
和 StudentSummaryDTO
两个不同的目标对象。
// 目标对象 StudentDTO
public class StudentDTO {
private String name;
private Integer age;
// getters and setters
}
// 目标对象 StudentSummaryDTO
public class StudentSummaryDTO {
private String name;
// getters and setters
}
你可以定义多个映射方法来实现这个需求:
@Mapper(componentModel = "spring")
public interface StudentMapper {
// 将 Student 映射到 StudentDTO
@Mapping(source = "name", target = "name")
@Mapping(source = "age", target = "age")
StudentDTO toStudentDTO(Student student);
// 将 Student 映射到 StudentSummaryDTO
@Mapping(source = "name", target = "name")
StudentSummaryDTO toStudentSummaryDTO(Student student);
}
Student student = new Student();
student.setName("李华");
student.setAge(25);
StudentDTO studentDTO = studentMapper.toStudentDTO(student);
System.out.println(studentDTO.getName()); // 李华
System.out.println(studentDTO.getAge()); // 25
StudentSummaryDTO studentSummaryDTO = studentMapper.toStudentSummaryDTO(student);
System.out.println(studentSummaryDTO.getName()); // 李华
六.逆映射
谨慎使用,尤其是自定义了很多方法的时候,emmmmmn......
逆映射意味着将 DTO 映射回实体,或者将目标类型映射回源类型。这通常是双向映射的需求,前向映射和反向映射的规则是相似的。@InheritInverseConfiguration
可以让你重用前向映射中的映射规则并自动反转它们,避免重复代码。
逆映射的注意点:
-
字段类型一致性: 在进行双向映射时,确保源对象和目标对象的字段类型一致,或者确保类型转换是可行的。比如
String
到Integer
,或者自定义类型的转换。MapStruct 默认会处理这些转换,但如果你有自定义的转换需求,可能需要手动配置。 -
空值处理: 在进行反向映射时,需要确保处理
null
值的情况。如果源对象的某个字段是null
,你可能需要特殊的逻辑来处理反向映射。 -
嵌套对象映射: 如果 DTO 中包含嵌套的对象或集合,需要确保 MapStruct 的反向映射也能够正确处理这些嵌套对象。
-
方法参数顺序: 在映射方法中,参数的顺序会影响映射规则。在双向映射的情况下,确保前向方法和反向方法中的参数顺序是正确的。
常见错误和调试方法
-
字段类型不匹配:如果源对象和目标对象的字段类型不匹配,MapStruct 会尝试进行默认转换,但如果类型不兼容(例如
Integer
到String
),可能会报错。解决方法是使用自定义的转换器或者使用@Mapping
注解的qualifiedBy
来指定转换逻辑。 -
遗漏嵌套映射:如果
StudentDTO
或Student
中有嵌套的对象,而没有为这些嵌套对象创建映射方法,那么在反向映射时可能会导致NullPointerException
或其他错误。你需要确保嵌套对象也有相应的映射方法,并且可以通过@Mapping
注解在反向映射中显式指定它们。 -
多对多映射时的错误:如果源对象和目标对象有集合或数组属性,需要确保正确配置集合类型的映射。如果没有映射规则,可能会导致集合没有被正确转换。
-
参数顺序错误:在某些情况下,反向映射方法的参数顺序可能会被错误地处理,导致映射失败。
@InheritInverseConfiguration
在继承前向映射的配置时,确保参数顺序正确,但如果有多个参数,你需要特别小心。
假设我们有一个 Student
实体类和一个 StudentDTO
,并且我们希望实现从 Student
到 StudentDTO
的映射,以及从 StudentDTO
到 Student
的反向映射。
// Student 实体类
public class Student {
private String name;
private Integer age;
// getters and setters
}
// StudentDTO 类
public class StudentDTO {
private String name;
private Integer age;
// getters and setters
}
使用 MapStruct 创建映射接口
@Mapper(componentModel = "spring")
public interface StudentMapper {
// 从实体到 DTO 的映射
@Mapping(source = "name", target = "name")
@Mapping(source = "age", target = "age")
StudentDTO toStudentDTO(Student student);
// 使用 @InheritInverseConfiguration 来自动继承反向映射配置
@InheritInverseConfiguration
Student toStudent(StudentDTO studentDTO);
}
在上面的代码中,toStudentDTO
方法实现了从 Student
到 StudentDTO
的映射,而 toStudent
方法则通过 @InheritInverseConfiguration
注解自动继承了 toStudentDTO
方法的映射规则,从而实现了从 StudentDTO
到 Student
的反向映射。
Student student = new Student();
student.setName("李华");
student.setAge(25);
StudentDTO studentDTO = studentMapper.toStudentDTO(student);
System.out.println(studentDTO.getName()); // 李华
System.out.println(studentDTO.getAge()); // 25
Student studentFromDTO = studentMapper.toStudent(studentDTO);
System.out.println(studentFromDTO.getName()); // 李华
System.out.println(studentFromDTO.getAge()); // 25
七. 使用 @BeforeMapping
和 @AfterMapping
注解
MapStruct 提供了 @BeforeMapping
和 @AfterMapping
注解,允许你在映射开始之前或结束之后执行一些额外的操作。这对于需要在映射之前或之后对多个字段进行处理的场景非常有用。
@Mapper
public interface PersonMapper {
@Mapping(source = "name", target = "fullName")
@Mapping(source = "dateOfBirth", target = "birthDate", dateFormat = "yyyy-MM-dd")
PersonDTO personToPersonDTO(Person person);
@BeforeMapping
default void beforeMapping(Person person) {
// 在映射之前进行额外处理
if (person.getDateOfBirth() != null) {
// 可以在这里做一些处理,比如数据验证、转换等
}
}
@AfterMapping
default void afterMapping(@MappingTarget PersonDTO personDTO, Person person) {
// 在映射之后执行额外操作
if (personDTO != null) {
// 比如填充其他字段或进行额外的计算
}
}
}
在这个示例中,我们使用了 @BeforeMapping
和 @AfterMapping
来分别在映射前后执行一些额外的操作。
-
多个
@Mapping
注解:可以对不同字段使用不同的转换规则。 -
自定义方法:通过自定义方法进行更复杂的字段处理。
-
@Mapping
的expression
属性:使用 Java 表达式进行字段转换。 -
@BeforeMapping
和@AfterMapping
注解:在映射前后进行额外的处理。
这些方法可以帮助你在转换时同时处理多个字段,并且灵活地定制转换逻辑。
八.MapStruct 和 MyBatis 的 Mapper区别
它们在设计理念、应用场景和用法上有显著的区别,虽然它们的接口看起来有些相似,但实际用途和工作方式完全不同。以下是它们的主要区别和它们是否可以互相兼容的一些解答。
1. MapStruct 和 MyBatis 的工作原理
MapStruct:
-
主要用途:MapStruct 是一个 Java 编译时注解处理器,用于简化 Java Bean 之间的映射过程。它通过在编译时生成实现代码,帮助你将一个对象的字段映射到另一个对象的字段。
-
映射:MapStruct 主要用于在数据传输对象(DTO)和实体类之间转换,尤其是当你有类似
Person
到PersonDTO
这样的需求时。 -
处理方式:MapStruct 是通过
@Mapper
注解来定义映射关系,生成转换代码,而不涉及数据库操作。
MyBatis:
-
主要用途:MyBatis 是一个持久层框架,它帮助你将数据库操作与 Java 对象进行映射。你编写 XML 或注解来定义 SQL 查询,然后通过 MyBatis 执行这些查询,并自动将数据库结果映射为 Java 对象。
-
映射:MyBatis 的
Mapper
用于将数据库表的记录映射为 Java 对象,通常是用于执行数据库增、删、改、查(CRUD)操作。 -
处理方式:MyBatis 依赖于 XML 文件或者注解来定义 SQL 语句,并自动将结果集映射为 Java 实体类对象,或者将 Java 对象插入/更新到数据库。
2. MapStruct 和 MyBatis Mapper 的区别
特性 | MapStruct Mapper | MyBatis Mapper |
---|---|---|
用途 | 用于对象之间的转换(如 DTO 与实体类之间的转换)。 | 用于执行数据库操作,将 SQL 查询结果映射到 Java 对象,或者将 Java 对象持久化到数据库。 |
映射方式 | 基于注解,通过编译时生成映射代码。 | 基于 XML 或注解,依赖 SQL 查询。 |
执行方式 | 仅在 Java 对象之间进行字段映射,不涉及数据库操作。 | 执行 SQL 查询或更新操作,涉及数据库交互。 |
映射过程 | 编译时生成代码,性能高,完全静态。 | 运行时通过 SQL 执行,涉及数据库交互。 |
使用场景 | 数据转换、对象字段映射、DTO 和实体类的转换。 | 数据库操作、ORM(对象关系映射)。 |
依赖关系 | 不依赖数据库,只依赖 Java 对象。 | 依赖数据库,使用 SQL 查询操作数据库。 |
3. MapStruct 和 MyBatis Mapper 是否可以互用?
虽然 MapStruct 和 MyBatis 都使用类似的 @Mapper
注解来定义映射接口,但它们的用途和工作机制完全不同,不能直接互用。你不能直接将一个 MyBatis Mapper
用作 MapStruct 的 Mapper
。原因如下:
-
目的不同:MapStruct 是用来处理 Java 对象之间的字段映射,而 MyBatis 是用来处理数据库操作的。它们的应用场景完全不同。
-
实现方式不同:MapStruct 生成的是映射代码,而 MyBatis 使用 SQL 执行数据库操作并将结果映射到 Java 对象。
-
注解和接口不同:虽然它们使用
@Mapper
注解,但 MyBatis 的@Mapper
只是标记为 MyBatis 映射器接口,并没有任何映射逻辑。而 MapStruct 的@Mapper
是告诉编译器生成映射实现的标识。
4. 如何将 MapStruct 与 MyBatis 配合使用?
虽然它们不能直接互用,但你可以将 MapStruct 和 MyBatis 结合起来使用。常见的做法是在业务逻辑层进行数据转换时,先使用 MyBatis 执行数据库操作(查询或插入),然后用 MapStruct 将 MyBatis 查询返回的实体类转换为 DTO,反之亦然。这样,你可以在 MyBatis 执行数据库操作后,用 MapStruct 对数据进行转换处理。
示例:
假设你有一个 Person
实体类和一个 PersonDTO
,你使用 MyBatis 来查询数据库,然后用 MapStruct 来转换数据。
// MyBatis Mapper
@Mapper
public interface PersonMapper {
@Select("SELECT * FROM person WHERE id = #{id}")
Person selectPersonById(int id);
@Insert("INSERT INTO person (name, dateOfBirth) VALUES (#{name}, #{dateOfBirth})")
void insertPerson(Person person);
}
// MapStruct Mapper
@Mapper
public interface PersonDTOMapper {
@Mapping(source = "name", target = "fullName")
@Mapping(source = "dateOfBirth", target = "birthDate", dateFormat = "yyyy-MM-dd")
PersonDTO personToPersonDTO(Person person);
@Mapping(source = "fullName", target = "name")
@Mapping(source = "birthDate", target = "dateOfBirth")
Person personDTOToPerson(PersonDTO personDTO);
}
在业务逻辑中,你可以这样结合使用 MyBatis 和 MapStruct:
@Service
public class PersonService {
@Autowired
private PersonMapper personMapper; // MyBatis Mapper
@Autowired
private PersonDTOMapper personDTOMapper; // MapStruct Mapper
public PersonDTO getPersonDTOById(int id) {
// 使用 MyBatis 查询数据
Person person = personMapper.selectPersonById(id);
// 使用 MapStruct 转换为 DTO
return personDTOMapper.personToPersonDTO(person);
}
public void addPerson(PersonDTO personDTO) {
// 使用 MapStruct 转换为实体类
Person person = personDTOMapper.personDTOToPerson(personDTO);
// 使用 MyBatis 插入数据
personMapper.insertPerson(person);
}
}
总结:
-
MapStruct 和 MyBatis 的
Mapper
不能直接互用,因为它们的目的和工作方式不同。 -
MapStruct 主要用于 Java 对象之间的映射,而 MyBatis 主要用于数据库操作。
-
它们可以结合使用,通过 MyBatis 执行数据库操作,再使用 MapStruct 进行数据转换,处理 DTO 和实体类之间的映射。
java.lang.ClassNotFoundException: Cannot find implementation for com.example.study.mapper.DTOMapper
错误通常是由于 MapStruct 编译生成的实现类没有正确生成,或者相关的依赖未正确配置。
1.原因
定义的类使用了 lombok 但是 lombok 的依赖顺序迟于 mapstruct 导致编译的时候 getter setter 没有生成所有没有获取到
解决方式
不使用 lombok ,手动生成 getter setter,或调整pom中lombok依赖位置 放到 mapstruct前面 (pom引入的前后顺序)
2. 启用注解处理
确保你启用了注解处理器(Annotation Processing)。在 IDE(如 IntelliJ IDEA 或 Eclipse)中,需要确认是否启用了注解处理。
-
IntelliJ IDEA:可以通过以下步骤启用注解处理器:
-
打开
Settings/Preferences
->Build, Execution, Deployment
->Compiler
->Annotation Processors
-
选中
Enable annotation processing
-
-
Eclipse:
-
打开
Project
->Properties
->Java Compiler
->Annotation Processing
-
勾选
Enable annotation processing
-
3. 检查 DTOMapper 接口定义
确保你已经定义了 MapStruct 映射接口,并且接口上添加了 @Mapper
注解。例如:
package com.example.study.mapper;
import org.mapstruct.Mapper;
import com.example.study.dto.UserDTO;
import com.example.study.entity.UserEntity;
@Mapper(componentModel = "spring") // 如果你使用 Spring,可以加上这个注解
public interface DTOMapper {
UserDTO userToUserDTO(UserEntity userEntity);
}
4. 确保 DTOMapper
实现类生成
MapStruct 会在编译时自动生成实现类,通常生成的类位于 target/generated-sources/annotations
目录下。请确保该目录中存在生成的类。
-
编译时,MapStruct 会生成类似于
com.example.study.mapper.DTOMapperImpl
的类。