关于java:lombok注解背后的原理是什么让我们走近自定义Java注解处理器

42次阅读

共计 11642 个字符,预计需要花费 30 分钟才能阅读完成。

本文介绍了如何自定义 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。

参考

  • https://www.race604.com/annotation-processing/
  • http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/package-summary.html
  • http://tool.oschina.net/uploads/apidocs/jdk-zh/
  • https://github.com/greenrobot/EventBus
  • http://hannesdorfmann.com/annotation-processing/annotationprocessing101
  • https://github.com/sockeqwe/annotationprocessing101

作者:于卫国
链接:https://www.jianshu.com/p/50d…
起源:简书
简书著作权归作者所有,任何模式的转载都请分割作者取得受权并注明出处。

正文完
 0