自定义Java注解处理器

本文介绍了如何自定义Java注解处理器及涉及到的相关知识,看完本文可以很轻松看懂并理解各大开源框架的注解处理器的应用。

《游园不值》
应怜屐齿印苍苔 ,小扣柴扉久不开 。
春色满园关不住 ,一枝红杏出墙来 。
-宋,叶绍翁

本文首发:http://yuweiguocn.github.io/

关于自定义Java注解请查看自定义注解

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。

基本实现

实现一个自定义注解处理器需要有两个步骤,第一是实现Processor接口处理注解,第二是注册注解处理器。

实现Processor接口

通过实现Processor接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor类实现自定义注解处理器。实现抽象方法process处理我们想要的功能。

public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
}

除此之外,我们还需要指定支持的注解类型以及支持的Java版本通过重写getSupportedAnnotationTypes方法和getSupportedSourceVersion方法:

public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(CustomAnnotation.class.getCanonicalName());
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

对于指定支持的注解类型,我们还可以通过注解的方式进行指定:

@SupportedAnnotationTypes({"io.github.yuweiguocn.annotation.CustomAnnotation"})
public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

因为Android平台可能会有兼容问题,建议使用重写getSupportedAnnotationTypes方法指定支持的注解类型。

注册注解处理器

最后我们还需要将我们自定义的注解处理器进行注册。新建res文件夹,目录下新建META-INF文件夹,目录下新建services文件夹,目录下新建javax.annotation.processing.Processor文件,然后将我们自定义注解处理器的全类名写到此文件:

io.github.yuweiguocn.processor.CustomProcessor

上面这种注册的方式太麻烦了,谷歌帮我们写了一个注解处理器来生成这个文件。
github地址:https://github.com/google/auto
添加依赖:

compile 'com.google.auto.service:auto-service:1.0-rc2'

添加注解:

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
    ...
}

搞定,体会到注解处理器的强大木有。后面我们只需关注注解处理器中的处理逻辑即可。

我们来看一下最终的项目结构:

基本概念

抽象类中还有一个init方法,这是Processor接口中提供的一个方法,当我们编译程序时注解处理器工具会调用此方法并且提供实现ProcessingEnvironment接口的对象作为参数。

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
}

我们可以使用ProcessingEnvironment获取一些实用类以及获取选项参数等:

方法 说明
Elements getElementUtils() 返回实现Elements接口的对象,用于操作元素的工具类。
Filer getFiler() 返回实现Filer接口的对象,用于创建文件、类和辅助文件。
Messager getMessager() 返回实现Messager接口的对象,用于报告错误信息、警告提醒。
Map<String,String> getOptions() 返回指定的参数选项。
Types getTypeUtils() 返回实现Types接口的对象,用于操作类型的工具类。

元素

Element元素是一个接口,表示一个程序元素,比如包、类或者方法。以下元素类型接口全部继承自Element接口:

类型 说明
ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素。
PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
TypeParameterElement 表示一般类、接口、方法或构造方法元素的形式类型参数。
VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。

如果我们要判断一个元素的类型,应该使用Element.getKind()方法配合ElementKind枚举类进行判断。尽量避免使用instanceof进行判断,因为比如TypeElement既表示类又表示一个接口,这样判断的结果可能不是你想要的。例如我们判断一个元素是不是一个类:

if (element instanceof TypeElement) { //错误,也有可能是一个接口
}

if (element.getKind() == ElementKind.CLASS) { //正确
    //doSomething
}

下表为ElementKind枚举类中的部分常量,详细信息请查看官方文档。

类型 说明
PACKAGE 一个包。
ENUM 一个枚举类型。
CLASS 没有用更特殊的种类(如 ENUM)描述的类。
ANNOTATION_TYPE 一个注解类型。
INTERFACE 没有用更特殊的种类(如 ANNOTATION_TYPE)描述的接口。
ENUM_CONSTANT 一个枚举常量。
FIELD 没有用更特殊的种类(如 ENUM_CONSTANT)描述的字段。
PARAMETER 方法或构造方法的参数。
LOCAL_VARIABLE 局部变量。
METHOD 一个方法。
CONSTRUCTOR 一个构造方法。
TYPE_PARAMETER 一个类型参数。

类型

TypeMirror是一个接口,表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。以下类型接口全部继承自TypeMirror接口:

类型 说明
ArrayType 表示一个数组类型。多维数组类型被表示为组件类型也是数组类型的数组类型。
DeclaredType 表示某一声明类型,是一个类 (class) 类型或接口 (interface) 类型。这包括参数化的类型(比如 java.util.Set<String>)和原始类型。TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。
ErrorType 表示无法正常建模的类或接口类型。
ExecutableType 表示 executable 的类型。executable 是一个方法、构造方法或初始化程序。
NoType 在实际类型不适合的地方使用的伪类型。
NullType 表示 null 类型。
PrimitiveType 表示一个基本类型。这些类型包括 boolean、byte、short、int、long、char、float 和 double。
ReferenceType 表示一个引用类型。这些类型包括类和接口类型、数组类型、类型变量和 null 类型。
TypeVariable 表示一个类型变量。
WildcardType 表示通配符类型参数。

同样,如果我们想判断一个TypeMirror的类型,应该使用TypeMirror.getKind()方法配合TypeKind枚举类进行判断。尽量避免使用instanceof进行判断,因为比如DeclaredType既表示类 (class) 类型又表示接口 (interface) 类型,这样判断的结果可能不是你想要的。

TypeKind枚举类中的部分常量,详细信息请查看官方文档。

类型 说明
BOOLEAN 基本类型 boolean。
INT 基本类型 int。
LONG 基本类型 long。
FLOAT 基本类型 float。
DOUBLE 基本类型 double。
VOID 对应于关键字 void 的伪类型。
NULL null 类型。
ARRAY 数组类型。
PACKAGE 对应于包元素的伪类型。
EXECUTABLE 方法、构造方法或初始化程序。

创建文件

Filer接口支持通过注解处理器创建新文件。可以创建三种文件类型:源文件、类文件和辅助资源文件。

1.创建源文件

JavaFileObject createSourceFile(CharSequence name,
                                Element... originatingElements)
                                throws IOException

创建一个新的源文件,并返回一个对象以允许写入它。文件的名称和路径(相对于源文件的根目录输出位置)基于该文件中声明的类型。如果声明的类型不止一个,则应该使用主要顶层类型的名称(例如,声明为 public 的那个)。还可以创建源文件来保存有关某个包的信息,包括包注解。要为指定包创建源文件,可以用 name 作为包名称,后跟 ".package-info";要为未指定的包创建源文件,可以使用 "package-info"。

2.创建类文件

JavaFileObject createClassFile(CharSequence name,
                               Element... originatingElements)
                               throws IOException

创建一个新的类文件,并返回一个对象以允许写入它。文件的名称和路径(相对于类文件的根目录输出位置)基于将写入的类型名称。还可以创建类文件来保存有关某个包的信息,包括包注解。要为指定包创建类文件,可以用 name 作为包名称,后跟 ".package-info";为未指定的包创建类文件不受支持。

3.创建辅助资源文件

FileObject createResource(JavaFileManager.Location location,
                          CharSequence pkg,
                          CharSequence relativeName,
                          Element... originatingElements)
                          throws IOException

创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象。该文件可以与新创建的源文件、新创建的二进制文件或者其他受支持的位置一起被查找。位置 CLASS_OUTPUT 和 SOURCE_OUTPUT 必须受支持。资源可以是相对于某个包(该包是源文件和类文件)指定的,并通过相对路径名从中取出。从不太严格的角度说,新文件的完全路径名将是 location、 pkg 和 relativeName 的串联。

对于生成Java文件,还可以使用Square公司的开源类库JavaPoet,感兴趣的同学可以了解下。

打印错误信息

Messager接口提供注解处理器用来报告错误消息、警告和其他通知的方式。

注意:我们应该对在处理过程中可能发生的异常进行捕获,通过Messager接口提供的方法通知用户。此外,使用带有Element参数的方法连接到出错的元素,用户可以直接点击错误信息跳到出错源文件的相应行。如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),这样用户会从javac中得到一个非常难懂出错信息。

方法 说明
void printMessage(Diagnostic.Kind kind, CharSequence msg) 打印指定种类的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e) 在元素的位置上打印指定种类的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a) 在已注解元素的注解镜像位置上打印指定种类的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v) 在已注解元素的注解镜像内部注解值的位置上打印指定种类的消息。

配置选项参数

我们可以通过getOptions()方法获取选项参数,在gradle文件中配置选项参数值。例如我们配置了一个名为yuweiguoCustomAnnotation的参数值。

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ yuweiguoCustomAnnotation : 'io.github.yuweiguocn.customannotation.MyCustomAnnotation' ]
            }
        }
    }
}

在注解处理器中重写getSupportedOptions方法指定支持的选项参数名称。通过getOptions方法获取选项参数值。

public static final String CUSTOM_ANNOTATION = "yuweiguoCustomAnnotation";

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   try {
       String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
       if (resultPath == null) {
           ...
           return false;
       }
       ...
   } catch (Exception e) {
       e.printStackTrace();
       ...
   }
   return true;
}


@Override
public Set<String> getSupportedOptions() {
   Set<String> options = new LinkedHashSet<String>();
   options.add(CUSTOM_ANNOTATION);
   return options;
}

处理过程

Java官方文档给出的注解处理过程的定义:注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。这些初始输入,可以看成是虚拟的第0次的循环的输出。这也就是说我们实现的process方法有可能会被调用多次,因为我们生成的文件也有可能会包含相应的注解。例如,我们的源文件为SourceActivity.class,生成的文件为Generated.class,这样就会有三次循环,第一次输入为SourceActivity.class,输出为Generated.class;第二次输入为Generated.class,输出并没有产生新文件;第三次输入为空,输出为空。

每次循环都会调用process方法,process方法提供了两个参数,第一个是我们请求处理注解类型的集合(也就是我们通过重写getSupportedAnnotationTypes方法所指定的注解类型),第二个是有关当前和上一次 循环的信息的环境。返回值表示这些注解是否由此 Processor 声明,如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们。

public abstract boolean process(Set<? extends TypeElement> annotations,
                                RoundEnvironment roundEnv)

获取注解元素

我们可以通过RoundEnvironment接口获取注解元素。process方法会提供一个实现RoundEnvironment接口的对象。

方法 说明
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 返回被指定注解类型注解的元素集合。
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) 返回被指定注解类型注解的元素集合。
processingOver() 如果循环处理完成返回true,否则返回false。

示例

了解完了相关的基本概念,接下来我们来看一个示例,本示例只为演示无实际意义。主要功能为自定义一个注解,此注解只能用在public的方法上,我们通过注解处理器拿到类名和方法名存储到List集合中,然后生成通过参数选项指定的文件,通过此文件可以获取List集合。

自定义注解:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}

注解处理器中关键代码:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   try {
       String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
       if (resultPath == null) {
           messager.printMessage(Diagnostic.Kind.ERROR, "No option " + CUSTOM_ANNOTATION +
                   " passed to annotation processor");
           return false;
       }

       round++;
       messager.printMessage(Diagnostic.Kind.NOTE, "round " + round + " process over " + roundEnv.processingOver());
       Iterator<? extends TypeElement> iterator = annotations.iterator();
       while (iterator.hasNext()) {
           messager.printMessage(Diagnostic.Kind.NOTE, "name is " + iterator.next().getSimpleName().toString());
       }

       if (roundEnv.processingOver()) {
           if (!annotations.isEmpty()) {
               messager.printMessage(Diagnostic.Kind.ERROR,
                       "Unexpected processing state: annotations still available after processing over");
               return false;
           }
       }

       if (annotations.isEmpty()) {
           return false;
       }

       for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
           if (element.getKind() != ElementKind.METHOD) {
               messager.printMessage(
                       Diagnostic.Kind.ERROR,
                       String.format("Only methods can be annotated with @%s", CustomAnnotation.class.getSimpleName()),
                       element);
               return true; // 退出处理
           }

           if (!element.getModifiers().contains(Modifier.PUBLIC)) {
               messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must be public", element);
               return true;
           }

           ExecutableElement execElement = (ExecutableElement) element;
           TypeElement classElement = (TypeElement) execElement.getEnclosingElement();
           result.add(classElement.getSimpleName().toString() + "#" + execElement.getSimpleName().toString());
       }
       if (!result.isEmpty()) {
           generateFile(resultPath);
       } else {
           messager.printMessage(Diagnostic.Kind.WARNING, "No @CustomAnnotation annotations found");
       }
       result.clear();
   } catch (Exception e) {
       e.printStackTrace();
       messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in CustomProcessor: " + e);
   }
   return true;
}

private void generateFile(String path) {
   BufferedWriter writer = null;
   try {
       JavaFileObject sourceFile = filer.createSourceFile(path);
       int period = path.lastIndexOf('.');
       String myPackage = period > 0 ? path.substring(0, period) : null;
       String clazz = path.substring(period + 1);
       writer = new BufferedWriter(sourceFile.openWriter());
       if (myPackage != null) {
           writer.write("package " + myPackage + ";\n\n");
       }
       writer.write("import java.util.ArrayList;\n");
       writer.write("import java.util.List;\n\n");
       writer.write("/** This class is generated by CustomProcessor, do not edit. */\n");
       writer.write("public class " + clazz + " {\n");
       writer.write("    private static final List<String> ANNOTATIONS;\n\n");
       writer.write("    static {\n");
       writer.write("        ANNOTATIONS = new ArrayList<>();\n\n");
       writeMethodLines(writer);
       writer.write("    }\n\n");
       writer.write("    public static List<String> getAnnotations() {\n");
       writer.write("        return ANNOTATIONS;\n");
       writer.write("    }\n\n");
       writer.write("}\n");
   } catch (IOException e) {
       throw new RuntimeException("Could not write source for " + path, e);
   } finally {
       if (writer != null) {
           try {
               writer.close();
           } catch (IOException e) {
               //Silent
           }
       }
   }
}

private void writeMethodLines(BufferedWriter writer) throws IOException {
   for (int i = 0; i < result.size(); i++) {
       writer.write("        ANNOTATIONS.add(\"" + result.get(i) + "\");\n");
   }
}

编译输出:

Note: round 1 process over false
Note: name is CustomAnnotation
Note: round 2 process over false
Note: round 3 process over true

获取完整代码:https://github.com/yuweiguocn/CustomAnnotation

关于上传自定义注解处理器到jcenter中,请查看上传类库到jcenter

很高兴你能阅读到这里,此时再去看EventBus 3.0中的注解处理器的源码,相信你可以很轻松地理解它的原理。

注意:如果你clone了工程代码,你可能会发现注解和注解处理器是单独的module。有一点可以肯定的是我们的注解处理器只需要在编译的时候使用,并不需要打包到APK中。因此为了用户考虑,我们需要将注解处理器分离为单独的module。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容