• MapStruct类型之间映射的实现


    前言

    本篇文章 会介绍MapStruct 是什么,如何使用mapstruct,使用的什么原理达到 实体类之间映射;MapStruct 在使用上简化了不同类之间映射问题,而对比其他 类映射框架来说 MapStruct有什么优缺点也;我们在开发过程中使不使用该框架,也许看了本篇文章会有个大致的概念

    MapStruct简介

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

    官网:MapStruct – Java bean mappings, the easy way! 

    我们在开发过程时,当遇到不会使用,以及学习原理时,一定要去看官方文档,你一定会有不一样的收获。

    为什么要使用它

    多层应用程序通常需要在不同的对象模型(例如实体和 DTO)之间进行映射。编写这样的映射代码是一项繁琐且容易出错的任务。MapStruct旨在通过尽可能地自动化来简化这项工作。

    与其他映射框架相比,MapStruct在编译时生成bean映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查。

    MapStruct 是一个 Java 注释处理器,用于生成类型安全的 Bean 映射类。

    遵循约定而不是配置方法,MapStruct使用合理的默认值,但在配置或实现特殊行为时会避开您的方式。

    使用方便

    定义一个映射器接口,该接口声明任何所需的映射方法。在编译期间,MapStruct 将生成此接口的实现。此实现使用纯 Java 方法调用在源对象和目标对象之间进行映射,即没有反射或类似。

    与手动编写映射代码相比,MapStruct 通过生成繁琐且容易出错的代码来节省时间。

    类对象映射框架

    实体类映射框架大致有两种:一种是运行期通过java反射机制动态映射;另一种是编译期动态生成getter/setter,在运行期直接调用框架编译好的class类实现实体映射。      

    而mapstruct映射是在编译期间实现的,你注意看一下你build过后,会在 target 里面会找到编译生成的对应impl实现类。

    MapStruct是一个基于JSR 269的Java注释处理器,因此可以在命令行构建(javac,Ant,Maven等)以及IDE中使用。

     

    优点

    与动态映射框架相比,MapStruct 具有以下优点

    • 通过使用纯方法调用而不是反射来快速执行

    • 编译时类型安全:只有相互映射的对象和属性可以映射,不会意外地将订单实体映射到客户 DTO 等。

    • 在生成时清除错误报告,如果

      • 映射不完整(并非所有目标属性都已映射)

      • 映射不正确(找不到正确的映射方法或类型转换)

    缺点

    很明显,如果我们在开发中,这段代码是我们自己写的,自己维护的,我们也会知道映射规则,维护也非常简单,后面对于这个类的字段进行改动,当然也没问题。但是实际的场景 ,这样经常项目是变更的。要改动功能时,改动了属性,因为时编译时生成,它是不会报错的,但是数据转换就不会进行了。就会导致一系列的问题。 

    这就是一个比较大的问题。 在项目开发和使用中,作为开发者,这一点一定要考虑到,才能选择适合的类对象映射框架(因为这个是常用的框架)。 而除了这种,还有还有 通过插件之类的来 生成代码,显示的出现,就不会导致这样的问题。

    引入

    基于maven的

    1. ...
    2. <properties>
    3. <org.mapstruct.version>1.5.2.Finalorg.mapstruct.version>
    4. properties>
    5. ...
    6. <dependencies>
    7. <dependency>
    8. <groupId>org.mapstructgroupId>
    9. <artifactId>mapstructartifactId>
    10. <version>${org.mapstruct.version}version>
    11. dependency>
    12. dependencies>
    13. ...
    14. <build>
    15. <plugins>
    16. <plugin>
    17. <groupId>org.apache.maven.pluginsgroupId>
    18. <artifactId>maven-compiler-pluginartifactId>
    19. <version>3.8.1version>
    20. <configuration>
    21. <source>1.8source>
    22. <target>1.8target>
    23. <annotationProcessorPaths>
    24. <path>
    25. <groupId>org.mapstructgroupId>
    26. <artifactId>mapstruct-processorartifactId>
    27. <version>${org.mapstruct.version}version>
    28. path>
    29. annotationProcessorPaths>
    30. configuration>
    31. plugin>
    32. plugins>
    33. build>
    34. ...

    基本映射

    要创建映射器,只需使用所需的映射方法定义一个 Java 接口,并使用 org.mapstruct.Mapper 注释对其进行注释:

    1. @Mapper
    2. public interface CarMapper {
    3. CarDto carToCarDto(Car car);
    4. PersonDto personToPersonDto(Person person);
    5. }

    在生成的方法实现中,源类型(例如Car)中的所有可读属性将被复制到目标类型(例如CarDto)中的相应属性中。):

    • 当属性与其目标实体对应项具有相同的名称时,它将隐式映射。

    • 当属性在目标实体中具有不同的名称时,可以通过@Mapping批注指定其名称。

    检索映射器

    4.1. 映射器工厂(无依赖注入)

    不使用 DI 框架时,可以通过 org.mapstruct.factory.Mappers 实例。只需调用 getMapper() 方法,传递映射器的接口类型以返回:

    1. @Mapper
    2. public interface CarMapper {
    3. CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
    4. CarDto carToCarDto(Car car);
    5. }

    得到数据

    CarDto dto = CarMapper.INSTANCE.carToCarDto( car );
    

    MapStruct 生成的映射器是无状态且线程安全的,因此可以同时从多个线程安全地访问。

    4.2. 使用依赖注入

    您可以指定生成的映射器类应基于的组件模型,这些映射器类应基于@Mapper#componentModel或使用处理器选项,如配置选项中所述.

    支持CDI和Spring(后者通过其自定义注释或使用JSR 330注释)

    1. @Mapper(componentModel = MappingConstants.ComponentModel.Spring)
    2. public interface CarMapper {
    3. CarDto carToCarDto(Car car);
    4. }

    直接使用@autowrite就可以注入

    简单的使用方式,就可以使用 起来mapstrust

    MapStrust组件

    映射组成

    MapStruct 支持使用元注记。允许@Mapping用于其他(用户定义的)注释以进行重用

    1. @Retention(RetentionPolicy.CLASS)
    2. @Mapping(target = "id", ignore = true)
    3. @Mapping(target = "creationDate", expression = "java(new java.util.Date())")
    4. @Mapping(target = "name", source = "groupName")
    5. public @interface ToEntity { }

    对于确实有一些共同的特性,@ToEntity假定目标 Bean ShelveEntity 和 BoxEntity 都具有属性 ,则可以采用这种方式 去引用。

    官网说这个功能处于实验阶段,谨慎使用了。但是还是可以使用这种方式

    1. @Mapper
    2. public interface StorageMapper {
    3. StorageMapper INSTANCE = Mappers.getMapper( StorageMapper.class );
    4. @ToEntity
    5. @Mapping( target = "weightLimit", source = "maxWeight")
    6. ShelveEntity map(ShelveDto source);
    7. @ToEntity
    8. @Mapping( target = "label", source = "designation")
    9. BoxEntity map(BoxDto source);
    10. }

    向映射器添加自定义方法

    使用默认方法定义自定义映射的映射器

    1. @Mapper
    2. public interface CarMapper {
    3. @Mapping(...)
    4. ...
    5. CarDto carToCarDto(Car car);
    6. default PersonDto personToPersonDto(Person person) {
    7. //hand-written mapping logic
    8. }
    9. }

    在映射driver属性时carToCarDto() 中生成的代码将调用手动实现的 personToPersonDto() 方法。

    这个都会自动映射的,增加便利性

    更新现有 Bean 实例

    这个也属于我们经常需要用到的,可以通过为目标对象添加一个参数并用@MappingTarget标记该参数来实现

    1. @Mapper
    2. public interface CarMapper {
    3. void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
    4. }

    在使用过程中,可以结合  @AfterMapping @BeforeMapping 一起使用

    使用映射前和映射后方法进行映射自定义

    可以使用在映射开始之前或映射完成后调用的回调方法

    只要 对应类型正确 可以 在映射前或者映射后调用对应的方法。

    如果@BeforeMapping/@AfterMapping方法具有参数,则仅当方法的返回类型(如果非 void)可分配给映射方法的返回类型并且所有参数都可以由映射方法的源或目标参数分配时,才会生成方法调用:

    • 使用 @MappingTarget 注释的参数将使用映射的目标实例进行填充。

    • 使用@TargetType注释的参数将使用映射的目标类型进行填充。

    • 使用@Context注释的参数将使用映射方法的上下文参数进行填充。

    • 任何其他参数都将使用映射的源参数进行填充。


    默认值和常数

    如果相应的源属性为空,则可以指定默认值以将预定义值设置为目标属性。在任何情况下,都可以指定常量来设置这样的预定义值。默认值和常量指定为字符串值。当目标类型是基元类型或装箱类型时,字符串值被视为文本。在这种情况下,允许位/八进制/十进制/十六进制模式,只要它们是有效的文字。在所有其他情况下,常量或默认值都会通过内置转换或调用其他映射方法进行类型转换,以匹配目标属性所需的类型。

    1. @Mapper(uses = StringListMapper.class)
    2. public interface SourceTargetMapper {
    3. SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    4. @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
    5. @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
    6. @Mapping(target = "longWrapperConstant", constant = "3001")
    7. @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
    8. @Mapping(target = "stringListConstants", constant = "jack-jill-tom")
    9. Target sourceToTarget(Source s);
    10. }

    表达式

    通过表达式,可以包括来自多种语言的构造。目前只支持Java作为一种语言。

    1. @Mapper(import={TimeAndFormat.class})
    2. public interface SourceTargetMapper {
    3. SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
    4. @Mapping(target = "timeAndFormat",
    5. expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    6. Target sourceToTarget(Source s);
    7. }

    默认我们是要在 表达式中引入具体的包类的。 我们可以通过@Mapper(import={TimeAndFormat.class})  解决这样的问题

    并在这里引出了默认表达式

    1. @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
    2. Target sourceToTarget(Source s);

    源字段为空,如何使用defaultExpression设置ID字段。如果设置了源对象,则可以使用它从源对象中获取现有的源ID,如果未设置,则可以创建新的ID。


    限定符的问题

    MapStruct 使用限定符机制来解决冲突。MapStruct 根据源类型和目标类型的组合选择方法。

    标记为“限定符错误”的错误是 MapStructs 的方式,它让您知道它找不到要使用限定符批注或@Named进行批注的方法。这可能有几个原因:

    • 您忘记添加正确的保留策略。它必须@Retention(RetentionPolicy.CLASS)
    • 您忘记将限定符(您自己的注释或@Named)添加到指定的方法
    • 向其添加限定符的方法签名与映射所需的源类型和目标类型不匹配
    • 在1.3.x及更早版本中,MapStruct更加宽松,如果MapStruct实际上没有使用它们,也允许限定符。此问题已在 1.4.x 中修复,以便获得一致的行为
    • 如果要使用 2 步映射(因此选择两个映射方法)从源到目标,则需要将限定符添加到两个指定的方法。

    也就是说你可以采用@Named注记。此注释是预定义的限定符(使用@Qualifier本身进行注释),可用于命名映射器,或者更直接地通过其值命名映射方法。

    1. @Named("TitleTranslator")
    2. public class Titles {
    3. @Named("EnglishToGerman")
    4. public String translateTitleEG(String title) {
    5. // some mapping logic
    6. }
    7. @Named("GermanToEnglish")
    8. public String translateTitleGE(String title) {
    9. // some mapping logic
    10. }
    11. }

    但是当我们在使用 list的 批量转换也,多个接口方法的类型和返回值都是一致的如何处理,

      这就可以采用 IterableMapping#qualifiedBy   来设置我需要那个  方法来指定 list的实现

    @MapMapping#qualifiedBy  也可以使用方式,

    结语

    最后,就这篇文章主要介绍了一下使用方式,想要完整的使用方式等,可以去官方文档去看一下。我觉得那是最好学习mapstruts的最好的方式。

  • 相关阅读:
    使用Python的requests库采集充电桩LBS位置经纬度信息
    javaScript操作数组的方法
    【vim 学习系列文章 9 -- .vim 脚本文件开发学习】
    2023-9-14 最长上升子序列
    Linux通过端口号找到对应的服务及其安装位置
    计算机网络-网络层
    剑指 Offer II 021. 删除链表的倒数第 n 个结点【链表】
    【经验】通过跳板机远程连接内网服务器的相关配置
    秋招前端面试题总结
    Apollo自动驾驶系统概述(文末参与活动赠送百度周边)
  • 原文地址:https://blog.csdn.net/qq_33373609/article/details/126571231