MapStruct 实践

MapStruct

简介

MapStruct 是一个代码生成器,它基于约定优于配置方法极大地简化了 Java bean 类型之间映射的实现。生成的映射代码使用简单的方法调用,因此速度快、类型安全且易于理解。MapStruct 是一个注解处理器,它插入到 Java 编译器中,可用于命令行构建(Maven、Gradle 等)或者IDE。MapStruct 使用合理的默认值,但在配置或实现特殊行为时会避开自定义的实现方式。MapStruct Spring Extensions 已经加入到org.springframework.core.convert包中作为一种转换的实现提供使用

目的

在分布式架构(或微服务架构)需要拆分模块时,不得不思考一个问题:WEB 层能不能出现 DAO 或者 DO 对象?我觉得不行。服务层和表现层应当解耦,后者不应当触碰到任何持久化对象,其所有的数据来源,均应当由前者提供 ,而这需要在不同的对象模型(例如实体和 DTO)之间进行映射。编写这样的映射代码是一项乏味且容易出错的任务。MapStruct 旨在通过尽可能自动化来简化这项工作。映射代码的工具有很多种,如各种BeanUtils等,与其他映射框架相比,MapStruct 在编译时生成 bean 映射,以确保高性能和安全.

对比

数据流对比 可以看 5种常见Bean映射工具的性能比对 (juejin.cn)

市面上还是有很多的相关代码映射工具,如

以上工具可以大概分为2类

  1. **通过反射调用set/get或者直接对成员变量赋值, 一般都是调用反射包的invoke 方法 **
    • BeanUtils 都是通过java.beans.PropertyDescriptorreflect包来进行对应的处理,apache 支持名称相同但类型不同的属性的转换,spring 支持忽略某些属性不进行映射
    • ModelMapper 也是在reflect包封装反射支持的
  2. 编译期动态生成set/get代码的class文件 ,在运行时直接调用该class文件。
    • selma 使用静态编译生成字节码,而不会在运行时或在字符串字段中编写的伪代码进行任何反射。
    • mapStruct 是最初提出了映射生成的想法,功能更加丰富,社区建设比较好

从性能、问题排查、文档、成熟度、扩展性等因素来考虑,MapStruct是一个不错的选择;

使用篇

基础使用

  1. pom加载依赖

    <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency>

    此外,需要加载maven的compiler 插件

    <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>15</source> <target>15</target> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> <compilerArgs>--enable-preview</compilerArgs> </configuration> </plugin> </plugins> </build>
  2. 编写对应的转换实体和转换Mapper

    • CarEntity

      @Data public class CarDto{ private String make; private Integer seatCount; private String type; }
    • CarDTO

      @Data public class CarDto{ private String make; private Integer seatCount; private String type; }
    • CarMapper

      CarMapper 有多种 实现方式,建议如果要使用Mapper的话,继承org.springframework.core.convert.converter.Converter,2个类的成员变量基本相同的情况下,可以不用做额外的方法处理,Mapper最常见的还是以下2种

      1. 声明为SpringBean
      2. 生成静态常量

      2种方式都行,代码如下

      @Mapper //@Mapper(componentModel = "spring") //第一种方式 public interface CarMapper extends Converter<Car, CarDto> { //第二种方式 CarMapper MAPPER = Mappers.getMapper(CarMapper.class); @Mapping(target = "seatCount", source = "numberOfSeat") @Override CarDto convert(Car car); }
    • test

      测试类采用的第二种方式进行的转换,可见在使用方面还是比较方便的

      @Test public void transferTest(){ Car car = new Car(); car.setMake("转换测试"); car.setNumberOfSeat(11); car.setType("dd"); System.out.println(car); CarDto convert = CarMapper.MAPPER.convert(car); System.out.println(convert); }

进阶使用方式

下面选择几个常用场景描述下

  1. 多参数
@Mapper public interface AddressMapper { @Mapping(source = "person.description", target = "description") @Mapping(source = "address.houseNo", target = "houseNumber") DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address); }
  1. 使用spring管理,不写常量类
@Mapper(componentModel = "spring") public interface CarMapper { CarDto carToCarDto(Car car); }

3.调用其他的映射

@Mapper(uses=DateMapper.class) public class CarMapper { CarDto carToCarDto(Car car); }
  1. 直接将mapper中返回的值转换出去
@Repository // CDI component model public class ReferenceMapper { @PersistenceContext private EntityManager entityManager; public <T extends BaseEntity> T resolve(Reference reference, @TargetType Class<T> entityClass) { return reference != null ? entityManager.find( entityClass, reference.getPk() ) : null; } public Reference toReference(BaseEntity entity) { return entity != null ? new Reference( entity.getPk() ) : null; } } @Mapper(componentModel = "cdi", uses = ReferenceMapper.class ) public interface CarMapper { Car carDtoToCar(CarDto carDto); }
  1. 指定字段使用指定方法转换
@Mapper( uses = Titles.class ) public interface MovieMapper { @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } ) GermanRelease toGerman( OriginalRelease movies ); } @Named("TitleTranslator") public class Titles { @Named("EnglishToGerman") public String translateTitleEG(String title) { // some mapping logic } @Named("GermanToEnglish") public String translateTitleGE(String title) { // some mapping logic } }

解析篇

框架实现依赖

mapStruct 采用了JDK6中的新特性 插入式注解处理API(Pluggable Annotation Processing API) ,lombok注解,IDEA在编写代码时候的标记语法错误的红色下划线都是通过这个特性实现的.其主要抽象类是AbstractProcessor,需要注意的是,该API只处理编译期注解

插入式注解处理API(JSR 269)提供一套标准API来处理Annotations(JSR 175),实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method, package, constructor, type, variable, enum, annotation等Java语言元素映射为Types和Elements, 从而将Java语言的语义映射成为对象, 我们可以在javax.lang.model包下面可以看到这些类. 所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境. JSR 269用Annotation Processor在编译期间而不是运行期间处理Annotation, Annotation Processor相当于编译器的一个插件,所以称为插入式注解处理.如果Annotation Processor处理Annotation时(执行process方法)产生了新的Java代码,编译器会再调用一次Annotation Processor,如果第二次处理还有新代码产生,就会接着调用Annotation Processor,直到没有新代码产生为止.每执行一次process()方法被称为一个"round",这样整个Annotation processing过程可以看作是一个round的序列. JSR 269主要被设计成为针对Tools或者容器的API.

注解API实现步骤

  1. 定义annotation process: org.mapstruct.ap.MappingProcessor,并继承javax.annotation.processing.AbstractProcessor

  2. 定义注解 org.mapstruct.Mapper,并将运行策略改成@Retention(RetentionPolicy.SOURCE)

  3. MappingProcessor中使用javax.annotation.processing.SupportedAnnotationTypes指定在第2步创建的注解类型的名称(注意需要全类名,"包名.注解类型名称",否则会不生效)

  4. MappingProcessor中使用javax.annotation.processing.SupportedSourceVersion指定编译版本SourceVersion.latestSupported()

  5. MappingProcessor中使用javax.annotation.processing.SupportedOptions指定编译参数。

  6. 指定processor参与编译

    • 直接使用编译参数指定,javac -processor org.mapstruct.ap.MappingProcessor Main.java

    • 通过服务注册指定,就是META-INF/services/javax.annotation.processing.Processor文件中添加org.mapstruct.ap.MappingProcessor。

    • 通过Maven的编译插件的配置指定如下:

      <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <annotationProcessors> <annotationProcessor> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessor> </annotationProcessors> </configuration> </plugin>

框架流程

mapStruct虽然说实现的功能流程简单, 但是它巧妙利用了Types和Elements,将复杂的class生成分析转成对应去处理,倒是有其独特和称赞的地方

项目测试地址

https://github.com/fulln/converter

参考资料


__EOF__

本文作者我在清水河边
本文链接https://www.cnblogs.com/wzqshb/p/14907761.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   _我在清水河边  阅读(545)  评论(5编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示