前言
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。后端程序经常和
对象
你来我往,你来我往就算了,每个对象都还是new
的,这怎一个渣
字了得啊?!哈哈哈~大家别误会哈,我说的对象是Object
。谈到对象,那就不得不说说对象与对象之间的互相转换,这时就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都get/set
会很麻烦,麻烦是其次,很影响代码的优雅性,就让人感觉很low
;可能有人就会说了,为什么不用org.springframework.beans.BeanUtils
浅拷贝完成对象与对象之间的转换呢?我只能说BeanUtils
不够灵活,如果属性名不同就需要手动赋值,个人感觉没有MapStruct
香,不信请接着看正文 ☟
须知
何为 VO、DTO、DO以及PO?
答:
VO
(View Object):视图对象,用于视图页面层,将制定页面或组件中的数据封装起来组合成一个对象DTO
(Data Transfer Object):数据传输对象,这个概念来源于J2EE
的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,用于展示层与服务层之间的数据传输对象DO
(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体类PO
(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系
MapStruct
What
官方地说,
MapStruct
是一个代码生成器,它遵循约定优于配置的原则,极大地简化了Java bean
类型之间映射的实现,只需要定义一个Mapper
接口,MapStruct
就会自动实现这个映射接口,避免了复杂繁琐的映射实现。
Why
-
生成的映射代码使用简单的方法调用,因此
速度快
、类型安全
且易于理解
-
MapStruct
旨在通过尽可能自动化来简化对象与对象模型之间相互转换这一项乏味且容易出错的任务 -
与其他映射框架相比,
MapStruct
在编译时生成bean
映射,以确保 高性能,允许快速的开发人员 反馈 和 彻底的错误检查
How
第一步:引 MapStruct
依赖
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
<!--MapStruct-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<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>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
复制代码
写实体类
该类由 MP
根据 tb_fruit
表结构自动生成的一个实体类,涉及到八个字段
/**
* <p>
* 水果实体类
* </p>
*
* @author HUALEI
* @since 2021-09-04
*/
@TableName("tb_fruit")
@Data
public class Fruit {
/**
* 主键id
*/
@TableId(value = "fruit_id", type = IdType.AUTO)
private Long fruitId;
/**
* 水果名称
*/
private String fruitName;
/**
* 水果销量
*/
private Integer fruitSale;
/**
* 本地图标地址
*/
private String localIcon;
/**
* 备用的网络图标地址
*/
private String spareIcon;
/**
* 逻辑删除 0 未删除,1 被删除
*/
@TableLogic
private Byte isDeleted;
/**
* 修改更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 乐观锁标识,用于控制唯一的修改操作
*/
@Version
private Integer version;
}
复制代码
写转换对象
根据实体类,明确转换的对象应该包含哪些字段属性
这里我拿两个 VO
对象举例,其中一个 FruitVO
对象中属性从 Fruit
中任意选取了六个,属性名保持一致,但另一个 FruitVO2
总共三个字段,都和实体类中属性名不同
@Data
public class FruitVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long fruitId;
private String fruitName;
private Integer fruitSale;
private String localIcon;
private String spareIcon;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
}
@Data
public class FruitVO2 implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private Integer sale;
}
复制代码
写映射接口
明确要转换的对象后,编写映射接口,目的是将源对象属性和目标对象属性形成一一对应的映射关系
注意:@Mapper
注解是 org.mapstruct.factory.Mappers
下的,千万不要引入错咯!!
@Mapper
public interface FruitMap {
FruitMap mapper = Mappers.getMapper(FruitMap.class);
@Mappings({
@Mapping(source = "fruitId", target = "fruitId"),
@Mapping(source = "fruitName", target = "fruitName"),
@Mapping(source = "fruitSale", target = "fruitSale"),
@Mapping(source = "localIcon", target = "localIcon"),
@Mapping(source = "spareIcon", target = "spareIcon"),
@Mapping(source = "updateTime", target = "updateTime")
})
FruitVO pojo2vo(Fruit fruit);
Fruit vo2pojo(FruitVO fruitVO);
@Mappings({
@Mapping(source = "fruitId", target = "id"),
@Mapping(source = "fruitName", target = "name"),
@Mapping(source = "fruitSale", target = "sale")
})
FruitVO2 pojo2vo2(Fruit fruit);
List<FruitVO> pojoList2vo(List<Fruit> fruits);
List<FruitVO2> pojoList2vo2(List<Fruit> fruits);
@Mappings({
@Mapping(source = "updateTime", target = "orderId", qualifiedByName = "setOrderIdByUpdateTime"),
@Mapping(source = "fruitId", target = "shortage.id"),
@Mapping(source = "fruitSale", target = "shortage.lackNum")
})
PurchaseList pojo2PurchaseList(Fruit fruit);
@Named("setOrderIdByUpdateTime")
default Long setOrderIdByUpdateTime(Date updateTime) {
return System.currentTimeMillis() - updateTime.getTime();
}
}
复制代码
这段代码开头,创建了一个 FruitMap
类型的实例 mapper
,这是我们接下来调用该接口中方法的 入口
- 首先,看第一个接口方法,
pojo2vo
是将实体类对象转换成VO
对象,由于它们之间的属性名一致,是可以不使用@Mappings
一一与目标对象中的属性对应的,不过写了也没事……vo2pojo
是将VO
转换为PO
已经声明了它们属性之间的映射关系了,所以MapStruct
底层实现会自动根据声明的规则进行转换 - 其次,看
pojo2vo2
方法使用了@Mappings
注解规定了映射关系,使得在不同属性名的情况下完成对应关系 - 最后,
pojoList2vo
和pojoList2vo2
是将PO
集合转换成VO
集合的,由于对象层面都能相互转换了,集合之间的转换只不过是外面套一层for
循环罢了
调用接口 | 测试转换
自动注入对象转换接口,调用接口中对应的方法完成转换即可
@SpringBootTest
class MapStructToolTests {
@Autowired
private FruitMapper fruitMapper;
@Test
// 使用 MapStruct 工具,将实体类转 VO 对象,VO 中的字段和 pojo 字段一致的情况
void pojo_to_vo_field_same() {
Fruit fruit = fruitMapper.selectById(18L);
FruitVO fruitVO = FruitMap.mapper.pojo2vo(fruit);
System.out.println(fruitVO);
}
@Test
// 使用 MapStruct 工具,将实体类转 VO 对象,VO 中的字段和 pojo 字段不一致的情况
void pojo_to_vo2_field_different() {
Fruit fruit = fruitMapper.selectById(15L);
// 使用了 @Mappings( { @Mapping(...), ...}) 注解
FruitVO2 fruitVO2 = FruitMap.mapper.pojo2vo2(fruit);
System.out.println(fruitVO2);
}
@Test
// 不仅能实现实体类到 VO 的转换,还能反着来
void vo2_to_pojo_field_same() {
Fruit fruit = fruitMapper.selectById(1L);
System.out.println(fruit);
FruitVO fruitVO = FruitMap.mapper.pojo2vo(fruitMapper.selectById(2L));
System.out.println(fruitVO);
Fruit fruit1 = FruitMap.mapper.vo2pojo(fruitVO);
System.out.println(fruit1);
}
@Test
// 基于对象转换的集合转换(字段相同)
void pojo_list_to_vo_field_same() {
QueryWrapper<Fruit> wrapper = new QueryWrapper<>();
wrapper.le("fruit_id", 3);
List<Fruit> fruits = fruitMapper.selectList(wrapper);
List<FruitVO> fruitVOs = FruitMap.mapper.pojoList2vo(fruits);
fruitVOs.forEach(System.out::println);
}
@Test
// 基于对象转换的集合转换(字段不相同)
void pojo_list_to_vo2_field_different() {
QueryWrapper<Fruit> wrapper = new QueryWrapper<>();
wrapper.le("fruit_id", 3);
List<Fruit> fruits = fruitMapper.selectList(wrapper);
// pojo -> vo 之间的映射只要写一次即可
List<FruitVO2> fruitVO2s = FruitMap.mapper.pojoList2vo2(fruits);
for (FruitVO2 fruitVO2 : fruitVO2s) {
System.out.println(fruitVO2);
}
}
}
复制代码
全部通过测试,bingo
~ 小伙伴,可以尝试尝试哦,用了都说香……
其他用法
属性映射对象
随意乱七八糟,写两个类 o( ̄︶ ̄)o
/**
* 水果采购单
* MapStruct 测试类
*/
@Data
public class PurchaseList {
// 订单编号
private Long orderId;
// 水果缺货额
private Shortage shortage;
}
复制代码
/**
* 水果缺货额
* MapStruct 测试类
*/
@Data
public class Shortage {
// 缺额水果 id
private Long id;
// 缺货量
private Integer lackNum;
}
复制代码
PurchaseList
中含有两个属性,其中一个属性是对象,如果要从 Fruit
映射到这个对象,MapStruct
面对这种复杂的映射关系该如何做呢?
向 FruitMap
中加入如下方法:
注意:在接口中写方法的实现,要使用 default
关键字
@Mappings({
@Mapping(source = "updateTime", target = "orderId", qualifiedByName = "setOrderIdByUpdateTime"),
@Mapping(source = "fruitId", target = "shortage.id"),
@Mapping(source = "fruitSale", target = "shortage.lackNum")
})
PurchaseList pojo2PurchaseList(Fruit fruit);
复制代码
映射关系为:水果 id 映射到 缺额水果 id;水果销量 映射到 缺额数量
掘友:
qualifiedByName
这个属性有什么用呢?如果有这个疑问的,请继续往下 ☟
转换中间值处理
倘若,我规定 PurchaseList
中的订单编号属性值是通过当前时间 减去 数据更新时间 的 时间差,那这时该怎么做呢?能不能在转换之前使用一个中间函数处理一下完成赋值呢?答案是肯定的,如下:
@Named("setOrderIdByUpdateTime")
default Long setOrderIdByUpdateTime(Date updateTime) {
return System.currentTimeMillis() - updateTime.getTime();
}
复制代码
此时,使用 @Named
注解将 setOrderIdByUpdateTime()
这个中间处理函数命名为 setOrderIdByUpdateTime
,然后使用 qualifiedByName
将源对象中 updataTime
属性通过指定的名字找到中间处理函数并且将该属性入参,返回值就是转换的结果,是不是很简单呢?
底层实现
不知名的掘友:我想康康
MapStruct
底层到底干了什么?
通过 yyds
的 IDE
的反编译功能查看编译后的实现类,如下:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-09-09T13:27:31+0800",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_221 (Oracle Corporation)"
)
public class FruitMapImpl implements FruitMap {
@Override
public FruitVO pojo2vo(Fruit fruit) {
if ( fruit == null ) {
return null;
}
FruitVO fruitVO = new FruitVO();
fruitVO.setFruitId( fruit.getFruitId() );
fruitVO.setFruitName( fruit.getFruitName() );
fruitVO.setFruitSale( fruit.getFruitSale() );
fruitVO.setLocalIcon( fruit.getLocalIcon() );
fruitVO.setSpareIcon( fruit.getSpareIcon() );
fruitVO.setUpdateTime( fruit.getUpdateTime() );
return fruitVO;
}
@Override
public Fruit vo2pojo(FruitVO fruitVO) {
if ( fruitVO == null ) {
return null;
}
Fruit fruit = new Fruit();
fruit.setFruitId( fruitVO.getFruitId() );
fruit.setFruitName( fruitVO.getFruitName() );
fruit.setFruitSale( fruitVO.getFruitSale() );
fruit.setLocalIcon( fruitVO.getLocalIcon() );
fruit.setSpareIcon( fruitVO.getSpareIcon() );
fruit.setUpdateTime( fruitVO.getUpdateTime() );
return fruit;
}
@Override
public FruitVO2 pojo2vo2(Fruit fruit) {
if ( fruit == null ) {
return null;
}
FruitVO2 fruitVO2 = new FruitVO2();
fruitVO2.setId( fruit.getFruitId() );
fruitVO2.setName( fruit.getFruitName() );
fruitVO2.setSale( fruit.getFruitSale() );
return fruitVO2;
}
@Override
public List<FruitVO> pojoList2vo(List<Fruit> fruits) {
if ( fruits == null ) {
return null;
}
List<FruitVO> list = new ArrayList<FruitVO>( fruits.size() );
for ( Fruit fruit : fruits ) {
list.add( pojo2vo( fruit ) );
}
return list;
}
@Override
public List<FruitVO2> pojoList2vo2(List<Fruit> fruits) {
if ( fruits == null ) {
return null;
}
List<FruitVO2> list = new ArrayList<FruitVO2>( fruits.size() );
for ( Fruit fruit : fruits ) {
list.add( pojo2vo2( fruit ) );
}
return list;
}
@Override
public PurchaseList pojo2PurchaseList(Fruit fruit) {
if ( fruit == null ) {
return null;
}
PurchaseList purchaseList = new PurchaseList();
purchaseList.setShortage( fruitToShortage( fruit ) );
purchaseList.setOrderId( setOrderIdByUpdateTime( fruit.getUpdateTime() ) );
return purchaseList;
}
protected Shortage fruitToShortage(Fruit fruit) {
if ( fruit == null ) {
return null;
}
Shortage shortage = new Shortage();
shortage.setId( fruit.getFruitId() );
shortage.setLackNum( fruit.getFruitSale() );
return shortage;
}
}
复制代码
看完之后,是不是大吃一惊,这不就是 get/set
的调用嘛,也不是什么高大上的东西啊!确实,MapStruct
本质上就是一个代码生成器,能够帮助我们省去很多繁琐、重复的事情,保证快捷方便的同时,还很便于理解、代码编写也容易,和 BeanUtils 相比灵活性更强
综上,你是选择 BeanUtils
呢?还是选择学习 MapStruct
?希望对看到这里的你有一定的帮助和启发 (^▽^)
更多
如果要探索更多且更详细的使用方式,可以参考 MapStruct
官网 哆啦A梦的传送门
结尾
撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。