问题背景
在 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 来获得确定性的处理器路径顺序,从而从根本上解决协作编译问题。
