@lombok注解背后的原理是什么,让我们走近自定义Java注解处理器

Posted 黄小斜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了@lombok注解背后的原理是什么,让我们走近自定义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)和原始类型。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 的伪类型。
NULLnull 类型。
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.jianshu.com/p/50d95fbf635c/
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

以上是关于@lombok注解背后的原理是什么,让我们走近自定义Java注解处理器的主要内容,如果未能解决你的问题,请参考以下文章

从“半路出家”到“技术大拿”,走近Windows 11背后的中国程序员

面试官:你天天用 Lombok,说说它什么原理?我竟然答不上来…

面试官:你天天用 Lombok,说说它什么原理?我竟然答不上来…

lombok中的builder注解居然是一种设计模式:让我们了解一下超级实用的“建造者模式”吧

lombok的SneakyThrows究竟还原成什么代码它的用法

学习注解和了解lombok的原理