MapStruct

1、MapStruct简介

  • 它是什么?

MapStruct 是一个代码生成器,它基于约定优于配置的方法,极大地简化了 Java bean 类型之间的映射实现。生成的映射代码使用普通的方法调用,因此速度快、类型安全且易于理解。

  • 为什么?

多层应用程序通常需要在不同的对象模型(例如实体和 DTO)之间进行映射。编写这样的映射代码是一项繁琐且容易出错的任务。MapStruct 旨在通过尽可能地自动化来简化这项工作。与其他映射框架相比,MapStruct 在编译时生成 bean 映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查。

  • 如何?

MapStruct 是一个注解处理器,它插入到 Java 编译器中,可用于命令行构建(Maven、Gradle 等)以及您首选的 IDE。MapStruct 使用合理的默认值,但在配置或实现特殊行为时会采取措施。

官方参考文档:https://mapstruct.org/documentation/stable/reference/html/

2、原理分析

常见的属性拷贝工具:

工具来源原理备注10个字段200万次耗时(ms)特点
BeanUtils apache 运行期通过java反射机制动态映射 (阿里代码规范不推荐)apache追求严谨,校验、日志很多 / 仅支持属性名称与类型相同的赋值
BeanUtils spring 运行期通过java反射机制动态映射 相比apache减少了校验与日志 1774
BeanCopier cglib 动态字节码   675 支持属性名称与类型相同的赋值,特殊赋值
MapStruct mapstruct 编译期动态生成getter/setter 类似lombook 编译期间生成代码 669 支持字段不同类型赋值,特殊赋值
  • 通过反射

直接调用的时候,是静态的 ,实例类型,方法名,参数类型这些都是明确的,编译阶段已经处理了权限,方法可见性,参数类型等校验,之后jvm加载解析的时候已经将方法的符号引用转为地址引用了,到我们执行方法的时候,就可以直接新建栈帧进行方法调用了。

那么反射的时候就不一样了,反射是动态的,在运行的过程中才知道我们要调用什么类的什么方法,在执行的时候才明确下来,但是你那些编译阶段的校验以及一些安全机制的操作 仍然 是不能少的,所以你在执行的时候依然要做校验等安全机制的操作,所以反射性能慢

  • 动态生成字节码

  • 动态编译生成类

 

新建对象拥有十个字段,进行两百万次调用赋值比较结果:

 

java程序在运行前需要经历以下过程:编写java源代码->字节码->机器码->运行。在这个流程中,我们可以参与,并且主动权最大的是在第一步,源代码的编写,也就是定义程序的执行逻辑。在由java源代码->字节码的过程中,主要是javac编译器在工作,javac完成将java源代码转换成符合jvm规范的字节码。在这个转换的过程中,javac主要的做了3个工作:

  1. 词法语法分析:词法分析将源代码字符流分解成一个个的标记(Token),语法分析将标记序列构造成抽象语法树(AST)。

  2. 注解处理器处理:注解处理器可以对AST进行一定的更新处理,更新的逻辑具体是什么,取决于注解处理器的实现。

  3. 语义分析和字节码生成:对抽象语法树进行语义分析,完成语法糖的解析等,最终生成字节码(这个过程不是本文的重点,进行了简化说明)。

自从Java 6开始,javac开始支持JSR 269 Pluggable Annotation Processing API规范,也就是说在满足该规范的前提下,我们可以实现自己的注解处理器,来实现对javac编译器编译过程的人为干预。有了这个,我们就可以在java程序的编译阶段做很多有意思的事情了(有没有觉得注解处理器是观察者模式的一种应用,只不过,我们常用的观察者工作的运行期,注解处理器工作在编译期)。mapstruct是基于JSR 269实现的,JSR 269是JDK引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。

 

 

3、使用姿势

3.1 maven引入MapStruct依赖

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mapstruct.version>1.5.2.Final</mapstruct.version>
</properties>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>provided</scope>
</dependency>

 

3.2 新建一个抽象类或接口

通用操作场景:基础转换(字段名称完全一致,类型可以不同,但是支持的可转换范围)

工厂模式使用:

@Mapper
public abstract class TestConvert {

public static final TestConvert INSTANCE = Mappers.getMapper(TestConvert.class);

public abstract TestPO dto2Po(TestDTO testDTO);
}

使用:
TestPO testPO = TestConvert.INSTANCE.dto2Po(testDTO);

Spring 自动注入:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class OrderConvert {
public abstract TestPO dto2Po(TestDTO testDTO);
}

使用:
@Autowired
private OrderConvert orderConvert;

 

  • 场景1:DTO字段userName 赋值给PO字段accountName
@Mapping(source = "accountName", target = "userName")
public abstract TestPO dto2Po(TestDTO testDTO);
  • 场景2:PO时间字段Date/LocalDateTime类型赋值给PO字符串类型,使用日期格式化
@Mapping(source = "dateTime", target = "dateTime",dateFormat = "yyyy-MM-dd HH:mm:ss")
public abstract TestPO dto2Po(TestDTO testDTO);
  • 场景3:PO浮点数字赋值DTO 字符串并保留2位小数
@Mapping(source = "doubleVal", target = "doubleVal",numberFormat = "0.00")
public abstract TestPO dto2Po(TestDTO testDTO);

注意:必须是double/float 转string,才可以保留,且会四舍五入。相同类型转换不会保留指定位小数。

 

  • 场景4:PO内有对象赋值给DTO内的对象
@Mapping(source = "testItemDTO", target = "testItemPO")
public abstract TestPO dto2Po(TestDTO testDTO);

@Mapping(source = "id", target = "itemId")
@Mapping(source = "name", target = "itemName")
public abstract TestItemPO dto2Po(TestItemDTO testItemDTO);
  • 场景5:批量转换
public abstract TestPO dto2Po(TestDTO testDTO);

public abstract List<TestPO> dto2PoList(List<TestDTO> testDTOList);
  • 场景6:设置默认值
@Mapping(target = "another",constant = "99")
public abstract TestPO dto2Po(TestDTO testDTO);
  • 场景7:多字段赋值
@Mapping(source = "testDTO.id", target = "id")
@Mapping(source = "testItemDTO.name", target = "name")
@Mapping(source = "address", target = "address")
public abstract TestBasePO dto2Po(TestDTO testDTO, TestItemDTO testItemDTO, String address);


  • 场景8:设置不转换字段
@Mapping(target = "id", ignore = true)
public abstract TestPO dto2Po(TestDTO testDTO);
  • 场景9:调用其他映射器
@Mapper(uses = TestItemConvert.class)
public abstract class TestConvert {
public static final TestConvert INSTANCE = Mappers.getMapper(TestConvert.class);

@Mapping(target = "another", constant = "99")
@Mapping(source = "testItemDTO",target = "testItemPO")
public abstract TestPO dto2Po(TestDTO testDTO);
  • 场景10:后置特殊字段处理
public abstract TestPO dto2Po(TestDTO testDTO);

@AfterMapping
public void dto2PoAfter(TestDTO testDTO, @MappingTarget TestPO testPO){
List<TestItemDTO> testItemDTOList = testDTO.getTestItemDTOList();
testPO.setItemSize(testItemDTOList.size());
}
  • 场景11:两个对象大部分字段都相同,想赋值指定字段

@BeanMapping(ignoreByDefault = true) 含义是忽略所有字段

@BeanMapping(ignoreByDefault = true)
@Mapping(source = "id",target = "id")
public abstract TestPO dto2Po(TestDTO testDTO);
 
posted @   yczhang1011  阅读(660)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示