NGC 346 https://esawebb.org/news/weic2301
概述
MapStruct是一个Java注解处理器,日常开发好用的JavaBean映射转换器;类似Bean拷贝常用的BeanUtils、ObjectMapper。
基础用法
部分同学可能没有使用过MapStruct,这里用一个简单的示例来演示用法;示例是常见的Entity转DTO。
1、相关依赖
<!-- mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</dependency>
2、示例相关类
转换来源类BookEntity、目标类BookDTO
@Builder
@Data
public class BookEntity {
private String name;
private String author;
private BigDecimal price;
private LocalDateTime publishTime;
private String bookManager;
}
@Data
@Builder
public class BookDTO {
private String name;
private String author;
private BigDecimal price;
private LocalDateTime publishTime;
private String manager;
}
转换接口,接口增加@Mapper(org.mapstruct.Mapper)注解,增加INSTANCE实例属性用于外部调用,@Mapping为转换方法自定义功能,可省略。
@Mapper
public interface BookTransfer {
BookTransfer INSTANCE = Mappers.getMapper(BookTransfer.class);
@Mapping(source = "bookManager", target = "manager")
BookDTO bookEntityToDTO(BookEntity bookEntity);
}
3、调用示例
调用时,只需要使用转换接口的INSTANCE调用转换方法即可。
这里演示的仅仅是最基础的转换,MapStruct可以灵活支持多对象映射、数据类型转换、流映射、自定义SPI实现等高级映射功能,更多功能见:https://mapstruct.org/documentation/stable/reference/html/#Preface。
public static void main(String[] args) {
BookEntity bookEntity = BookEntity.builder()
.name("三体")
.author("刘慈欣")
.price(new BigDecimal("125.8"))
.publishTime(LocalDateTime.of(2006, 5, 1, 0, 0))
.bookManager("winn")
.build();
BookDTO bookDTO = BookTransfer.INSTANCE.bookEntityToDTO(bookEntity);
System.out.println(bookDTO);
}
4、编译生成实现类
编译后我们可以发现转换接口下会对应生成XXXImpl实现类
编译后生成实现类代码如下,有没有种熟悉的感觉?这不就是我们平常手写的转换代码嘛。
public BookDTO bookEntityToDTO(BookEntity bookEntity) {
if (bookEntity == null) {
return null;
} else {
BookDTO.BookDTOBuilder bookDTO = BookDTO.builder();
bookDTO.manager(bookEntity.getBookManager());
bookDTO.name(bookEntity.getName());
bookDTO.author(bookEntity.getAuthor());
bookDTO.price(bookEntity.getPrice());
bookDTO.publishTime(bookEntity.getPublishTime());
return bookDTO.build();
}
}
5、IDEA插件
原理解析
模块划分
核心模块为两部分:
- org.mapstruct: mapstruct:包含所有注释,例如 @Mapping
- org.mapstruct: mapstruct-processor:包含生成映射器实现的注释处理器等
初始化入口
熟悉的同学会想到lombok,lombok也是编译期处理。确实两个都是基于JSR-269实现,JSR-269为JDK规范:插入式注解处理;该规范规定在编译期处理注解,并且读取、修改或添加抽象语法书中的内容,相当于编译器的插件,这样就能在编译期完成映射实现类的构建。
核心处理器MappingProcessor,该处理器继承javax.annotation.processing.AbstractProcessor,主要包含初始化方法 init 和执行方法 process,简要代码如下:
@SupportedAnnotationTypes("org.mapstruct.Mapper")
@SupportedOptions({
MappingProcessor.SUPPRESS_GENERATOR_TIMESTAMP,
MappingProcessor.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT,
MappingProcessor.UNMAPPED_TARGET_POLICY,
MappingProcessor.UNMAPPED_SOURCE_POLICY,
MappingProcessor.DEFAULT_COMPONENT_MODEL,
MappingProcessor.DEFAULT_INJECTION_STRATEGY,
MappingProcessor.DISABLE_BUILDERS,
MappingProcessor.VERBOSE
})
public class MappingProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init( processingEnv );
// 省略部分代码...
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
// nothing to do in the last round
if ( !roundEnvironment.processingOver() ) {
RoundContext roundContext = new RoundContext( annotationProcessorContext );
// 省略部分代码...
}
}
这里重点关注 process 方法执行流程:
process(public) -> processMapperElements -> processMapperTypeElement -> process(private)
这里分享个IDEA编译期调试方法,已经get了技能的同学可以忽略哈
- 这里使用maven进行编译期调试,所以确认下maven环境是否配置完成:mvn -version
- 直接从IDEA打开终端(Terminal),执行命令:mvnDebug compile
- 在IDEA中添加Remote JVM Debug,端口号使用compile监听的8000(即日常JVM远程debug配置)
- 在调试位置打上断点,IDEA中以debug模式启动即可
核心处理器
处理器相关类图结构:
处理器按priority从小到大编排成执行链,并按顺序执行:
处理器名称 | priority |
MethodRetrievalProcessor | 1 |
MapperCreationProcessor | 1000 |
AnnotationBasedComponentModelProcessor | 1100 |
MapperRenderingProcessor | 9999 |
MapperServiceProcessor | 10000 |
MethodRetrievalProcessor
用于解析类的方法等基本信息
MapperCreationProcessor
初始化MapperReference,解析出Mapper
AnnotationBasedComponentModelProcessor
处理ComponentModel相关逻辑,总共有CdiComponentProcessor、JakartaComponentProcessor、Jsr330ComponentProcessor、SpringComponentProcessor、JakartaCdiComponentProcessor(未发布)五种实现
MapperRenderingProcessor
创建转换接口的具体实现类(例:BookTransfer接口,生成BookTransferImpl)
MapperServiceProcessor
处理spi和META-INF/services/目录相关逻辑
获取映射器实例
对应转换接口的实现类已经生成了,接下来在运行时获取实现类;通过转换接口中的 INSTANCE 属性直接访问实例,第一次访问时调用Mappers.getMapper方法加载:
Mappers.getMapper方法执行流程如下:
getMapper方法获取类加载器 -> doGetMapper方法完成实现类类名拼接,并根据拼接后的类构建实例返回;
下次再访问实例时则无需重新加载。
public class Mappers {
private static final String IMPLEMENTATION_SUFFIX = "Impl";
private Mappers() {
}
public static <T> T getMapper(Class<T> clazz) {
try {
// 获取类加载器列表
List<ClassLoader> classLoaders = collectClassLoaders( clazz.getClassLoader() );
return getMapper( clazz, classLoaders );
}
catch ( ClassNotFoundException | NoSuchMethodException e ) {
throw new RuntimeException( e );
}
}
private static <T> T getMapper(Class<T> mapperType, Iterable<ClassLoader> classLoaders)
throws ClassNotFoundException, NoSuchMethodException {
// 遍历类加载器列表用于加载类
for ( ClassLoader classLoader : classLoaders ) {
T mapper = doGetMapper( mapperType, classLoader );
// 获取实例成功后直接返回
if ( mapper != null ) {
return mapper;
}
}
throw new ClassNotFoundException("Cannot find implementation for " + mapperType.getName() );
}
private static <T> T doGetMapper(Class<T> clazz, ClassLoader classLoader) throws NoSuchMethodException {
try {
@SuppressWarnings( "unchecked" )
// 拼接实现类类名(例:BookTransfer + Impl)
Class<T> implementation = (Class<T>) classLoader.loadClass( clazz.getName() + IMPLEMENTATION_SUFFIX );
Constructor<T> constructor = implementation.getDeclaredConstructor();
constructor.setAccessible( true );
// 构建实例并返回
return constructor.newInstance();
}
catch (ClassNotFoundException e) {
return getMapperFromServiceLoader( clazz, classLoader );
}
catch ( InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException( e );
}
}
// 省略部分代码...
}
优点
- MapStruct则是编译期间根据接口方法、自定义配置生成实现类,通过调用实现类的普通Java方法完成映射;相对BeanUtils运行时反射来映射效率更高,资源占用也更少
- 包含很多高级特性:自定义映射、时间等规则映射,让代码更简洁,开发过程更聚焦业务
- 对比手写转换代码而言,更便捷更不易出错
- 社区活跃,持续添加新特性
缺点
- 开发新增、修改字段时会对转换代码有影响,存在一定风险
- 集合转换时存在隐式转换(如:转换List时,String->Integer),null或字段类型没对齐时存在一定风险
- 大量使用时一定程度增加JVM加载类数量,增加编译时间
- 简单使用学习成本较低,高级特性有一定学习成本
常见问题
- 本地调试无法热部署生效
- 与lombok组合使用时注意依赖引入顺序