Java 世界里"自动生成代码"有两条完全不同的时机:编译期用 APT 扫描注解、写出新源文件;运行期用 ASM / Javassist 直接改字节码或靠 JavaAgent 织入。两条路不要混:APT 安全、可审计、出错在编译报错;字节码改写强大但危险,写错栈帧会 VerifyError,改系统类会被 ART 限制。本文把六篇笔记分成两半——编译期(APT + JavaPoet + AutoService)和运行期(ASM + Javassist,其中两篇 Javassist 草稿合并),末尾给一张对比表。


一、编译期注解处理:APT 扫描 + JavaPoet 生成 + AutoService 注册 SPI

APT(Annotation Processing Tool)在 javac 编译期扫描注解并生成源码或资源。实现 Processor 注册到 META-INF/services。ButterKnife、Dagger、Room 都靠它。处理要幂等、支持增量,不要改已有源文件,只生成新文件。Java APT 在 Android 上用 annotationProcessor / kapt / ksp

JavaPoet 用类型安全的 DSL 拼 Java 源文件:TypeSpecMethodSpecParameterSpec。配合 APT 输出可读的生成代码,避免手写字符串拼接;NameAllocator 处理关键字冲突。Kotlin 项目现多用 KotlinPoet。生成时注意缩进、javadoc 与 SuppressWarnings;不要生成后再人工改,应改生成器;写 Processor 时把 JavaFile.write 到 Filer。

AutoService 在编译期把 @AutoService(Xxx.class) 写成 META-INF/services 条目,配合 ServiceLoader 做 SPI。组件化里常用来发现路由初始化器、编译期处理器插件,避免手写配置被 ProGuard 丢掉。注意 AGP 合并 META-INF 时不要被 packOptions 排除;多模块要让 annotationProcessor 在实现模块生效,接口模块只声明 API。

三个容易踩的坑

  • APT 处理要幂等、支持增量:不要改已有源文件,只生成新文件;循环生成要控制 round;错误用 Messager 打到 IDE;Kotlin 优先 KSP 更快。
  • JavaPoet 单元测试要断言生成文本:单元测试里可用 compile-testing;ProGuard 要 keep 生成的工厂类;避免在生成代码里依赖不稳定的运行时 API。
  • AutoService 多模块生效范围annotationProcessor 要在实现模块生效,接口模块只声明 API;AGP 合并 META-INF 时别被 packOptions 排除。

什么时候用:想消除样板代码(路由、DI、Room 式 DAO)时走 APT + JavaPoet + AutoService 的编译期路线;普通运行时反射足够的小项目不必上。


二、运行期字节码改写:ASM 访问者模式 vs Javassist 源码风格

运行期改字节码有两套主流库。ASM 是访问者模式字节码库:ClassReader 扫类,ClassVisitor / MethodVisitor 改指令,ClassWriter 写出;COMPUTE_FRAMES 可重算栈帧但慢;插桩常插 INVOKESPECIAL 前后统计耗时。Android Transform/ASM 用于埋点、隐私合规与方法替换;要注意 API version 与 Java 类文件版本,Lambda 与 invokedynamic 要小心,debug 用 TraceClassVisitor 打文本。

Javassist 用源码风格字符串改字节码,学习曲线低于 ASM:ClassPoolCtClass,再 insertBefore / insertAfter 或添加方法。它内部仍会编译片段,复杂控制流不如 ASM 精确。适合 AOP、热补丁实验和 Gradle Transform 里插桩。它提供 CtClass API,用类似 Java 的字符串插入方法体,适合快速做 AOP 与热补丁实验;ClassPool.getDefault() 要注意隔离,Android Transform 里应自建 pool 并 detachinsertBefore 访问 $0/$1 参数,makeClass 可动态造类。和 ASM 比可读性好、控制力弱,复杂控制流、stack map 出错时仍得回退 ASM。运行时 defineClass 在模块化 JDK 上受限;生成代码要防注入字符串;内存上 ClassPool 会缓存 CtClass,用完 prune

注:本组合并了同题两篇 Javassist 草稿(拼写 Javassit 与 javassist),前者侧重建池与 Android 插桩流程,后者补全 CtClass API、隔离与 prune 细节,二者互补,已合并为一节。

三个容易踩的坑

  • 写错帧会 VerifyError(ASM):和 Javassist 比更底层更快,但写错帧会 VerifyError;不要在运行时对系统类动手;增量编译要做 visitable 缓存;单元测试用 ClassWriter 产出再加载验证。
  • Javassist 栈帧与隔离:维护栈帧,异常表出错会 VerifyError;ClassPool 会缓存 CtClass 用完 prune,Android 上自建 pool 并 detach 避免常驻撑爆内存。
  • 不要在运行时改系统类:ASM 和 Javassist 都不要在运行时对系统类做重定义,权限与 ART 限制很大;Android 上要注意 minify 后类名变化,以及对 Android Gradle Plugin 的 javac 版本。

什么时候用:要极致体积与帧栈控制选 ASM;要可读性、快速 AOP / 热补丁实验选 Javassist;两者都适合 Gradle Transform 插桩(埋点、隐私合规、方法替换)。运行期方案比 APT 更强但更危险,改系统类或写错帧会直接崩,优先编译期 APT 能解决的别上字节码改写。


附:APT 与字节码改写对比表

维度 编译期 APT + JavaPoet + AutoService 运行期 ASM / Javassist
时机 javac 编译期扫描注解、生成源文件 运行期 / 构建期 Transform 改字节码
安全边界 安全,出错编译期报错、可审计 危险,写错栈帧 VerifyError,改系统类受限
可读性 JavaPoet DSL 类型安全、可读 ASM 底层快但难读;Javassist 源码风格易上手
典型用途 路由、DI、Room 式 DAO、SPI 注册 埋点、隐私合规、方法替换、热补丁
注册机制 AutoService 写 META-INF/services + ServiceLoader JavaAgent / Transform 织入
增量/缓存 APT 支持增量、幂等 ASM 增量编译做 visitable 缓存;Javassist 用完 prune
Android 注意 kapt/kapt→KSP;ProGuard keep 生成类 minify 后类名变化;AGP javac 版本;勿动系统类