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)

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

  • ModelMapper(GitHub - modelmapper/modelmapper: Intelligent object mapping)
  • BeanUtils
  • selma(GitHub - xebia-france/selma: Selma Java bean mapping that compiles)
  • mapstruct
  • ...

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

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

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

使用篇

基础使用

  1. pom加载依赖

    
        org.mapstruct
        mapstruct
        ${org.mapstruct.version}
    
    
    
        org.mapstruct
        mapstruct-processor
        ${org.mapstruct.version}
    
    

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

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    15
                    15
                    
                        
                            org.projectlombok
                            lombok
                            ${lombok.version}
                        
                        
                            org.mapstruct
                            mapstruct-processor
                            ${org.mapstruct.version}
                        
                    
                    --enable-preview
                
            
        
    
    
  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 {
      	//第二种方式
          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 resolve(Reference reference, @TargetType Class 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的编译插件的配置指定如下:

      		
                  org.apache.maven.plugins
                  maven-compiler-plugin
                  3.5.1
                  
                      1.8
                      1.8
                      UTF-8
                      
                          
                               
                                  org.mapstruct
                                  mapstruct-processor
                                  ${org.mapstruct.version}
                          	
                          
                      
                  
              
      

框架流程

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

项目测试地址

https://github.com/fulln/converter

参考资料

  • java-mapping-selma-vs-mapstruct

  • ModelMapper - How It Works