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 源文件:TypeSpec、MethodSpec、ParameterSpec。配合 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:ClassPool 取 CtClass,再 insertBefore / insertAfter 或添加方法。它内部仍会编译片段,复杂控制流不如 ASM 精确。适合 AOP、热补丁实验和 Gradle Transform 里插桩。它提供 CtClass API,用类似 Java 的字符串插入方法体,适合快速做 AOP 与热补丁实验;ClassPool.getDefault() 要注意隔离,Android Transform 里应自建 pool 并 detach;insertBefore 访问 $0/$1 参数,makeClass 可动态造类。和 ASM 比可读性好、控制力弱,复杂控制流、stack map 出错时仍得回退 ASM。运行时 defineClass 在模块化 JDK 上受限;生成代码要防注入字符串;内存上 ClassPool 会缓存 CtClass,用完 prune。
注:本组合并了同题两篇 Javassist 草稿(拼写 Javassit 与 javassist),前者侧重建池与 Android 插桩流程,后者补全
CtClassAPI、隔离与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 版本;勿动系统类 |
Java 注解处理器与字节码:编译期生成 vs 运行期织入
https://lautung.com/archives/java-apt-and-bytecode
评论