当前位置: 首页 > news >正文

MapStruct 与 Lombok 协作的注解处理器执行顺序分析

问题背景

在 Java 项目中同时使用 MapStruct 与 Lombok 两款基于注解处理器的代码生成工具时,编译可能失败。MapStruct 需要读取由 Lombok 生成的 getter、setter 等方法来完成映射接口的实现代码生成。若 MapStruct 在 Lombok 尚未完成代码生成之前就尝试读取类的结构信息,将因找不到所需方法而报错。社区中常见的解决方案是引入 lombok-mapstruct-binding 依赖,但该依赖是否真的不可或缺,需要从 APT 的执行机制入手进行分析。

测试模型

本项目的测试模型为一个同时标注 Lombok 注解与 MapStruct 注解的 User 类:

@Getter
@Setter
public class User {private Long id;private String username;@Mapperinterface UserMapper {UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);User copy(User source);}
}

User 类使用了 Lombok 的 @Getter@Setter 注解,Lombok 将在编译期为其生成 getter 与 setter 方法。内部接口 UserMapper 标注了 MapStruct 的 @Mapper 注解,MapStruct 需要读取 User 类的 getter 方法来完成映射实现类的代码生成。

当两个注解处理器协作正常时,MapStruct 生成的实现类如下:

@Generated(value = "org.mapstruct.ap.MappingProcessor",comments = "version: 1.5.5.Final"
)
class User$UserMapperImpl implements User.UserMapper {@Overridepublic User copy(User source) {if ( source == null ) {return null;}User user = new User();user.setId( source.getId() );user.setUsername( source.getUsername() );return user;}
}

该实现类调用了 source.getId()source.getUsername() 等 getter 方法以及 user.setId()user.setUsername() 等 setter 方法,这些方法均由 Lombok 在编译期生成。若 MapStruct 先于 Lombok 执行,上述方法在 AST 中尚不存在,MapStruct 将无法生成正确的实现代码。

实验现象

在 Gradle 构建脚本中,编译器参数 -processorpath 中 jar 包的排列顺序直接决定了 APT 的处理器调度顺序。需要指出的是,Gradle 的 annotationProcessor 配置并不保证解析后的 jar 排列顺序与依赖声明顺序严格一致——配置的解析顺序受 Gradle 内部依赖解析策略的影响,在存在传递依赖或版本冲突时,实际顺序可能偏离声明顺序。在本项目的实验环境中,annotationProcessor 的解析顺序恰好与声明顺序一致,但这并非 Gradle 的契约保证。若需对 -processorpath 顺序进行确定性控制,应使用自定义配置并显式指定 options.annotationProcessorPath

configurations {customAnnotationProcessor
}dependencies {implementation 'org.mapstruct:mapstruct:1.5.5.Final'implementation 'org.projectlombok:lombok:1.18.28'customAnnotationProcessor 'org.projectlombok:lombok:1.18.28'customAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}tasks.withType(JavaCompile) {options.annotationProcessorPath = configurations.customAnnotationProcessor
}

通过 Gradle 的 --debug 选项可以观察到编译器实际接收的 -processorpath 参数,以此验证 jar 排列顺序是否符合预期。

-processorpath 中 Lombok 的 jar 排在 MapStruct 的 jar 之前时,构建脚本如下:

dependencies {implementation 'org.mapstruct:mapstruct:1.5.5.Final'implementation 'org.projectlombok:lombok:1.18.28'annotationProcessor 'org.projectlombok:lombok:1.18.28'annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}tasks.withType(JavaCompile) {options.annotationProcessorPath = configurations.annotationProcessor
}

此时 -processorpath 输出为:

-processorpath ...lombok-1.18.28.jar;...mapstruct-processor-1.5.5.Final.jar

编译与运行均成功,无需引入 lombok-mapstruct-binding

-processorpath 中 MapStruct 的 jar 排在 Lombok 的 jar 之前时,构建脚本如下:

dependencies {implementation 'org.mapstruct:mapstruct:1.5.5.Final'implementation 'org.projectlombok:lombok:1.18.28'annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'annotationProcessor 'org.projectlombok:lombok:1.18.28'
}tasks.withType(JavaCompile) {options.annotationProcessorPath = configurations.annotationProcessor
}

此时 -processorpath 输出为:

-processorpath ...mapstruct-processor-1.5.5.Final.jar;...lombok-1.18.28.jar

编译失败。若在此基础上引入 lombok-mapstruct-binding

dependencies {implementation 'org.mapstruct:mapstruct:1.5.5.Final'implementation 'org.projectlombok:lombok:1.18.28'annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'annotationProcessor 'org.projectlombok:lombok:1.18.28'annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
}tasks.withType(JavaCompile) {options.annotationProcessorPath = configurations.annotationProcessor
}

编译成功。

lombok-mapstruct-binding 的工作机制

lombok-mapstruct-binding 的核心作用是在两个注解处理器之间建立状态协作。Lombok 注解处理器在执行 init() 方法时,会将其所处理的抽象语法树(Abstract Syntax Tree,AST)节点状态标记为"已完成"。MapStruct 注解处理器在执行 process() 方法时,通过 lombok-mapstruct-binding 判断 Lombok 所处理的节点状态是否为"已完成":若已完成,则立即处理;若未完成,则延迟到下一轮处理。

通过分析 OpenJDK 中 com.sun.tools.javac.processing.JavacProcessingEnvironment 类的源码可知,APT 的实际执行逻辑是逐个处理器成对调用 init()process() 方法,而非先统一执行所有处理器的 init() 方法再统一执行所有处理器的 process() 方法。具体的遍历流程为:取出第一个处理器,执行 init(),执行 process();取出第二个处理器,执行 init(),执行 process();以此类推。因此,当 -processorpath 中 MapStruct 的 jar 排在 Lombok 的 jar 之前时,实际执行顺序为:MapStruct 执行 init(),MapStruct 执行 process(),Lombok 执行 init(),Lombok 执行 process(),MapStruct 再次执行 process()。在此顺序下,MapStruct 首次执行 process() 时,通过 binding 判断节点状态尚未完成(Lombok 的 init() 尚未被调用),于是选择延迟处理;待 Lombok 完成全部处理后,MapStruct 在后续轮次中再行处理,此时 Lombok 已生成的代码可见,编译成功。而当 -processorpath 中 Lombok 的 jar 排在 MapStruct 的 jar 之前时,实际执行顺序为:Lombok 执行 init(),Lombok 执行 process(),MapStruct 执行 init(),MapStruct 执行 process()。在此顺序下,MapStruct 执行 process() 时,Lombok 已经完成了全部代码生成工作,MapStruct 可以正确读取到 Lombok 生成的 getter 与 setter 方法,编译自然成功,无需引入 lombok-mapstruct-binding

由此可见,lombok-mapstruct-binding 的状态判断机制依赖于 Lombok 的 init() 方法将节点状态标记为"已完成",但"节点状态已完成"并不等价于"Lombok 已完成代码生成"。这一语义鸿沟意味着,若假设 APT 先统一执行所有处理器的 init() 方法再统一执行所有处理器的 process() 方法,则 MapStruct 执行 process() 时通过 binding 判断节点状态已经完成(因为 Lombok 的 init() 已将状态标记为"已完成"),于是立即处理,然而此时 Lombok 实际上尚未执行 process() 方法,其所生成的代码并不存在,MapStruct 读取到的仍然是未经过 Lombok 处理的原始 AST,于是报错。所幸 APT 的真实执行顺序并非如此,因此只要保证 -processorpath 中 Lombok 的 jar 排在 MapStruct 的 jar 之前,lombok-mapstruct-binding 即非必要依赖。

结论

MapStruct 与 Lombok 协作时的编译失败,其根本原因在于 -processorpath 中两个注解处理器 jar 包的排列顺序不当,导致 MapStruct 先于 Lombok 执行 process() 方法。lombok-mapstruct-binding 通过在两个处理器之间建立状态协作机制来缓解这一问题,但该机制本身存在语义上的局限性——Lombok 在 init() 阶段标记的节点完成状态并不代表其代码生成已真正完成。

由于 JavacProcessingEnvironment 对注解处理器的调度方式是逐个执行 init()process() 的成对调用,因此只要保证 -processorpath 中 Lombok 的 jar 排在 MapStruct 的 jar 之前,即可确保 Lombok 先完成代码生成,随后 MapStruct 再读取已修改的 AST 结构。在此前提下,lombok-mapstruct-binding 并非必要依赖。在 Gradle 构建脚本中,由于 annotationProcessor 配置不保证解析顺序与声明顺序严格一致,推荐通过自定义配置并显式指定 options.annotationProcessorPath 来获得确定性的处理器路径顺序,从而从根本上解决协作编译问题。

http://www.jsqmd.com/news/931176/

相关文章:

  • 公链革命2.0:Layer 1与Layer 2如何重构区块链开发者的黄金时代
  • m4s转MP4完整指南:3分钟解锁B站缓存视频的终极解决方案
  • MapLibre GL JS第36课:一个Source配置多个图层样式
  • 【收藏干货】2026 新版大模型转行全攻略:零基础小白、在职程序员转行避坑指南
  • FlipIt翻页时钟:Windows桌面上的时光艺术,告别Flash的复古新选择
  • 如何快速解密.NET混淆代码:de4dot终极完整指南
  • 用AI翻译你的WordPress —— WordPress AI Generator 2.4.0发布
  • PLC项目开发流程详解:从需求分析到现场调试
  • 嘉兴修漏水哪家好|2026嘉兴靠谱防水补漏、全屋漏水维修分区推荐 - 吉修匠
  • 基于仿生机械手的肌动传感器动作识别解析方案【附仿真】“
  • 谷歌秒收录需要什么条件?解决“发现未索引”报错的3步急救法
  • 微博舆情监控:定时爬取热点话题,通过NLP判断正负面情绪。微博舆情监控实战:基于定时爬取与NLP情感分析的Python实现
  • 3步解决抖音内容采集难题:你的自动化下载工作流指南
  • 空间计算在未来大有前景
  • Palworld存档修复终极指南:如何在不同服务器间无缝迁移游戏进度
  • 终极指南:掌握RPFM游戏模组开发的10个关键技术
  • rpm方式安装minio
  • 聊一聊TCP:三次握手我背了100遍,TIME_WAIT还是把我问住了
  • 给资产装上“数字翅膀”:RWA系统开发者的千亿级造富风口
  • 抖音创作者作品批量下载神器:5分钟掌握高效视频采集
  • 成都角钢公司|角钢厂家|角钢批发推荐|四川盛世钢联国际贸易有限公司供应 - 四川盛世钢联营销中心
  • YACReader终极指南:如何打造你的个人漫画图书馆
  • 2026年连锁酒店加盟品牌差异横评:定位层级、物业适配与收益模型全对比 - 科技焦点
  • 青岛修漏水哪家好|2026 青岛靠谱防水补漏、全屋漏水维修分区推荐 - 吉修匠
  • 3PEAK思瑞浦 TPA6031-S5TR SOT23-5 运算放大器
  • 零基础理解 RAG:从文档分块、向量化到相似度检索,带你搞懂检索增强生成的底层核心逻辑
  • OmenSuperHub深度解析:开源硬件控制工具的技术实现与实践指南
  • 科研写作从低效到持续高产,只需要掌握这套Gemini 3.1 Pro的辅助路径
  • 500+网站支持:WebToEpub如何将任意网页小说转换为标准EPUB电子书
  • m4s-converter:轻松解锁B站缓存视频的免费转换神器