@lombok注解背后的原理是什么,让我们走近自定义java注解处理器(代码片段)

黄小斜 黄小斜     2023-02-10     738

关键词:

本文介绍了如何自定义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原理分析与功能实现(代码片段)

...意。刚这两天读”深入理解JVM”的时候突然想起来有个叫Lombok的东西以前一直不能理解他的实现原理,现在正好趁着闲暇的时间研究研究。Lombok代码Lombok是一个开源项目,源代码托管在GITHUB/rzwitserloot,如果需要在maven里引用,只... 查看详情

lombok注解介绍

lombok注解介绍lombok注解文档lombok官网下载lombok是一个可以帮助我们简化java代码编写的工具类,尤其是简化javabean的编写,即通过采用注解的方式,消除代码中的构造方法,getter/setter等代码,使我们写的类更加... 查看详情

学习注解和了解lombok的原理(代码片段)

注解的定义注解通过@interface关键字进行定义。packagecom;importjava.lang.annotation.*;public@interfaceTestSelect元注解元注解就是用来修饰注解的。常用的元注解有@Retention和@Target。@Retention注解可以理解为设置注解的生命周期。... 查看详情

lombok之使用详解(代码片段)

...使用起来很是方便,其实,我们还有更方便的办法,那就是-Lombok:非常强大的POJO注解器。Lombok是什么?  lombok 提供了简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的java代码。 查看详情

lombok介绍(代码片段)

Lombok是什么Lombok是一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,通过使用对应的注解,可以在编译源码的时候生成对应的方法.官方地址: https://projectlombok.org/ github地址:&n... 查看详情

从“半路出家”到“技术大拿”,走近windows11背后的中国程序员(代码片段)

在学校学的是汽车专业,毕业后改行程序员,从艳吉他是如何一步步走近“大厂”的呢?现为微软工程师,他平时都在写“bug”吗?他又面临程序员35岁危机了吗?下面让我们看看从艳吉的程序人生是什么... 查看详情

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

lombok中的builder注解本质上是为你生成了一个构造器Builder类,通过这个类我们可以构造出带此注解的对象。本质上它实现了设计模式中一种经典的模式:建造者模式1.认识:①一句话来说:封装一个复杂对象的构建... 查看详情

git内部原理探索(代码片段)

...Git是如何存储文件信息的当我们执行gitadd、gitcommit时,Git背后做了什么Git分支的本质是什么HEAD引用参考@前言洞悉技术的本质,可以让我们在层出不穷的框架面前仍能泰然处之。用了那么久的Git,不懂点内部原理,那可不行!懂... 查看详情

“Serializable”接口背后的基本原理是啥? [复制]

】“Serializable”接口背后的基本原理是啥?[复制]【英文标题】:What\'stherationalebehind"Serializable"interface?[duplicate]“Serializable”接口背后的基本原理是什么?[复制]【发布时间】:2015-04-2515:32:06【问题描述】:如果我们想序... 查看详情

lombok使用(代码片段)

1.Lombok简介Lombok是一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,通过使用对应的注解,可以在编译源码的时候生成对应的方法。2.使用方法在IDEA中使用,首先安装插件,这样才能在... 查看详情

lombok常见注解(代码片段)

一、使用lombok简化代码lombok提供了很多注解,在编译时候生成java代码,代替了手工编写一些简单的代码,使程序员可以关注更重要的实现。二、常用注解以model为例publicclassDataDemoprivateIntegerid;privateStringname;privateDatetime;一下是添... 查看详情

lombok使用及常用注解

...一,这些工作肯定已经有大牛封装好了处理方法,这就是lombok。ide 查看详情

人脸识别背后,卷积神经网络的数学原理原来是这样的

...认为是不可能的事情。卷积神经网络可能是这一巨大成功背后的关键组成模块。这次,我们将要使用卷积神经网络的思想来拓宽我们对神经网络工作原理的理解。简介过去我们接触到了密集连接的神经网络。那些神经网络中,所... 查看详情

了解lombok插件(代码片段)

Lombok是什么Lombok可以通过注解形式帮助开发人员解决POJO冗长问题,帮助构造简洁和规范的代码,通过注解可产生相应的方法。Lombok如何在IDEA中使用我们都知道,使用一种工具,一定要在Maven中添加相应的依赖在pom.xml中添加依赖&... 查看详情

lombok注解介绍

lombok注解介绍lombok注解文档lombok官网下载lombok是一个可以帮助我们简化java代码编写的工具类,尤其是简化javabean的编写,即通过采用注解的方式,消除代码中的构造方法,getter/setter等代码,使我们写的类更加... 查看详情

阿昌教你使用常用lombok@注解

...ff61;・∀・)ノ゙!今天阿昌分享的是常用的Lombok注解,官方地址,github地址做Java开发的,哪个不知道这小辣椒呢。Lombok是一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java... 查看详情

lombok插件使用方法步骤

什么是Lombok?Lombok是一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,通过使用对应的注解,可以在编译源码的时候生成对应的方法。 1、添加依赖找到maven依赖仓库选一个依赖。&l... 查看详情

走近abstractqueuedsynchronizer

...k,ConcurrentHashMap中的Segment,阻塞队列,CountDownLatch等。按我们一贯的风格,让我们直接走近设计者对其的诠释。在java.util.concurrent.locks包中,AbstractQueuedSynchronizer直接继承 查看详情