Android APT 系列 (三):APT 技术探究

news2024/11/15 20:45:28

APT 介绍

什么是 APT ?

APT 全称 Annotation Processing Tool,翻译过来即注解处理器。引用官方一段对 APT 的介绍:APT 是一种处理注释的工具, 它对源代码文件进行检测找出其中的注解,并使用注解进行额外的处理。
APT 有什么用?

APT 能在编译期根据编译阶段注解,给我们自动生成代码,简化使用。很多流行框架都使用到了APT技术,如 ButterKnife,Retrofit,Arouter,EventBus 等等
APT 工程
1)、APT 工程创建

一般情况下,APT 大致的的一个实现过程:
1、创建一个Java Module ,用来编写注解
2、创建一个Java Module,用来读取注解信息,并根据指定规则,生成相应的类文件
3、创建一个Android Module ,通过反射获取生成的类,进行合理的封装,提供给上层调用
如下图:
在这里插入图片描述这是我的 APT工程,关于 Module名称可以任意取,按照我上面说的规则去进行就好了

2)、Module 依赖

工程创建好后,我们就需要理清楚各个 Module 之间的一个依赖关系:

1、因为 apt-processor 要读取apt-annotation的注解,所以apt-processor需要依赖apt-annotation

//apt-processor 的 build.gradle 文件
dependencies {
    implementation project(path: ':apt-annotation')
}

2、app 作为调用层,以上 3 个 Module都需要进行依赖

//app 的 build.gradle 文件
dependencies {
    //...
    implementation project(path: ':apt-api')
    implementation project(path: ':apt-annotation')
    annotationProcessor project(path: ':apt-processor')
}

APT工程配置好之后,我们就可以对各个 Module 进行一个具体代码的编写了
apt-annotation 注解编写

这个 Module的处理相对来说很简单,就是编写相应的自定义注解就好了,我编写的如下:

@Inherited
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface AptAnnotation {
    String desc() default "";
}

apt-processor 自动生成代码

这个 Module 相对来说比较复杂,我们把它分为以下 3 个步骤:
1、注解处理器声明
2、注解处理器注册
3、注解处理器生成类文件
1)、注解处理器声明

1、新建一个类,类名按照自己的喜好取,继承javax.annotation.processing这个包下的 AbstractProcessor类并实现其抽象方法

public class AptAnnotationProcessor extends AbstractProcessor {
  
    /**
     * 编写生成 Java 类的相关逻辑
     *
     * @param set              支持处理的注解集合
     * @param roundEnvironment 通过该对象查找指定注解下的节点信息
     * @return true: 表示注解已处理,后续注解处理器无需再处理它们;false: 表示注解未处理,可能要求后续注解处理器处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

重点看下第一个参数中的 TypeElement ,这个就涉及到Element 的知识,我们简单的介绍一下:
Element 介绍
实际上,Java 源文件是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element,例如包,类,字段,方法等等:

package com.dream;         // PackageElement:包元素

public class Main<T> {     // TypeElement:类元素; 其中 <T> 属于 TypeParameterElement 泛型元素

    private int x;         // VariableElement:变量、枚举、方法参数元素

    public Main() {        // ExecuteableElement:构造函数、方法元素
    }
}

Java 的 Element是一个接口,源码如下:

public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 获取元素的类型,实际的对象类型
    TypeMirror asType();
    // 获取Element的类型,判断是哪种Element
    ElementKind getKind();
    // 获取修饰符,如public static final等关键字
    Set<Modifier> getModifiers();
    // 获取类名
    Name getSimpleName();
    // 返回包含该节点的父节点,与getEnclosedElements()方法相反
    Element getEnclosingElement();
    // 返回该节点下直接包含的子节点,例如包节点下包含的类节点
    List<? extends Element> getEnclosedElements();

    @Override
    boolean equals(Object obj);
  
    @Override
    int hashCode();
  
    @Override
    List<? extends AnnotationMirror> getAnnotationMirrors();
  
    //获取注解
    @Override
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
  
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}

我们可以通过Element获取如上一些信息(写了注释的都是一些常用的)
由 Element衍生出来的扩展类共有 5 种:
1、PackageElement表示一个包程序元素
2、TypeElement表示一个类或者接口程序元素
3、TypeParameterElement 表示一个泛型元素
4、VariableElement表示一个字段、enum常量、方法或者构造方法的参数、局部变量或异常参数
5、ExecutableElement表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)
可以发现,Element 有时会代表多种元素,例如 TypeElement代表类或接口,此时我们可以通过 element.getKind()来区分:

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
        // 如果元素是类

    } else if (element.getKind() == ElementKind.INTERFACE) {
        // 如果元素是接口

    }
}

ElementKind是一个枚举类,它的取值有很多,如下:

PACKAGE //表示包
ENUM //表示枚举
CLASS //表示类
ANNOTATION_TYPE //表示注解
INTERFACE //表示接口
ENUM_CONSTANT //表示枚举常量
FIELD //表示字段
PARAMETER //表示参数
LOCAL_VARIABLE //表示本地变量
EXCEPTION_PARAMETER //表示异常参数
METHOD //表示方法
CONSTRUCTOR //表示构造函数
OTHER //表示其他

关于Element就介绍到这,我们接着往下看

2、重写方法解读
除了必须实现的这个抽象方法,我们还可以重写其他 4 个常用的方法,如下:

public class AptAnnotationProcessor extends AbstractProcessor {
    //...
  
    /** 
     * 节点工具类(类、函数、属性都是节点)
     */
    private Elements mElementUtils;

    /** 
     * 类信息工具类
     */
    private Types mTypeUtils;

    /**
     * 文件生成器
     */
    private Filer mFiler;

    /**
     * 日志信息打印器
     */
    private Messager mMessager;
  
    /**
     * 做一些初始化的工作
     * 
     * @param processingEnvironment 这个参数提供了若干工具类,供编写生成 Java 类时所使用
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }
  
    /**
     * 接收外来传入的参数,最常用的形式就是在 build.gradle 脚本文件里的 javaCompileOptions 的配置
     *
     * @return 属性的 Key 集合
     */
    @Override
    public Set<String> getSupportedOptions() {
        return super.getSupportedOptions();
    }

    /**
     * 当前注解处理器支持的注解集合,如果支持,就会调用 process 方法
     *
     * @return 支持的注解集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }
        
    /**
     * 编译当前注解处理器的 JDK 版本
     *
     * @return JDK 版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

注意:getSupportedAnnotationTypes()、getSupportedSourceVersion()和getSupportedOptions()这三个方法,我们还可以采用注解的方式进行提供:

@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
    //...
}

2)、注解处理器注册

注解处理器声明好了,下一步我们就要注册它,其中注册有两种方式:
1、手动注册
2、自动注册
手动注册比较繁琐固定且容易出错,不推荐使用,这里就不讲了。我们主要看下自动注册
自动注册

1、首先我们要在apt-processor这个 Module 下的 build.gradle 文件导入如下依赖:

implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

注意:这两句必须都要加,否则注册不成功,我之前踩坑了

2、在注解处理器上加上 @AutoService(Processor.class) 即可完成注册

@AutoService(Processor.class)
public class AptAnnotationProcessor extends AbstractProcessor {
    //...
}

3)、注解处理器生成类文件

注册完成之后,我们就可以正式编写生成Java 类文件的代码了,其中生成也有两种方式:
1、常规的写文件方式
2、通过 javapoet 框架来编写
1 的方式比较死板,需要把每一个字母都写上,不推荐使用,这里就不讲了。我们主要看下通过 javapoet 这个框架生成Java 类文件
javapoet 方式

这种方式更加符合面向对象编码的一个风格,对 javapoet还不熟的朋友,可以去github 上学习一波 传送门,这里我们介绍一下它常用的一些类:
TypeSpec:用于生成类、接口、枚举对象的类
MethodSpec:用于生成方法对象的类
ParameterSpec:用于生成参数对象的类
AnnotationSpec:用于生成注解对象的类
FieldSpec:用于配置生成成员变量的类
ClassName:通过包名和类名生成的对象,在JavaPoet中相当于为其指定 Class
ParameterizedTypeName:通过 MainClass和 IncludeClass 生成包含泛型的Class
JavaFile:控制生成的 Java 文件的输出的类
1、导入 javapoet 框架依赖

implementation 'com.squareup:javapoet:1.13.0'

2、按照指定代码模版生成 Java 类文件

例如,我在app的build.gradle下进行了如下配置:

android {
    //...
    defaultConfig {
        //...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [MODULE_NAME: project.getName()]
            }
        }
    }
}

在 MainActivity下面进行了如下注解:

@AptAnnotation(desc = "我是 MainActivity 上面的注解")
public class MainActivity extends AppCompatActivity {

    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAptApi.init();
    }
}@AptAnnotation(desc = "我是 MainActivity 上面的注解")
public class MainActivity extends AppCompatActivity {

    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAptApi.init();
    }
}

我希望生成的代码如下:

public class HelloWorld {
  public void test(String param) {
    System.out.println("模块: apt-app");
    System.out.println("节点: MainActivity  描述: 我是 MainActivity 上面的注解");
    System.out.println("节点: onCreate  描述: 我是 onCreate 上面的注解");
  }
}

现在我们来实操一下:

@AutoService(Processor.class)
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
        
    //文件生成器
    Filer filer;
    //模块名
    private String mModuleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //初始化文件生成器
        filer = processingEnvironment.getFiler();
        //通过 key 获取 build.gradle 中对应的 value
        mModuleName = processingEnv.getOptions().get("MODULE_NAME");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set == null || set.isEmpty()) {
            return false;
        }
                
        //获取当前注解下的节点信息
        Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);

        // 构建 test 函数
        MethodSpec.Builder builder = MethodSpec.methodBuilder("test")
                .addModifiers(Modifier.PUBLIC) // 指定方法修饰符
                .returns(void.class) // 指定返回类型
                .addParameter(String.class, "param"); // 添加参数
        builder.addStatement("$T.out.println($S)", System.class, "模块: " + mModuleName);

        if (rootElements != null && !rootElements.isEmpty()) {
            for (Element element : rootElements) {
                //当前节点名称
                String elementName = element.getSimpleName().toString();
                //当前节点下注解的属性
                String desc = element.getAnnotation(AptAnnotation.class).desc();
                // 构建方法体
                builder.addStatement("$T.out.println($S)", System.class, 
                                     "节点: " + elementName + "  " + "描述: " + desc);
            }
        }
        MethodSpec main =builder.build();

        // 构建 HelloWorld 类
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC) // 指定类修饰符
                .addMethod(main) // 添加方法
                .build();

        // 指定包路径,构建文件体
        JavaFile javaFile = JavaFile.builder("com.dream.aptdemo", helloWorld).build();
        try {
            // 创建文件
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }
}

经过上面这些步骤,我们运行App就能生成上面截图的代码了,现在还差最后一步,对生成的代码进行使用

注意:不同版本的 Gradle 生成的类文件位置可能不一样,我的Gradle 版本是 6.7.1,生成的类文件在如下位置:
在这里插入图片描述一些低版本的 Gradle 生成的类文件在 /build/generated/source这个目录下

apt-api 调用生成代码完成业务功能

这个Module的操作相对来说也比较简单,就是通过反射获取到生成的类,进行相应的封装使用即可,我的编写如下:

public class MyAptApi {

    @SuppressWarnings("all")
    public static void init() {
        try {
            Class c = Class.forName("com.dream.aptdemo.HelloWorld");
            Constructor declaredConstructor = c.getDeclaredConstructor();
            Object o = declaredConstructor.newInstance();
            Method test = c.getDeclaredMethod("test", String.class);
            test.invoke(o, "");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接着我们在 MainActivity 的 oncreate 方法里面进行调用:

@AptAnnotation(desc = "我是 MainActivity 上面的注解")
public class MainActivity extends AppCompatActivity {
  
    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAptApi.init();
    }
}

//打印结果
模块:app
节点: MainActivity 描述: 我是MainActivity上面的注解
节点: onCreate 描述: 我是onCreate 上面的注解
总结

本篇文章讲的一些重点内容:
1、APT 工程所需创建的不同种类的 Module 及Module之间的依赖关系
2、Java源文件实际上是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element
3、采用 auto-service对注解处理器进行自动注册
4、采用 javapoet框架编写所需生成的Java类文件
5、通过反射及适当的封装,将生成的类的功能提供给上层调用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/690270.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Java+Swing实现推箱子游戏

基于JavaSwing实现推箱子游戏 一、系统介绍二、功能展示三、其他系统四、获取源码 一、系统介绍 基于JAVA的推箱子游戏系统主要用于实现游戏推箱子功能&#xff0c;提供多种不同难度的关卡。本系统结构如下&#xff1a; &#xff08;1&#xff09;初始化模块: 该模块包括屏幕初…

如何用 ChatGPT 和你的卡片笔记对话?开源应用 Quivr 尝试

卡片 我 非常喜欢使用卡片笔记。其优点明显&#xff0c;例如能显著减轻写作压力。在你面对空白屏幕时&#xff0c;写一篇文章的压力可能会很大&#xff0c;而随时三言两语记录卡片笔记则显得更为轻松。由于笔记以卡片形式存在&#xff0c;可以进行大量重组复用&#xff0c;对于…

基于SpringBoot+vue的音乐网站与分享平台设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

分布式理论和一致性算法详解

1、什么是分布式系统 分布式系统是一个硬件或软件组成分布在不同的网络计算机上&#xff0c;彼此之间仅仅通过消息传递进行通信和协调的系统 2、分布式系统的特征 分布性 分布式系统的多台计算机都会在空间上随意分布的&#xff0c;同时&#xff0c;机器的分布情况也会随时变动…

【详细分析】thinkphp反序列化漏洞

文章目录 配置xdebug反序列化漏洞利用链详细分析poc1&#xff08;任意文件删除&#xff09;测试pocpoc2&#xff08;任意命令执行&#xff09;poc3&#xff08;任意命令执行&#xff09; 补充代码基础函数trait关键字应用案例优先级多trait 配置xdebug php.ini [Xdebug] zend…

聊聊我在店铺开放域做性能优化的体会

我们新推出大淘宝技术年度特刊《长期主义&#xff0c;往往从一些小事开始——工程师成长总结专题》&#xff0c;专题收录多位工程师真诚的心路历程与经验思考&#xff0c;覆盖终端、服务端、数据算法、技术质量等7大技术领域&#xff0c;欢迎一起沟通交流。 本文为此系列第五篇…

nodejs+mysql+vue+elementUI 实现选择资源管理工具

我们在写后台管理系统时会遇到上传视频、音频等内容。如果上传资源地方多的话,每个地方都要加上传的代码,比较麻烦。而且遇到有些图片是已经上传过的,下次遇到了又要重复上传,浪费服务器或cos资源。 这时候可以实现一个资源管理工具,上传图片到资源管理工具里,然后再去选…

35年前,金山WPS上的当终于找补回来,没想到,钉钉也深度参与

我们当年上了微软的当&#xff01; 近日&#xff0c;雷军在武汉科技大学毕业典礼致辞&#xff0c;登上了热搜。 关于“小米汽车”的话题再次被推上了舆论的风口。这是54岁雷军的最后一搏&#xff0c;距离2024年量产&#xff0c;时间也所剩不多了。 从软件到电商&#xff0c;到…

Flutter控件封装之视频进度条

视频控制器&#xff0c;三方所提供的样式&#xff0c;有时很难满足我们的需求&#xff0c;对于此情况&#xff0c;我们不得不在此基础上自行封装&#xff0c;今天所分享的文章就是一个很简单的控制器封装案例&#xff0c;包含了基本的播放暂停&#xff0c;全屏和退出全屏&#…

在ElementUI中如何通过按钮控制输入框的隐藏与显示

点击此按钮会出现两个输入框 当点击完新增后会出现两个输入框及一个按钮&#xff0c;解决此方案思路比较简单&#xff1a;就是设置一个属性值&#xff0c;用v-if进行绑定&#xff0c;在什么情况下需要显示就需要设置v-if绑定的值等于需要显示的值 <div style"height:7…

Istio 熔断 连接池 故障处理

istio熔断 熔断主要是无感的处理服务异常并保证不会发生级联甚至雪崩的服务异常。在微服务方面体现是对异常的服务情况进行快速失败&#xff0c;它对已经调用失败的服务不再会继续调用&#xff0c;如果仍需要调用此异常服务&#xff0c;它将立刻返回失败。 与此同时&#xff0…

c语言修炼第一章--初识c语言(1.3)

目录 前言&#xff1a; 1.关键字 1.1常见关键字: 1.1.1typedef类型重定义 1.2.static 1.2.1static修饰局部变量 1.2.2static修饰全局变量 1.2.3static修饰函数 …

待办事项工具用哪个?每天都在用的待办工具

随着职场“内卷”程度的加深&#xff0c;越来越多的上班族感到自己每天都会面临各种繁忙的日程安排、琐碎的待办事项&#xff0c;并且工作事项和家庭事项交织在一起&#xff0c;需要各方面都兼顾到&#xff0c;这简直让人恨不得长出三头六臂。在待办的事情比较多时&#xff0c;…

艺考之路,很艰难也很幸福

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 卷圈 运营 / SandLiu 卷圈 监制 / 姝琦 嘉宾 / 小川同学 产品统筹 / bobo 录音间 / 声湃轩北京站 本期节目是拼娃时代的第三十期&#xff0c;我们请来了拼娃时代的老朋友&#xff0c;小川同学。 拼娃时代的听友们肯定…

带有10位数码管显示的语音芯片ic,节省30%MCU硬件资源WTV890-B001

随着科技的不断发展&#xff0c;WTV890-B001多功能语音芯片的推出&#xff0c;为工程师带来了极大的便利和产品体验&#xff0c;这款多功能语音芯片具有许多优势&#xff0c;其中WTV890-B001版本中&#xff0c;最引人注目的特点是它带有10位的数码管显示功能&#xff0c;无需再…

nginx配置https证书(nginx-docker版本)

1、下载证书文件 以下两种多可配置 2、nginx server下加上配置 注意&#xff1a;ssl_certificate 参数可为crt文件、或者pem文件。需要将文件docker cp进容器中&#xff0c; ssl_certificate、ssl_certificate_key 为容器内的文件路径。 listen 443 ssl;server_name 域名地…

TMS FlexCel Studio for net 7.17 cRACK

TTMS FlexCel Studio for net 100% 托管代码 Excel 文件操作引擎以及 Excel 和 PDF 报告生成&#xff0c;适用于 .NET、Xamarin.iOS、Xamarin.Android、Xamarin.Mac​​ 功能概述 使用 FlexCel Studio for .NET 创建可动态快速读写 Excel 文件的应用程序、Web 服务和移动设备应…

深入理解浏览器的缓存机制之协商缓存与强缓存

目录 什么是浏览器缓存 浏览器缓存的分类 &#x1f397;️ 强缓存 &#x1f397;️ 协商缓存 &#x1f449;&#x1f3fb; 缓存请求流程 &#x1f449;&#x1f3fb; 为什么要有Etag &#x1f449;&#x1f3fb; 缓存优先级 &#x1f449;&#x1f3fb; 启发式缓存 &a…

Midjourney教程古风人像类

古风图像的特点&#xff1a; 人物发型多为飘逸的长发&#xff0c;或是精致的盘发&#xff1b; 人物服装多为飘逸的长袍、长裙&#xff1b; 整体画风以水墨、水彩、工笔为c主&#xff0c;线条写意&#xff0c;色彩清新淡雅&#xff1b; 背景中多用花鸟、亭台楼阁、桃林等构建氛…

抖音seo矩阵系统源码|需求文档编译说明(三)

文章目录 批量剪辑工具技术源码框架 短视频SEO矩阵系统源码技术开发&#xff0c;遵循步骤抖音矩阵系统源码搭建功能 开发语言及开发环境 抖音账号矩阵系统源码搭建包括以下步骤 概要 ​​抖音seo源码&#xff0c;抖音矩阵系统如何使用源码二次开发&#xff0c;开发的功能有那些…