• 注解处理器(APT)是什么?


    感谢大家和我一起,在Android世界打怪升级!

    上一篇讲完注解,这篇咱们科普一下注解的其中一种用途——注解处理器(APT),文章会手把手的帮助大家学会APT的使用,并使用简单的例子来进行练习。

    一、定义

    注解处理器(Annotation Processing Tool,简称APT),是JDK提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。处理方式大部分都是根据注解的信息生成新的Java代码与文件。

    APT使用相当广泛,EventBus、ARouter、ButterKnife等流行框架都使用了该技术。

    二、生成注解处理器

    2.1 创建注解模块

    在项目中新建Module,选择【Java or Kotlin Library】,名字和包名随意填入,点击Finish。

    ② 在模块中定义注解,注解保留范围选择SOURCE即可(因为APT是作用在源码阶段的,生成class之前),当然选择CLASS和RUNTIME也可以,因为他们都包含SOURCE阶段。

    我们创建一个Test注解,并且包含int、String、Class、String[]四种类型。

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Test {
        int key();
    
        String value() default "";
    
        Class clazz();
    
        String[] array() default {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.2 创建注解处理器模块

    ① 在项目中新建Module,选择【Java or Kotlin Library】,名字和包名随意填入,点击Finish。

    ② 修改新建Module的build.gradle文件,根据是否使用Kotlin分为两种情况

    项目不使用Kotlin

    apply plugin: "java-library"
    
    dependencies {
    	// 注解模块(必选)
        implementation project(':lib-annotation')
    	// 注解处理器(必选)
        compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
        annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
        // 生成代码方式之一(可选)
        implementation 'com.squareup:javapoet:1.13.0'
    }
    
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    项目使用Kotlin:

    apply plugin: "java-library"
    apply plugin: "kotlin"
    apply plugin: "kotlin-kapt"
    
    dependencies {
    	// 注解模块(必选)
        implementation project(':lib-annotation')
        // 注解处理器(必选)
        kapt 'com.google.auto.service:auto-service:1.0-rc7'
        implementation 'com.google.auto.service:auto-service:1.0-rc7'
        // 生成代码方式之一(可选)
        implementation 'com.squareup:javapoet:1.13.0'
    }
    
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.3 创建注解处理器

    在2.2的注解处理器模块中新建类,继承自AbstractProcessor类
    使用@AutoService、@SupportedAnnotationTypes、@SupportedSourceVersion注解注释该类,注解处理器即创建完成,具体如下:

    // 让该类拥有了获取注解的能力(必选)
    @AutoService(Processor.class)
    // 设置该处理器支持哪几种注解(必选)
    // 字符串类型,例:com.kproduce.annotation.TEST
    @SupportedAnnotationTypes({Const.CARD_ANNOTATION,Const.TEST_ANNOTATION})
    // 源码版本(可选)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class TestAnnotationProcessor extends AbstractProcessor {
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
        }
        
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.4 在app模块中引入注解处理器

    在主项目app模块的build.gradle中引入注解处理器,使用kapt或annotationProcessor修饰,所以在gradle中看到这两种修饰的项目就是注解处理器项目了。

    dependencies {
    	// 注解模块
        implementation project(":lib-annotation")
        // 注解处理器模块,以下二选一
        // 使用Kotlin选择这种
        kapt project(":compiler")
        // 使用Java选择这种
        annotationProcessor project(":compiler")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.5 测试

    经过上面的一系列操作,注解处理器已经注册完成,在其process方法中会接收到想要处理的注解。

    ① 在项目中使用@Test注解

    @Test(id = 100, desc = "Person类", clazz = Person.class, array = {"111", "aaa", "bbb"})
    public class Person {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    ② 在注解处理器的init方法中,可以通过ProcessingEnvironment参数获取Messager对象(可以打印日志),在process方法中获取到注解后输出日志查看被注解的类名。(注意:如果打印日志使用Diagnostic.Kind.ERROR,会中断构建)

    @AutoService(Processor.class)
    @SupportedAnnotationTypes(Const.TEST_ANNOTATION)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class TestAnnotationProcessor extends AbstractProcessor {
    
        private Messager messager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            // 获取Messager对象
            messager = processingEnv.getMessager();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        	// 获取所有的被Test注解的对象,无论是类还是属性都会被封装成Element
            for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) {
                messager.printMessage(Diagnostic.Kind.NOTE, ">>>>>>>>>>>>>>>GetAnnotation:" + element.getSimpleName());
            }
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    ③ 构建项目,查看日志,成功获取到注解注释过的类名

    三、解析注解

    获取到了注解,我们看一下如何正确的拿到注解内的信息,在注解处理器中类、方法、属性都会被形容成Element,由于我们定义的@Test只修饰类,所以Element也都是类。

    @AutoService(Processor.class)
    @SupportedAnnotationTypes(Const.TEST_ANNOTATION)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class TestAnnotationProcessor extends AbstractProcessor {
    
        private Messager messager;
        // 这个是处理Element的工具
        private Elements elementTool;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            messager = processingEnv.getMessager();
            elementTool = processingEnv.getElementUtils();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        	// 拿到被Test修饰的Element,因为我们只修饰类,所以拿到的Element都是类
            for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) {
            	// ===============获取当前被修饰的类的信息===============
            	
            	// 获取包名,例:com.kproduce.androidstudy.test
                String packageName = elementTool.getPackageOf(element).getQualifiedName().toString();
                // 获取类名,例:Person
                String className = element.getSimpleName().toString();
    			// 拼装成文件名,例:com.kproduce.androidstudy.test.Person
                String fileName = packageName + Const.DOT + className;
                
    			// ===============解析注解===============
    			
    			// 获取注解
                Test card = element.getAnnotation(Test.class);
    			// 注解中的int值
                int id = card.id();
                // 注解中的String
                String desc = card.desc();
                // 注解中的数组[]
                String[] array = card.array();
                // 获取类有比较奇葩的坑,需要特别注意!
    			// 在注解中拿Class然后调用getName()会抛出MirroredTypeException异常
    			// 处理方式可以通过捕获异常后,在异常中获取类名
                String dataClassName;
                try {
                    dataClassName = card.clazz().getName();
                } catch (MirroredTypeException e) {
                    dataClassName = e.getTypeMirror().toString();
                }
            }
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    四、生成代码

    获取到了注解信息,下一步就是根据注解生成Java代码了。但是生成代码的意义是什么呢?

    答:WMRouter路由在获取到了注解信息之后,会根据注解的内容生成路由注册的代码。 把一些复杂的可能写错的冗长的代码变成了自动生成,避免了人力的浪费和错误的产生。下面是WMRouter生成的代码:

    WMRouter自动生成的代码

    目前生成Java代码有两种方式,原始方式和JavaPoet。原始方式理解即可,咱们使用JavaPoet来解析注解、生成代码。

    4.1 原始方式

    原始方式就是通过流一行一行的手写代码。
    优点:可读性高。
    缺点:复用性差。

    咱们看一下EventBus的源码就能更深刻的理解什么是原始方式:

    // 截取EventBusAnnotationProcessor.java中的片段
    private void createInfoIndexFile(String index) {
        BufferedWriter writer = null;
        try {
            JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index);
            int period = index.lastIndexOf('.');
            String myPackage = period > 0 ? index.substring(0, period) : null;
            String clazz = index.substring(period + 1);
            writer = new BufferedWriter(sourceFile.openWriter());
            if (myPackage != null) {
                writer.write("package " + myPackage + ";\n\n");
            }
            writer.write("import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n");
            writer.write("import org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n");
            writer.write("import org.greenrobot.eventbus.meta.SubscriberInfo;\n");
            writer.write("import org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n");
            writer.write("import org.greenrobot.eventbus.ThreadMode;\n\n");
            writer.write("import java.util.HashMap;\n");
            writer.write("import java.util.Map;\n\n");
            writer.write("/** This class is generated by EventBus, do not edit. */\n");
            writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n");
            writer.write("    private static final Map, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
            writer.write("    static {\n");
            writer.write("        SUBSCRIBER_INDEX = new HashMap, SubscriberInfo>();\n\n");
            writeIndexLines(writer, myPackage);
            writer.write("    }\n\n");
            writer.write("    private static void putIndex(SubscriberInfo info) {\n");
            writer.write("        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n");
            writer.write("    }\n\n");
            writer.write("    @Override\n");
            writer.write("    public SubscriberInfo getSubscriberInfo(Class subscriberClass) {\n");
            writer.write("        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n");
            writer.write("        if (info != null) {\n");
            writer.write("            return info;\n");
            writer.write("        } else {\n");
            writer.write("            return null;\n");
            writer.write("        }\n");
            writer.write("    }\n");
            writer.write("}\n");
        } catch (IOException e) {
            throw new RuntimeException("Could not write source for " + index, e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    //Silent
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    4.2 JavaPoet

    JavaPoet是使用Java的API和面向对象思想来生成.java文件的库。
    优点:面向对象思想、复用性高。
    缺点:学习成本高、可读性一般。

    因为学习点比较多,咱们仅对用到的API进行说明,其他的可以参考GitHub地址,里面有相当全面的教程。

    4.2.1 生成代码

    我们先用JavaPoet生成一个HelloWorld类,下面是我们想要的Java代码:

    package com.example.helloworld;
    
    public final class HelloWorld {
      public static void main(String[] args) {
        System.out.println("Hello, JavaPoet!");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在JavaPoet中使用了面向对象的思想,万物皆对象,方法和类也变成了对象。在类中代码主要被分为了两块,一块是方法(MethodSpec),一块是类(TypeSpec)。

    接下来我们使用JavaPoet生成这段代码,比较易懂。
    ① 先创建main方法的MethodSpec对象;
    ② 再创建HelloWorld类的TypeSpec对象,将main方法传入。

    // 创建main方法的MethodSpec对象
    MethodSpec main = MethodSpec.methodBuilder("main")	// 方法名:main
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)	// 方法修饰:public static
        .returns(void.class)	// 返回类型 void
        .addParameter(String[].class, "args")	// 参数:String[] args
        .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")	// 内容System.out.println("Hello, JavaPoet!");
        .build();
        
    // 创建HelloWorld类的TypeSpec对象,将main方法传入
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")	// 类名:HelloWorld
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)	// 类修饰:public final
        .addMethod(main)	// 添加方法main
        .build();
    
    // 构建生成文件,第一个参数为包名
    JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
        .build();
    javaFile.writeTo(System.out);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    经过以上步骤就可以生成HelloWorld类。

    4.2.2 JavaPoet中的自定义类型

    上面代码中会发现几个奇怪的类型写在字符串中,详细的讲解可以查看GitHub地址

    $L:值,可以放各种对象,比如int,Object等。
    $S:字符串。
    $T:类的引用,会自动导入该类的包,比如new Date()中的Date。
    $N:定义好的Method方法名,可以调用代码中的其他方法。

    4.2.3 各种案例

    提供几个案例,更好的理解JavaPoet,详细的讲解可以查看GitHub地址

    ① 循环

    void main() {
      int total = 0;
      for (int i = 0; i < 10; i++) {
        total += i;
      }
    }
    
    // JavaPoet方式 1
    MethodSpec main = MethodSpec.methodBuilder("main")
        .addStatement("int total = 0")
        .beginControlFlow("for (int i = 0; i < 10; i++)")
        .addStatement("total += i")
        .endControlFlow()
        .build();
        
    // JavaPoet方式 2
    MethodSpec main = MethodSpec.methodBuilder("main")
        .addCode(""
            + "int total = 0;\n"
            + "for (int i = 0; i < 10; i++) {\n"
            + "  total += i;\n"
            + "}\n")
        .build();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    ② ArrayList

    package com.example.helloworld;
    
    import com.mattel.Hoverboard;
    import java.util.ArrayList;
    import java.util.List;
    
    public final class HelloWorld {
      List<Hoverboard> beyond() {
        List<Hoverboard> result = new ArrayList<>();
        result.add(new Hoverboard());
        result.add(new Hoverboard());
        result.add(new Hoverboard());
        return result;
      }
    }
    
    // JavaPoet方式
    ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
    ClassName list = ClassName.get("java.util", "List");
    ClassName arrayList = ClassName.get("java.util", "ArrayList");
    TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);
    
    MethodSpec beyond = MethodSpec.methodBuilder("beyond")
        .returns(listOfHoverboards)
        .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
        .addStatement("result.add(new $T())", hoverboard)
        .addStatement("result.add(new $T())", hoverboard)
        .addStatement("result.add(new $T())", hoverboard)
        .addStatement("return result")
        .build();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    ③ 属性

    public class HelloWorld {
      private final String android;
      private final String robot;
    }
    
    // JavaPoet方式
    FieldSpec android = FieldSpec.builder(String.class, "android")
        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
        .build();
        
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
        .addModifiers(Modifier.PUBLIC)
        .addField(android)
        .addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL)
        .build();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    总结

    最后咱们再总结一下APT。

    1. APT是JDK提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。
    2. 获取到注解后可以使用原始方法与JavaPoet生成Java代码。

    这样APT的介绍就结束了,希望大家读完这篇文章,会对APT有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。

    下次再见!

  • 相关阅读:
    ES6模块化(ES module)
    【mitmproxy】一、简介与快速上手
    产品-Axure9(英文版),中继器(Repeater)实现表格内容的增删查改(CRUD)
    吴恩达深度学习个人笔记
    一带一路合作高峰论坛召开,附市场开发建议
    matlab在管理学中的应用简matlab基础【二】
    力扣206 - 反转链表【校招面试高频考题】
    USB3.1HUB驱动芯片VL822
    牛客刷题<32~34>非整数倍数和整数倍数数据位宽转换
    将强化学习重新引入 RLHF
  • 原文地址:https://blog.csdn.net/kuanggang_android/article/details/127685124