android注解之APT和javapoet

前言

前面我们已经讲过注解的基本知识,对于注解还不太了解的,可以去看一下之前的文章,

android 注解详解_袁震的博客-CSDN博客

之前我们在讲注解的时候,提到过APT和JavaPoet,那么什么是APT和JavaPoet呢?下面我们来详细讲解一下。

1,APT是什么?

APT,英文全称Annotation Processing Tool,是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码, 如果想要自定义的注解处理器能够正常运行,必须要通过APT工具来进行处理。 也可以这样理解,只有通过声明APT工具后,程序在编译期间自定义注解解释器才能执行。注意,是在编译期间。

简单来讲,就是根据我们定义的注释规则,帮助我们生成代码,生成类文件。

2,APT中的元素

在APT中,它会分为包元素,类元素,属性元素,方法元素。那么,这些元素的意义是什么呢?

首先,我们需要明白APT真正的作用是什么。就拿ButterKnife来说,他真正要实现的就是我们通过BindView,把id传给注解,然后就会在编译时动态生成很多类,专门去处理你绑定的这些id,从而达到你只需要几行代码就能实现绑定,点击事件等功能。那为什么不能直接就写好类去处理,而非要到编译期去自动生成类处理呢?因为它不知道你会传哪些id,所以需要动态的生成。

所以,我认为APT的主要作用就是帮你动态生成类。

java 复制代码
import androidx.appcompat.app.AppCompatActivity;//PackageElement 包元素/节点

public class MainActivity2 extends AppCompatActivity { // TypeElement 类元素/节点

    private int a;// VariableElement 属性元素/节点

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {// ExecuteableElement 方法元素/节点
        super.onCreate(savedInstanceState);
    }
}

这些元素的意义就是,它们会提供相关信息,来帮助你后面生成类。

|----------|-----------------------|------------------------------------|
| 包元素 | PackageElement | 表示一个包程序元素。提供对有关包及其成员的信息的访问 |
| 方法元素 | ExecutableElement | 表示某个类或接口的方法、构造方法或初始化程序(静态或实例) |
| 类元素 | TypeElement | 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问 |
| 属性元素 | VariableElement | 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |

3,APT中常用的API

在AbstractProcessor中,有两个方法是核心方法:

java 复制代码
    
//初始化工作,主要做一些准备工作
public synchronized void init(ProcessingEnvironment processingEnv) {
    
}

//处理注解 核心方法
//annotations 使用了支持处理注解的节点集合
//roundEnv 当前或是之前的运行环境,可以通过该对象查找的注解
//return true 表示后续处理器不会再处理(已经处理完成)
public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

上面ProcessingEnvironment中常用的api如下:

|-------------------|-------------------------------|
| getElementUtils() | 获取操作Element的工具类 |
| getMessager() | 获取Messager,用来打印日志相关信息 |
| getFiler() | 获取文件生成器,类等最终要生成的文件,都是通过生成器生成的 |
| getTypeUtils() | 获取类信息的工具类,用于操作TypeMirror的工具方法 |
| getOptions() | 主要用来接收应用传过来的数据 |

上面RoundEnvironment常用api如下:

java 复制代码
//获取所有被@YuanZhen注解的元素集合
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(YuanZhen.class);

Element常用api如下:

|-----------------------|----------------------------------|
| getEnclosedElements() | 返回该元素直接包含的子元素 |
| getEnclosingElement() | 返回包含该element的父element,与上一个方法相反 |
| getKind() | 返回element的类型,判断是哪种element |
| getModifiers() | 获取修饰关键字,入public static final等关键字 |
| getSimpleName() | 获取名字,不带包名 |
| getQualifiedName() | 获取全名,如果是类的话,包含完整的包名路径 |
| getParameters() | 获取方法的参数元素,每个元素是一个VariableElement |
| getReturnType() | 获取方法元素的返回值 |
| getConstantValue() | 如果属性变量被final修饰,则可以使用该方法获取它的值 |

4,APT环境搭建

4.1新建注解javalib,命名为compiler

4.2在compiler的buidl.gradle中添加依赖,用来注册注解处理器

Groovy 复制代码
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'

4.3新建AnnotationProcessor

新建MyAnnotationProcessor类,继承AnnotationProcessor

java 复制代码
// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)

// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({"com.yuanzhen.yuanzhenannotation.YuanZhen"})

// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyAnnotationProcessor extends AbstractProcessor {

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

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

4.4新建注解javalib 命名为yuanzhenannotation

4.5创建自己的注解

java 复制代码
@Target(ElementType.TYPE) //作用与类上
@Retention(RetentionPolicy.SOURCE) //在编译时期生效
public @interface YuanZhen {

    String value();//一个默认值
}

4.6添加依赖

在compiler中依赖yuanzhenannotation

在app中添加依赖:

4.7在app中使用注解:

java 复制代码
@YuanZhen("study")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

至此,APT环境搭建完成

5,APT获取注解

在MyAnnotationProcessor类中:

java 复制代码
// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)

// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({"com.yuanzhen.yuanzhenannotation.YuanZhen"})

// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyAnnotationProcessor extends AbstractProcessor {

    private Messager messager;// 用来打印日志相关信息
    private Elements elementUtils;// 操作Element的工具类(类,函数,属性,其实都是Element)
    private Filer filer;//文件生成器, 类 资源 等,就是最终要生成的文件 是需要Filer来完成的
    private Types typeUtils;// type(类信息)的工具类,包含用于操作TypeMirror的工具方法

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        typeUtils = processingEnv.getTypeUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //因为javalib没有Log,所以我们使用messager来打印
        messager.printMessage(Diagnostic.Kind.NOTE,"aaaaaaZZZZZ");//打印 
        //获取所有被@YuanZhen注解的元素集合
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(YuanZhen.class);
        for (Element element : elements) {
            String className =element.getSimpleName().toString();//获取元素名
            messager.printMessage(Diagnostic.Kind.NOTE,"-----="+className);//打印类名
            YuanZhen annotation = element.getAnnotation(YuanZhen.class);//获取注解
            messager.printMessage(Diagnostic.Kind.NOTE,"-----value="+annotation.value());//打印注解参数
        }
        return true;
    }
}

具体的api在上文已经有个介绍,下面看Build日志输出:

参数study已经传递过来,类名MainActivity也已经获取到。

6,传统的生成类的方式

上面已经完成了APT环境的配置,下面就是动态生成类了,在javapoet之前,传统的生成类的方式就是采用字符串拼接的样式。最典型的应用就是EventBus。

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<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
        writer.write("    static {\n");
        writer.write("        SUBSCRIBER_INDEX = new HashMap<Class<?>, 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
            }
        }
    }
}

通过上面可以看到,Eventbus是通过字符串拼接的形式来生成类的,这种方式虽然比较简单,但是也不是一件轻松的体力活,不符合OOP的编程思想。下面我们就来介绍一下现在最流行的写法,采用javapoet。

7,JavaPoet简介

JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件,这个框架功能非常实用,也是我们习惯的Java面向对象OOP语法, 可以很方便的使用它根据注解生成对应代码。 通过这种自动化生成代码的方式, 可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。

8,JavaPoet中常用的类

|-----------------------------------------------|------------------------------|
| MethodSpec | 代表一个构造函数或方法声明 |
| TypeSpec | 代表一个类,接口,或者枚举声明 |
| FieldSpec | 代表一个成员变量,一个字段声明 |
| JavaFile | 包含一个顶级类的Java文件 |
| ParameterSpec | 用来创建参数 |
| AnnotationSpec | 用来创建注解 |
| ClassName | 用来包装一个类 |
| TypeName | 类型,如在添加返回值类型是使用 TypeName.VOI |
| S 字符串,如:S, "hello T 类、接口,如:T, MainActivit | |

9,JavaPoet生成类的思想

传统模式生成类的思想是先写包,然后类,然后方法。

JavaPoet的思想恰恰相反,它是先方法,然后类,然后包。

10,JavaPoet 使用

10.1 导包

在complier的build.gradle里面导包

Groovy 复制代码
 implementation "com.squareup:javapoet:1.9.0"

10.2写出要生成的class

java 复制代码
/**
package com.yuanzhen.yuanzhenannotation;

public class MyClass {

    public static void main(String[] args) {
        System.out.println("Hello, yuanzhen");
    }
}
*/

10.3 通过javapoet生成该类

java 复制代码
 // 1.方法 
MethodSpec mainMethod = MethodSpec.methodBuilder("main")//添加方法名
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//添加修饰符
                .returns(void.class)//添加返回值
                .addParameter(String[].class, "args")//添加方法参数
                .addStatement("$T.out.println($S)", System.class, "Hello, YuanZhen!")//添加内容
                .build();
        // 2.类
        TypeSpec helloWorld = TypeSpec.classBuilder("MyClass1")//添加类名
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)//添加修饰符
                .addMethod(mainMethod)//添加方法
                .build();
        // 3.包
        JavaFile packagef = JavaFile.builder("com.yuanzhen.apt1", helloWorld).build();
        // 去生成
        try {
            packagef.writeTo(filer);
            messager.printMessage(Diagnostic.Kind.NOTE, "success...");
        } catch (IOException e) {
            e.printStackTrace();
            messager.printMessage(Diagnostic.Kind.NOTE, "error...");
        }

10.4在app build中查看生成的文件

11,总结

关于APT和JavaPoet的使用就讲完了,利用这个技术,我们可以实现很多强大的功能。

相关推荐
张拭心2 分钟前
Android 17 来了!新特性介绍与适配建议
android·前端
SimonKing1 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean2 小时前
Jackson View Extension Spring Boot Starter
java·后端
Kapaseker2 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴3 小时前
Android17 为什么重写 MessageQueue
android
Seven973 小时前
剑指offer-79、最⻓不含重复字符的⼦字符串
java
皮皮林55112 小时前
Java性能调优黑科技!1行代码实现毫秒级耗时追踪,效率飙升300%!
java
冰_河12 小时前
QPS从300到3100:我靠一行代码让接口性能暴涨10倍,系统性能原地起飞!!
java·后端·性能优化
桦说编程15 小时前
从 ForkJoinPool 的 Compensate 看并发框架的线程补偿思想
java·后端·源码阅读