MapStruct-Bean拷贝的第二个选择

MapStruct是一个Java注解处理器,用于简化对象之间的映射转换。它在编译时自动生成转换代码,提高效率并减少错误。通过添加依赖、定义转换接口和使用转换方法,可以轻松实现Entity到DTO的转换。MapStruct还支持多对象映射、数据类型转换等功能,并且拥有活跃的社区和不断更新的特性。
摘要由CSDN通过智能技术生成

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了技能的同学可以忽略哈

  1. 这里使用maven进行编译期调试,所以确认下maven环境是否配置完成:mvn -version
  2. 直接从IDEA打开终端(Terminal),执行命令:mvnDebug compile 

  1. 在IDEA中添加Remote JVM Debug,端口号使用compile监听的8000(即日常JVM远程debug配置) 

  1. 在调试位置打上断点,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 );

}

}

// 省略部分代码...

}

优点

  1. MapStruct则是编译期间根据接口方法、自定义配置生成实现类,通过调用实现类的普通Java方法完成映射;相对BeanUtils运行时反射来映射效率更高,资源占用也更少
  2. 包含很多高级特性:自定义映射、时间等规则映射,让代码更简洁,开发过程更聚焦业务
  3. 对比手写转换代码而言,更便捷更不易出错
  4. 社区活跃,持续添加新特性

缺点

  1. 开发新增、修改字段时会对转换代码有影响,存在一定风险
  2. 集合转换时存在隐式转换(如:转换List时,String->Integer),null或字段类型没对齐时存在一定风险
  3. 大量使用时一定程度增加JVM加载类数量,增加编译时间
  4. 简单使用学习成本较低,高级特性有一定学习成本

常见问题

  1. 本地调试无法热部署生效
  2. 与lombok组合使用时注意依赖引入顺序

MapStruct更多内容见:官方教程github

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值