1、实体类
我们在工作中会经常性的接触实体类对象,并且这些对象在工作中会经常性的进行转化,比如在接受前端请求的时候我们需要查询并拼接一些实体,最后持久化保存在数据库中又会拆分成若干个实体对象。
在传统前后端不分离的架构中,我们通常会将命名规范拆分成一下几种形式。
实体类对象命名规则如下(不同公司可能存在差异):
含义描述 | 命名规范示例 |
---|---|
post body 请求参数 | XxRequest |
展示层对象命名 | XxVo |
数据传输对象命名 | XxDto |
es实体名命名 | XxIndexDo |
db实体命名 | 更表名相同(文件夹通常使用pojo或者entity) |
mongo实体命名 | XxDoc |
db组合关联实体命名 | Xx |
service接口命名 | Xxservice |
service实现命名 | XxServiceImpl |
manager,service引入多个manager进行负责的组合业务处理 | XxManager |
dao层命名 | XxMapper |
封装持久组合服务(一个实体需要从db,es,redis多种存储获取) | XxRepository |
在mvc三层架构中,他们的转化图如下所示:
1.1、对象转换工具
在我们的工作中,就可能会频繁的遇见各个对象之间的转化,下面是常见的java属性映射工具。
- get/set方法
- commons
- spring的beanUtils工具
- dozer
- orika
- cglib
- mapStruct
而他们的转换方式也各不相同,最常见的就是使用get/set方法,这种方法当然是最稳定最快的,当然,我们也会做很多重复的工作。因此,我们可以借用反射来进行简单的相同属性复制的操作。我们可以了解一下各个工具的耗时:
不难发现,假如我们不嫌麻烦,可以使用get/set方法来进行赋值,除此之外,我们也可以调用spring官方的工具 BeanUtils.copyProperties(Object resource, Object target)
进行快速的相同属性名赋值,当然,还有一个MapStruct工具是值得我们学习的,他不仅处理速度快,并且还支持一些更高级的转换。
1.2、MapStruct简介
Mapstruct使用时报错Unknown property xxx in result type xxx. Did you mean null?
随着微服务和分布式应用程序迅速占领开发领域,数据完整性和安全性比以往任何时候都更加重要。在这些松散耦合的系统之间,安全的通信渠道和有限的数据传输是最重要的。大多数时候,终端用户或服务不需要访问模型中的全部数据,而只需要访问某些特定的部分。
数据传输对象(Data Transfer Objects, DTO)经常被用于这些应用中。DTO只是持有另一个对象中被请求的信息的对象。通常情况下,这些信息是有限的一部分。例如,在持久化层定义的实体和发往客户端的DTO之间经常会出现相互之间的转换。由于DTO是原始对象的反映,因此这些类之间的映射器在转换过程中扮演着关键角色。
这就是MapStruct解决的问题:手动创建bean映射器非常耗时。 但是该库可以自动生成Bean映射器类。
即时编译技术能够将我们配置的转化工具在编译的时候生成,类似的还有lombok工具。
1.2.1、简单使用
我们能够简单的使用MapStruct来复制或者转化某两个实体,步骤如下:
- 引入maven依赖
- 编写两个需要转化的实体
- 配置MapStruct转化器
- 使用
1.2.1.1、引入maven依赖
如下所示,实际上MapStruct的配置还是挺麻烦的。你需要将MapStruct包引入,此外,你还需要引入一个MapStruct的插件。
lombok与MapStruct实际上会产生兼容性问题,所以我将lombok兼容完好的配置引入进来,后续将会进行讲解
<dependencies>
<!-- 最好放MapStruct上面 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- MapStruct插件配置,必须 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</path>
<!--lombok兼容mapstruct配置,lombok版本号与你依赖的版本号一致-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
<!--lombok兼容mapstruct配置,用最新版即可-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
1.2.1.2、编写实体类
我们需要编写两个实体类进行转化测试
我们需要是将 CoolBoy
pojo实体转化成 CoolBoyDto
。
CoolBoy如下所示
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CoolBoy {
private String name;
private Integer age;
private BigDecimal salary;
/**
* db 存的是 ids ==》 1,2,3
*/
private String girlFriends;
}
CoolBoyDto如下所示:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CoolBoyDto {
private String name;
private Integer age;
private BigDecimal salary;
private List<CoolGirlDto> girlFriends;
}
我们的需求就是将 CoolBoy
实体转化成 CoolBoyDto
,其他字段不改变,隐藏 girlFriends
属性不进行转化。
1.2.1.3、配置MapStruct转化器
我们再次明确目标,将 CoolBoy
实体转化成 CoolBoyDto
,其他字段不改变,隐藏 girlFriends
属性不进行转化。
具体步骤如下:
- 使用
@Mapper
标识这个接口是MapStruct的转化器 - Mappers工厂进行这个转化实例的创建
@Mapping(target = "girlFriends",ignore = true)
意思是忽略生成的girlFriends
属性- 方法返回值为我们需要转化生成(target)的对象,传入值为我们进行转化(resource)的源对象
@Mapper
public interface BoyGirlConverter {
// 使用Mappers工厂进行实例的创建
BoyGirlConverter INSTANCE = Mappers.getMapper(BoyGirlConverter.class);
@Mapping(target = "girlFriends",ignore = true)
CoolBoyDto po2Dto(CoolBoy coolBoy);
}
1.2.1.4、简单使用
我们编写一个controller进行简单的使用
@RestController
public class MapController {
@GetMapping("/")
public void map() {
CoolBoy coolBoy = new CoolBoy("王富贵",21,null,"1,2,3");
// 直接静态调用实例进行使用
CoolBoyDto coolBoyDto = BoyGirlConverter.INSTANCE.po2Dto(coolBoy);
System.out.println(coolBoyDto.toString());
}
}
1.2.2、MapStruct小节
实际上,MapStruct的使用流程是非常复杂的,因为我们时常要与lombok配合使用,我们简单了解一下他们的原理
lombok
lombok通过注解的方式,能够生成set/get方法,建造者模式构建,和构造函数等等代码。其原理就是通过即时编译技术,项目在编译阶段会扫描lombok的注解,即时生成所需代码。
MapStruct
MapStruct本质也是通过读取注解的方式,去生成对象转换的方法,而具体代码需要通过实体类对象的无参构造以及set方法进行。
前面我们提到,lombok能够生成set方法,当lombok和MapStruct一起使用的时候,那就必须先生成lombok的set方法之后,才能够生成MapStruct对象转化方法。假如我们没有配置相关即时编译技术,就会出现以下问题:Mapstruct使用时报错Unknown property xxx in result type xxx. Did you mean null?
官方文档也给出了解决方案,位于F&Q的第三条:MapStruct F&Q 配合lombok如何使用
解决方案详情后续下一节会仔细讲解。
1.3、MapStruct依赖
1.3.1、引入MapStruct依赖
2022年12月14日,MapStruct版本为 1.5.3.Final
MapStruct的依赖分为普通使用,以及搭配lombok使用。
1.3.1.1、MapStruct普通依赖
如下所示,在依赖包中我们需要引入 mapstruct
依赖,此外,我们还需要添加MapStruct的编译插件,使其能够正常的在编译时起作用,生成实体转换类。
...
<properties>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</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>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...
1.3.1.2、MapStruct配合lombok
我们在 1.2.2 中提到,MapStruct和lombok原理相同,但MapStruct需要set方法与lombok生成产生冲突,因此需要lombok编译完毕在编译MapStruct的内容。
官方文档也给出了解决方案:
实际上解决方法并不复杂,添加两个插件即可,加在官方编译插件下面
...
<properties>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<!-- 官方编译插件 -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!--lombok兼容mapstruct配置,lombok版本号与你依赖的版本号一致-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
<!--lombok兼容mapstruct配置,用最新版即可-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...
1.4、MapStruct映射器
所谓MapStruct映射器,也就是配置对象之间转换关系的类,MapStruct会读取并即时编译出他的实现类,对代码进行增强。
MapStruct重要定义:
- 映射器:我们需要定义映射器以配置转换策略,使用
@Mapper
注解定义,通常来说是一个接口,当然也可以定义成抽象类 - 源对象:我们需要从A转换成B对象,那么会从A取值出来做对应的转化,A被称为源对象,可能存在多个源对象
- 生成对象:我们需要从A转换成B对象,那么B对象就是生成对象
1.4.1、定义映射器
我们使用注解 @Mapper
来定义映射器。虽然官方支持抽象类定义映射器,但我们还是建议使用接口,示例如下:
@Mapper
public interface CarMapper {
}
使用该注解之后,MapStruct会扫描到这个注解,并进行其对应的编译操作。
1.4.2、生成自定义方法(入门)
我们依然引用上面的coolBoy转coolBoy的类,需要生成的类型为返回值,源对象作为传入类型。
假如你想对其中的某一些参数进行一定的处理,比如生成对象为String类型,源对象为int类型,等等增强,就需要用到一下几个注解:
@Mapping
:对生成对象的某个属性进行,映射、忽略、执行函数等增强操作。可以使用多次进行多属性增强。
1.4.2.1、直接映射
假如我们只想简单的映射,并且多的类型不需要映射,不需要任何操作,定义方法即可。当然,有一方不存在且没有定义的属性就会被忽略。
@Mapper
public interface BoyGirlConverter {
// pojo转Dot,返回类型为Dto
CoolBoyDto coolBoy2Dto(CoolBoy coolBoy);
}
1.4.2.2、属性映射
假如双方值相同,但属性名不相同,我们可以指定属性名。在 @Mapping
注解中,使用 target
标识生成对象的属性名称,使用 source
标识源对象属性名称:
// target生成类字段,source映射类字段
@Mapping(target = "girlFriends",source = "heGirls")
CoolBoyDto coolBoy2Dto(CoolBoy coolBoy);
如果你想要映射多个,那么使用多个 @Mapping
注解即可:
// target生成类字段,source映射类字段
@Mapping(target = "girlFriends",source = "heGirls")
@Mapping(target = "coolBoyDtoName",source = "name")
CoolBoyDto coolBoy2Dto(CoolBoy coolBoy);
1.4.2.3、多源对象、嵌套对象
我们在访问多源对象的时候,能够使用 .
符号,去访问某个对象底下属性
// target中 . 标识当前对象
@Mapping(target = ".girlFriends.girlName",source = "girl.Girlname")
@Mapping(target = "coolBoyDtoName",source = "coolBoy.name")
CoolBoyDto coolBoy2Dto(CoolBoy coolBoy,Girl girl);
1.4.2.4、自定义方法
可能有些映射太复杂,已经超过了我们的想象,无法使用MapStruct的API实现,我们希望能够在映射器中自定义手写属性赋值方法,可以使用 default
关键字定义方法,MapStruct在扫描的时候同样会将他扫描进去:
@Mapper
public interface CarMapper {
default PersonDto personToPersonDto(Person person) {
//手写映射逻辑
}
}
1.4.3、映射器实例的创建和使用
既然我们能够通过MapStruct定义映射方法,那么我该如何使用这些方法呢?他们本质都会创建一个对象,并实例化在你使用的地方。
1.4.3.1、使用 Mappers 工厂
我们在接口内部使用Mappers工厂进行实例化
创建:
@Mapper
public interface CarMapper {
// 使用 Mappers 工厂
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDto carToCarDto(Car car);
}
使用:
Car car = ...;
CarDto dto = CarMapper.INSTANCE.carToCarDto( car );
Mappers 工厂创建原理
MapStruct会在编译阶段将映射器实现类编译出来,而实例会通过 Mappers.getMapper(CarMapper.class);
代码,通过反射创建出来。而之所以需要使用 Mappers.getMapper(CarMapper.class);
的原因,正因为我们最后创建的对象实际上是MapStruct为我们生成的该接口的实现类。
1.4.3.2、使用IOC
我们了解到,使用 Mappers
工厂注入实际上是通过反射获取该接口的实现类并注入到我们创建的对象中。而使用IOC容器管理实例对象能够更便捷的使用映射器。
@Mapper
注解下的 componentModel
支持我们配置映射器实例的创建:
@Mapper(componentModel = "spring")
//@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) 与上面等效
public interface BoyGirlConverter {
}
如上所示,我们就将映射器装配进了spring的ioc容器,当然,使用的话就需要将映射器注入进来
使用:
@RestController
public class MapController {
// 注入映射器
@Autowired
private BoyGirlConverter boyGirlConverter;
@GetMapping("/")
public void map() {
CoolBoy coolBoy = new CoolBoy("王富贵",21,null,"1,2,3");
CoolBoyDto coolBoyDto = boyGirlConverter.po2Dto(coolBoy);
System.out.println(coolBoyDto.toString());
}
}
1.4.4、高级用法
mapStruct中支持更高级的用法,详情都藏在 @Mapper
注解中。我们可以详细了解一下这个注解:
@Repeatable(Mappings.class)
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Mapping {
// 重要,代表生成数据
String target();
// 重要:代表源数据
String source() default "";
// 重要:日期格式转换
String dateFormat() default "";
// 重要:数字格式,通常体现在精度损失的转换上
String numberFormat() default "";
// 赋常量
String constant() default "";
// 重要:表达式赋值
String expression() default "";
// 调用表达式赋默认值,当源字段为空时
/*
@Mapping(
target = "someProp",
defaultExpression = "java(new TimeAndFormat( s.getTime(), s.getFormat() ))"
)
*/
String defaultExpression() default "";
// 重要:忽略该字段,配合target使用,代表生成的字段是否忽略,默认为false
boolean ignore() default false;
// 条件表达式
String conditionExpression() default "";
// 重要:设定属性默认值
String defaultValue() default "";
}
如上所示,我们能够通过注解内属性赋值的方式来进行对象的转换。
1.4.4.1、容器沿用策略
当我们定义了普通的转化策略之后,我们在使用容器或者集合互相转化的时候,MapStruct会沿用之前我们所配置的普通转化策略。
@Mapper(componentModel = "spring")
public interface BoyGirlConverter {
@Mapping(target = "girlFriends", ignore = true)
CoolBoyDto po2Dto(CoolBoy coolBoy);
// 会沿用上面的策略进行集合整体的赋值
List<CoolBoy> po2DtoList(List<CoolBoy> coolBoy);
}
1.4.4.2、类型转换
MapStruct默认会将源对象和生成对象属性名相同的转换,并且内部会有默认的属性隐性类型转化,比如java原始数据类型与包装类型的转化,int与String的转化等等。
有一种特殊情况,那就是float向double类型的转化,这样子的转化可能会造成精度的丢失,这是你需要注意的。
此外,MapStruct在类似于int与String的转化中,能够调用 java.text.DecimalFormat
通过注解属性 numberFormat
指定理解为的格式字符串。
@Mapper(componentModel = "spring")
public interface CarMapper {
@Mapping(source = "price", numberFormat = "$#.00")
CarDto carToCarDto(Car car);
}
加入你需要转化日期类型,也可以使用 dateFormat
来指定,如下所示:
@Mapper(componentModel = "spring")
public interface CarMapper {
@Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
CarDto carToCarDto(Car car);
}
1.4.4.3、移除默认转化
我们之前提到了MapStruct会帮我们自动将相同的属性名映射过去,这样我们就不需要使用任何注解即可便捷的向生成类进行赋值。如下所示:
@Mapper(componentModel = "spring")
public interface BoyGirlConverter {
CoolBoyDto po2Dto(CoolBoy coolBoy);
}
加入你想取消默认的映射机制,只有我们通过 @Mapping
指定过的字段属性才进行映射,可以使用 @BeanMapping(ignoreByDefault = true)
1.4.4.4、表达式赋值
我们能够通过表达式来对值进行处理和赋予默认值。
使用注解 @Mapping
下的属性 expression
来执行表达式赋值,使用 defaultExpression
来执行默认值(源对象属性为null赋默认值)
如下所示,我们就能够表达式,java中的下面的 default
方法来进行赋值
@Mapper(componentModel = "spring")
public interface BoyGirlConverter {
@Mapping(target = "girlType",expression = "java(girlType(coolGirl.getGirlType()))")
CoolGirlDto po2Dto(CoolGirl coolGirl);
default int girlType(Integer girlType){
return girlType;
}
}
当然,他也是支持导入类或者导入全类名进行构造,官方文档:10.2. Expressions
@Mapper(componentModel = "spring")
public interface SourceTargetMapper {
@Mapping(target = "timeAndFormat",
expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}
在这里我们无法去判断是否成功的调用了表达式,因此我推荐你安装MapStruct官方的插件,来支持表达式的相关跳转
安装插件之后,我们就能够按住ctry跳转到对应的方法中,也就能够判断出我们的表达式调用是否能够成功了
1.4.4.5、反转转化
我们可能会出现两个对象相互转化的需求,比如CoolBoy转CoolBoyDto的策略现在想反转,通过 @InheritInverseConfiguration
能够实现这个反转功能。
@Mapper(componentModel = "spring")
public interface CarMapper {
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToDto(Car car);
@InheritInverseConfiguration
@Mapping(target = "numberOfSeats", ignore = true)
Car carDtoToCar(CarDto carDto);
}
当然,假如你的名字想替换一下, @InheritInverseConfiguration
注解是支持你指定反转的方法名称的
@Mapper(componentModel = "spring")
public interface CarMapper {
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToDto(Car car);
// name 属性表示我们需要反转的方法名
@InheritInverseConfiguration(name = "carToDto")
@Mapping(target = "numberOfSeats", ignore = true)
Car carDtoToCar(CarDto carDto);
}
评论区