关键在于按生命周期严格隔离注解:runtime仅反射读取,source专供apt处理,class慎用于字节码工具;需用@target和命名惯例分组,处理器与反射逻辑完全解耦,并通过构建检查和测试验证隔离性。

关键在于明确注解的生命周期归属,用 @Retention 严格划分阶段,并让不同消费方各司其职:运行时反射只读 RUNTIME 注解,APT 处理器只盯 SOURCE 注解,两者互不干扰。
按生命周期选对 Retention 策略
这是隔离的底层前提。同一语义的注解不能既用于运行时又用于编译期生成代码,否则会引发职责混乱和潜在冲突。
-
RUNTIME:仅用于运行时通过反射读取,比如
@Component、@Test。这类注解必须声明为@Retention(RetentionPolicy.RUNTIME),且不应被 APT 扫描或处理。 -
SOURCE:专供编译期使用,如
@Override、Lombok 的@Getter。必须用@Retention(RetentionPolicy.SOURCE),确保它不会进入字节码,反射永远看不到它 —— 这从根源上杜绝了运行时误读或重复处理。 - CLASS(慎用):保留在字节码中但不可反射访问,适合 ASM/Javassist 类工具。若项目同时用 APT 和字节码增强,需额外约定谁负责哪一层,避免重叠。
用 Target 和命名惯例做逻辑分组
即使同属 SOURCE 或 RUNTIME,也要通过 @Target 和命名体现意图,提升可维护性。
- 给 APT 专用注解限定精确作用域,例如只允许标注类:
@Target(ElementType.TYPE);而运行时注解常需更灵活,如@Target({ElementType.TYPE, ElementType.METHOD})。 - 命名上区分用途:APT 注解加
Generate或Auto前缀(如@GenerateBuilder),运行时注解倾向语义化名称(如@Transactional、@Valid)。 - 避免在同一个元素上混用两类注解做相似事,比如既用
@GenerateDto(APT 生成 DTO 类),又用@DtoMapping(运行时做对象转换)—— 它们解决的是不同阶段的问题,不该耦合。
处理器与反射代码完全解耦
APT 处理器和运行时反射解析器应视为两个独立模块,彼此无依赖、无共享逻辑。
- APT 处理器(继承
AbstractProcessor)只处理SOURCE注解,输出 Java 源文件。它不引用任何业务类,也不调用反射 API。 - 运行时解析逻辑(如 Spring BeanPostProcessor 或自定义 AnnotationConfigUtils)只处理
RUNTIME注解,通过Class.getAnnotation()或Method.isAnnotationPresent()获取,不感知 APT 的存在。 - 不要在运行时代码里尝试“模拟 APT 行为”(比如手动 new 对象替代生成类),也不要让 APT 处理器去读取
RUNTIME注解 —— 这违背了设计边界,也容易因类加载顺序出错。
构建与测试阶段主动验证隔离性
靠约定不如靠机制。在 CI 或本地构建中加入检查项,提前暴露越界行为。
- 用 Checkstyle 或自定义编译插件检查:所有标记
@Retention(RetentionPolicy.SOURCE)的注解,其处理器是否真的没在运行时代码中被反射调用。 - 反向检查:所有
RUNTIME注解是否出现在 APT 处理器的getSupportedAnnotationTypes()返回列表中 —— 如果出现,说明处理器试图处理不该管的东西。 - 单元测试中分别验证:APT 是否正确生成了预期源码;运行时逻辑是否能正确识别并响应注解,且不依赖生成的类是否存在(即单独测试反射路径)。











