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类
- **通过反射调用
set/get
或者直接对成员变量赋值, 一般都是调用反射包的invoke
方法 **BeanUtils
都是通过java.beans.PropertyDescriptor
和reflect
包来进行对应的处理,apache 支持名称相同但类型不同的属性的转换,spring 支持忽略某些属性不进行映射ModelMapper
也是在reflect
包封装反射支持的
- 编译期动态生成set/get代码的class文件 ,在运行时直接调用该class文件。
selma
使用静态编译生成字节码,而不会在运行时或在字符串字段中编写的伪代码进行任何反射。mapStruct
是最初提出了映射生成的想法,功能更加丰富,社区建设比较好
从性能、问题排查、文档、成熟度、扩展性等因素来考虑,MapStruct
是一个不错的选择;
使用篇
基础使用
-
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>
-
编写对应的转换实体和转换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种- 声明为
SpringBean
- 生成静态常量
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); }
-
进阶使用方式
下面选择几个常用场景描述下
- 多参数
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
- 使用spring管理,不写常量类
@Mapper(componentModel = "spring")
public interface CarMapper {
CarDto carToCarDto(Car car);
}
3.调用其他的映射
@Mapper(uses=DateMapper.class)
public class CarMapper {
CarDto carToCarDto(Car car);
}
- 直接将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);
}
- 指定字段使用指定方法转换
@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实现步骤
-
定义
annotation process
:org.mapstruct.ap.MappingProcessor
,并继承javax.annotation.processing.AbstractProcessor
-
定义注解
org.mapstruct.Mapper
,并将运行策略改成@Retention(RetentionPolicy.SOURCE)
-
在
MappingProcessor
中使用javax.annotation.processing.SupportedAnnotationTypes
指定在第2步创建的注解类型的名称(注意需要全类名,"包名.注解类型名称",否则会不生效) -
在
MappingProcessor
中使用javax.annotation.processing.SupportedSourceVersion
指定编译版本SourceVersion.latestSupported()
。 -
在
MappingProcessor
中使用javax.annotation.processing.SupportedOptions
指定编译参数。 -
指定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生成分析转成对应去处理,倒是有其独特和称赞的地方