Java注解处理器从入门到出门

简介

Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。
Java语言中的类、方法、变量、参数和包等都可以被标注。Java标注和Javadoc不同,标注有自反性。在编译器生成类文件时,标注可以被嵌入到字节码中,由Java虚拟机执行时获取到标注。
根据元注解@Retention指定值的不同,注解可分为SOURCECLASSRUNTIME三种类型。当被声明为SOURCE时,注解仅仅在源码级别被保留,编译时被丢弃;声明为CLASS时,注解会由编译器记录在class文件内,但在运行时会被忽略,默认的Retention级别即为CLASS;声明为RUNTIME时,注解将被保留到运行时,可通过反射在运行时获取到。
下面我们针对CLASS级别的注解,介绍在编译期处理注解的方法。

APT

注解处理器(Annotation Processing Tool)是javac内置的工具,用于在编译时期扫描和处理注解信息。从JDK 6开始,apt暴露了可用的API。一个特定的处理器接收一个Java源代码或已编译的字节码作为输入,然后输出一些文件(通常是.java文件)。这就意味着你可以使用apt动态生成代码逻辑,需要注意的是apt仅可以生成新的Java类而不能对已存在的Java类进行修改。所有生成的Java类将和其他源代码一起被javac编译。

定义和使用注解

举个栗子,此处我们定义一个用于标注Field的注解Meta,包含两个参数repeat和id,在编译阶段我们将通过处理这一注解,给被标注的Field赋值,如repeat为2,id为Aa,则被标注的Field会被赋值为”AaAa”。

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Meta {
int repeat() default 0;
String id() default "";
}

在Field上使用注解

@Meta(repeat = 3, id = "^_^")
public String test;

处理注解

下面我们基于Android Studio编写一个处理上文中定义的Meta注解的处理器。

创建Module

此处我们将注解解析器作为Android Project中的一个module来开发,新建一个Module,类型选择Java Library

创建处理器

注解需要通过注解处理器进行处理,所有的注解处理器都实现了Processor接口,一般我们选择继承AbstractProcessor来创建自定义注解处理器。
继承AbstractProcessor,实现public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)方法。方法参数中annotations包含了该处理器声明支持并已经在源码中使用了的注解,roundEnv则包含了注解处理的上下文环境。 此方法返回true时,表示此注解已经被处理完毕,返回false时将会交给其他处理器继续处理。

声明支持的注解类型和源码版本

覆盖getSupportedSourceVersion方法,返回处理器支持的源码版本,一般直接返回SourceVersion.latestSupported()即可。
覆盖getSupportedAnnotationTypes方法,返回处理器想要处理的注解类型,此处需返回一个包含了所有注解完全限定名的集合。
在Java 7及以上,可以使用类注解@SupportedAnnotationTypes@SupportedSourceVersion替代上面的方法进行声明。

声明注解处理器

注解处理器在使用前需要先向JVM注册,在module的META-INF目录下新建services目录,并创建一个名为javax.annotation.processing.Processor的文件,在此文件内逐行声明注解处理器。同样地,此处需要声明的也是处理器类的完全限定名。
另一个简便的方法是使用Google提供的auto-services库,在build.gradle中引入com.google.auto.service:auto-service:1.0-rc2,并在处理器类上添加注解@AutoService(Processor.class),auto-services也是一个注解处理器,会在编译时为该module生成声明文件。

解析注解

首选我们定义一个接口来规范生成的类:

public interface Actor {
void action();
}

再定义一个类结构来描述我们生成的Java类:

public class TargetGen<T extends Target> implements Actor{
protected T target;
public TargetGen(T obj) {
this.target = obj;
}
@Override
public void action() {
//赋值操作
}
}

如果我们有一个类A,其中的Field f包含了Meta注解,我们会为其生成一个AGen类,并在action方法中完成对f的赋值操作。
在process方法中完成对注解的解析和代码生成操作:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
/*roundEnv.getRootElements()会返回工程中所有的Class
在实际应用中需要对各个Class先做过滤以提高效率,避免对每个Class的内容都进行扫描*/
for (Element e : roundEnv.getRootElements()) {
List<String> statements = new ArrayList<>();
/*遍历Class内所有元素*/
for (Element el : e.getEnclosedElements()) {
/*只处理包含了注解并被修饰为public的Field*/
if (el.getKind().isField() && el.getAnnotation(Meta.class) != null && el.getModifiers().contains(Modifier.PUBLIC)) {
/*获取注解信息,生成代码片段*/
Meta meta = el.getAnnotation(Meta.class);
int repeat = meta.repeat();
String seed = meta.id();
String result = "";
for (int i = 0; i < repeat; i++) {
result += seed;
}
statements.add("\t\ttarget." + el.getSimpleName() + " = \"" + result + "\";");
}
}
if (statements.size() == 0) {
return true;
}
String enclosingName;
if (e instanceof PackageElement) {
enclosingName = ((PackageElement) e).getQualifiedName().toString();
} else {
enclosingName = ((TypeElement) e).getQualifiedName().toString();
}
/*获取生成类的类名和package*/
String pkgName = enclosingName.substring(0, enclosingName.lastIndexOf('.'));
String clsName = e.getSimpleName() + "Gen";
log(pkgName + "," + clsName);
/*创建文件,写入代码内容*/
try {
JavaFileObject f = processingEnv.getFiler().createSourceFile(clsName);
log(f.toUri().toString());
Writer writer = f.openWriter();
PrintWriter printWriter = new PrintWriter(writer);
printWriter.println("//Auto generated code, do not modify it!");
printWriter.println("package " + pkgName + ";");
printWriter.println("\nimport com.moxun.Actor;\n");
printWriter.println("public class " + clsName + "<T extends " + e.getSimpleName() + "> implements Actor{");
printWriter.println("\tprotected T target;");
printWriter.println("\n\tpublic " + clsName + "(T obj) {");
printWriter.println("\t\tthis.target = obj;");
printWriter.println("\t}\n");
printWriter.println("\t@Override");
printWriter.println("\tpublic void action() {");
for (String statement : statements) {
printWriter.println(statement);
}
printWriter.println("\t}");
printWriter.println("}");
printWriter.flush();
printWriter.close();
writer.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
return true;
}

在目标module的dependencies中加入处理器模块的依赖,clean并rebuild工程,源代码就能被自定义的注解处理器处理并将产出的类生成到build/intermediates/classes目录下。由于一个Android Gradle插件的issue,直到插件版本2.2.0-alpha4,产出的class仍会被放到此目录下。intermediates目录下的源文件不会被IDE索引,所以给生成代码的调试带来一些不便,不过这并不影响后续的编译过程。在未来的版本中,该issue可能会被修正,产物会被输出到正确的地方也就是build/generated/source/apt目录下。

在运行时使用生成的类

在运行时可以使用反射来访问生成的类,此处定义了一个简单的帮助类来实例化生成的类并给目标Field赋值:

public class MetaLoader {
public static void load(Object obj) {
String fullName = obj.getClass().getCanonicalName();
String pkgName = fullName.substring(0, fullName.lastIndexOf('.'));
String clsName = pkgName + "." + obj.getClass().getSimpleName() + "Gen";
try {
Class<Actor> clazz = (Class<Actor>) Class.forName(clsName);
Constructor<Actor> constructor = clazz.getConstructor(obj.getClass());
Actor actor = constructor.newInstance(obj);
actor.action();
} catch (Exception e) {
e.printStackTrace();
}
}
}

在目标类初始化的时候调用MetaLoader.load,传入目标类的实例,便可完成对Field的赋值操作。

在打包过程中排除处理器

由于在前面引入了auto-service库,最终打包apk的时候会报错Duplicate files copied in APK META-INF/services/javax.annotation.processing.Processor,而该文件在运行时又是不需要的,所以可以在packagingOptions中排除这个文件以规避该错误:

packagingOptions {
exclude 'META-INF/services/javax.annotation.processing.Processor'
}

然而这并不是彻底的解决方案,如上所述,注解处理器在运行时是完全无用的,能否让其仅存在于编译期而不打包进最终产物内呢?答案是肯定的。
在工程的build.gradle内添加插件:

dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' 
//etc……
}

在module的build.gradle内应用插件:

apply plugin: 'com.neenbedankt.android-apt'

应用插件后,dependencies会新增一个新的依赖方法apt,修改依赖声明为:

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
apt project(':processor')
//etc……
}

如此声明后处理器module内的类将不会被打包到最终的产物中,有利于缩小产物体积。

调试注解处理器

在Android Studio中添加新的Run/Debug Configurations,类型选择Remote;
在工程的gradle.properties中添加

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

选中上面定义的Configuration,点击Debug按钮等待目标进程attach;
在注解处理器逻辑内设置断点,选择Rebuild Project,触发注解处理器处理逻辑即可实现断点调试。

其他

大概的内容就是这些,剩下的就是一些小细节了,需要自己领会233
draw a hourse

示例

Github