7、KAPT实战:AbstractProcessor的实现、代码生成与Filer API
好,咱们今天聊点硬核的。前面几章我们把KAPT的原理、注解处理流程都捋了一遍,但说实话,光看不练等于白搭。这一章,我带你亲手写一个AbstractProcessor,把代码生成出来。
你可能要问:为什么要自己写代码生成?用运行时反射不香吗?嗯,我刚开始也是这么想的。直到我在一个大型项目里,发现运行时反射拖慢了启动速度,而且ProGuard一混淆,反射就炸了。从那以后,我彻底转向了编译期处理。
7.1 AbstractProcessor 的核心结构
写一个注解处理器,说白了就是继承 AbstractProcessor,然后重写几个关键方法。我习惯先搭个骨架,再往里填逻辑。
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions("kapt.kotlin.generated")
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// 这里可以拿到 Filer、Messager、Elements 等工具
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(MyAnnotation.class.getCanonicalName());
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 核心逻辑写在这里
return true;
}
}
这里有个细节:@AutoService 注解。我早期手写过 META-INF/services 文件,后来发现用 Google 的 auto-service 库能自动生成,省心多了。你想想看,每次加个处理器都要手动配文件,多容易漏啊。
7.2 核心方法详解
init() 方法
这个方法在处理器初始化时调用一次。我一般在这里做三件事:
- 保存
processingEnv引用 - 获取
Filer对象(用于生成文件) - 获取
Messager对象(用于打印日志)
private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "处理器初始化完成");
}
我个人习惯在 init 里打印一条日志,这样能确认处理器是否被正确加载。曾经有一次我配了半天发现处理器没跑,最后发现是 @AutoService 依赖没加对……嗯,这种坑踩过一次就记住了。
getSupportedAnnotationTypes()
这个方法返回处理器要处理的注解类型。注意,这里返回的是全限定名,不是注解类本身。
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add("com.example.BindView");
annotations.add("com.example.OnClick");
return annotations;
}
你也可以用 @SupportedAnnotationTypes 注解来声明,效果一样。我个人更倾向于用代码方式,因为可以动态判断。
process() 方法
这是核心中的核心。每次注解处理轮次都会调用这个方法。我一般这样组织代码:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
return false;
}
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
if (annotatedElement.getKind() != ElementKind.CLASS) {
messager.printMessage(Diagnostic.Kind.ERROR, "只能用在类上", annotatedElement);
continue;
}
TypeElement typeElement = (TypeElement) annotatedElement;
generateCode(typeElement);
}
return true;
}
为什么要检查 processingOver()?因为最后一轮处理完成后,系统会再调一次 process,这时候不需要做任何事。我之前没加这个判断,结果在最后一轮生成了空文件,编译报错,排查了半天。
7.3 代码生成与 Filer API
好,终于到了最激动人心的部分——生成代码。Filer API 提供了三种创建文件的方式:
| 方法 | 用途 | 示例 |
|---|---|---|
| createSourceFile | 生成 Java 源文件 | 生成 Activity_Binding.java |
| createClassFile | 生成编译后的 class 文件 | 直接输出字节码 |
| createResource | 生成资源文件 | 生成 META-INF 配置文件 |
我项目中用得最多的是 createSourceFile。说白了就是写一个 Java 文件,只不过是用代码来写代码。
private void generateCode(TypeElement typeElement) {
String className = typeElement.getSimpleName() + "_Binding";
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
try {
JavaFileObject sourceFile = filer.createSourceFile(
packageName + "." + className, typeElement);
Writer writer = sourceFile.openWriter();
writer.write(buildClassCode(packageName, className, typeElement));
writer.close();
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "生成代码失败: " + e.getMessage());
}
}
private String buildClassCode(String packageName, String className, TypeElement typeElement) {
StringBuilder sb = new StringBuilder();
sb.append("package ").append(packageName).append(";\n\n");
sb.append("public class ").append(className).append(" {\n");
sb.append(" public static void bind(").append(typeElement.getQualifiedName()).append(" target) {\n");
sb.append(" // 生成的绑定代码\n");
sb.append(" }\n");
sb.append("}\n");
return sb.toString();
}
这里有个坑:createSourceFile 的第一个参数是类的全限定名,第二个参数是触发生成的元素。如果你传了第二个参数,生成的代码会和原文件关联起来,这样增量编译时能正确清理旧文件。我之前没传第二个参数,结果改了注解后旧文件还在,编译报重复类错误。
7.4 实战:生成一个简单的 ViewBinding 代码
光说不练假把式。咱们写一个完整的例子:根据 @BindView 注解生成 ViewBinding 代码。
先定义注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value();
}
然后处理器核心逻辑:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
if (element.getKind() != ElementKind.FIELD) {
continue;
}
VariableElement field = (VariableElement) element;
TypeElement enclosingClass = (TypeElement) field.getEnclosingElement();
int viewId = field.getAnnotation(BindView.class).value();
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
// 生成代码
generateBinding(enclosingClass, fieldName, fieldType, viewId);
}
return true;
}
生成代码的方法:
private void generateBinding(TypeElement clazz, String fieldName, String fieldType, int viewId) {
String bindingClassName = clazz.getSimpleName() + "_ViewBinding";
String packageName = processingEnv.getElementUtils().getPackageOf(clazz).toString();
try {
JavaFileObject file = filer.createSourceFile(packageName + "." + bindingClassName, clazz);
try (PrintWriter out = new PrintWriter(file.openWriter())) {
out.println("package " + packageName + ";");
out.println();
out.println("public class " + bindingClassName + " {");
out.println(" public static void bind(" + clazz.getQualifiedName() + " target) {");
out.println(" target." + fieldName + " = (" + fieldType + ")");
out.println(" target.findViewById(" + viewId + ");");
out.println(" }");
out.println("}");
}
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "生成失败: " + e.getMessage());
}
}
7.5 知识体系总览
下面这张图是我自己总结的,把整个流程串起来看会更清晰:
7.6 避坑指南
最后,我把自己踩过的坑总结一下,希望能帮你少走弯路:
- 文件重复生成:每次 process 调用都会生成文件,记得用
roundEnv.processingOver()判断是否最后一轮 - 包名错误:生成代码的包名要和原类一致,否则 import 会找不到
- 增量编译问题:createSourceFile 的第二个参数一定要传触发元素,否则增量编译不会清理旧文件
- 编码问题:生成文件默认用 UTF-8,如果你项目用了其他编码,记得在 Writer 里指定
核心要点:
- AbstractProcessor 的 init 里获取 Filer 和 Messager
- process 方法里遍历注解元素,生成对应的 Java 文件
- Filer.createSourceFile 是生成代码的主要方式
- 生成的代码会自动参与编译,不需要额外配置
好了,这一章的内容就到这里。代码生成这块,说白了就是「用代码写代码」,掌握了 Filer API 的用法,剩下的就是字符串拼接的功夫了。我建议你动手写一个简单的处理器,哪怕只是生成一个 HelloWorld 类,也比光看强十倍。