APT开发指南

简介: 之前在学习组件化的时候,有一个组件生命周期插件源码让我百思不得其解,究其原因Annotation Processing Tools基础没过关,之前的两篇文章,一篇是ASM一篇是AspectJ,还有关如何自定义Plugin系列,反响还不错,果然理论 + 实践才是王道。现在准备将APT也补上,希望以后也能像大佬一样随意定制化插件。

之前在学习组件化的时候,有一个组件生命周期插件源码让我百思不得其解,究其原因Annotation Processing Tools基础没过关,之前的两篇文章,一篇是ASM一篇是AspectJ,还有关如何自定义Plugin系列,反响还不错,果然理论 + 实践才是王道。现在准备将APT也补上,希望以后也能像大佬一样随意定制化插件。

一.APT概述

APT 是 Annotation Processor Tool 的缩写,是一种处理注解的工具,准确的来说,它是javac 的一个工具,用于在编译时扫描和处理注解。注解处理器以java代码(编译过的代码)作为输入,以生成.java文件为输出。

1681568032739.png

二. Element

为什么要了解Element,自定义注解器,需要继承 AbstractProcessor,对于AbstractProcessor来说,最重要的是 process 方法,process 方法处理的核心就是Element对象,下面我们看一下Element具体接口方法或子接口吧~

1681568069432.png

三. AnnotationProcessor

3.1 什么是Annotation(注解)

元数据,标记可以在编译,类加载,运行时被读取,并执行相应的处理

3.2 Annotation有什么用?

通过使用注解,可以在不改变原有的逻辑下,在源文件中嵌入一些补充信息,代码分析采集可以利用这些补充信息进行验证或者进行部署

3.3 Annotation类型

1681568116419.png

3.4 Annotation Processor 实质原理

Javac编译器编根据注解(Annotation)获取相关数据,解决重复编码的问题

四. 自定义ButterKnife实战

4.1 ButterKnife是什么?

ButterKnife是一个编译时依赖注入框架,主要目的是用来简化android中类似findViewById、setOnclickListener等的template代码。

4.2 ButterKnife功能介绍

1681568724355.png

4.3 用反射实现 ButterKnife

  • Bind class
  • bind(Activity) method
  • 用反射获取Field[],然后获取 Annotation BindView

ButterKnife 是依赖注入吗?

什么是依赖注入: 把依赖的决定权交给外部,即依赖注入

  • ButterKnife: 自己决定的依赖的获取,只是执行过程交给 ButterKnife 所以ButterKnife 只是一个 View Binding 库,而不是依赖注入

4.4 项目结构

1681568826825.png

4.5 实现步骤

第一步: 自定义BindView注解 @MkBindView

在apt-annotation新建一个注解 @MkBindView

1681569048260.png

第二步: 添加模块依赖关系

1681569082850.png

第三步: 创建注解处理器

在apt-processor里定义注解处理器MkBindViewProcessor

1681569120494.png

来看一下MkBindViewProcessor的父类AbstractProcessor实现吧

1681569150681.png@SupportedAnnotationTypes

1681569176993.png@SupportedSourceVersion

1681569203081.png

process

1681569229789.png

第一步

通过roundEnvironment.getElementsAnnotatedWith(MkBindView.class)获得所以含有@MkBindView注解的element集合

        //得到所有的注解
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(MkBindView.class);
第二步

通过 classElement.getQualifiedName() 可以获取类的完整包名和类名

       String fullClassName = classElement.getQualifiedName().toString();

第三步

VariableElement variableElement = (VariableElement) element;
     //获取类信息
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
第四步

将element信息以"包名 + 类名"为key保存在mClassCreatorFactoryMap中

  MkClassCreatorFactory proxy = mClassCreatorFactoryMap.get(fullClassName);
            if (proxy == null) {
                proxy = new MkClassCreatorFactory(mElementUtils, classElement);
                mClassCreatorFactoryMap.put(fullClassName, proxy);
            }
第五步

通过mClassCreatorFactoryMap对应的java文件,其中mClassCreatorFactoryMap是MkClassCreatorFactory的Map集合

    MkBindView bindAnnotation = variableElement.getAnnotation(MkBindView.class);
            //获取 View 的 id
            int id = bindAnnotation.value();
            proxy.putElement(id, variableElement);

MkClassCreatorFactory源码查看

public class MkClassCreatorFactory {
    private String mBindingClassName;
    private String mPackageName;
    private TypeElement mTypeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();
    MkClassCreatorFactory(Elements elementUtils, TypeElement classElement) {
        this.mTypeElement = classElement;
        PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
        String packageName = packageElement.getQualifiedName().toString();
        String className = mTypeElement.getSimpleName().toString();
        this.mPackageName = packageName;
        this.mBindingClassName = className + "_SensorsDataViewBinding";
    }
    public void putElement(int id, VariableElement element) {
        mVariableElementMap.put(id, element);
    }
    /**
     * 创建 Java 代码
     *
     * @return String
     */
    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("/**\n" +
                " * Auto Created by SensorsData APT\n" +
                " */\n");
        builder.append("package ").append(mPackageName).append(";\n");
        builder.append('\n');
        builder.append("public class ").append(mBindingClassName);
        builder.append(" {\n");
        generateBindViewMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }
    /**
     * 加入Method
     *
     * @param builder StringBuilder
     */
    private void generateBindViewMethods(StringBuilder builder) {
        builder.append("\tpublic void bindView(");
        builder.append(mTypeElement.getQualifiedName());
        builder.append(" owner ) {\n");
        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String viewName = element.getSimpleName().toString();
            String viewType = element.asType().toString();
            builder.append("\t\towner.");
            builder.append(viewName);
            builder.append(" = ");
            builder.append("(");
            builder.append(viewType);
            builder.append(")(((android.app.Activity)owner).findViewById( ");
            builder.append(id);
            builder.append("));\n");
        }
        builder.append("  }\n");
    }
    /**
     * 使用 javapoet 创建 Java 代码
     * javapoet
     *
     * @return TypeSpec
     */
    public TypeSpec generateJavaCodeWithJavapoet() {
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethodsWithJavapoet())
                .build();
        return bindingClass;
    }
    /**
     * 使用 javapoet 创建 Method
     *
     * @return MethodSpec
     */
    private MethodSpec generateMethodsWithJavapoet() {
        ClassName owner = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bindView")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(owner, "owner");
        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String viewName = element.getSimpleName().toString();
            String viewType = element.asType().toString();
            methodBuilder.addCode("owner." + viewName + " = " + "(" + viewType + ")(((android.app.Activity)owner).findViewById( " + id + "));");
        }
        return methodBuilder.build();
    }
    public String getPackageName() {
        return mPackageName;
    }
    public String getProxyClassFullName() {
        return mPackageName + "." + mBindingClassName;
    }
    public TypeElement getTypeElement() {
        return mTypeElement;
    }
}

以上代码主要是从Elements, TypeElement 中获得 一些想要的信息,如: packName,Activity 名,变量类型,id等,通过StringBuilder拼接代码,每个对象分别代表一个对应的.java文件

第四步: 编写 Bind View 业务代码
  • 新增bindView方法 在上面的 MkBindViewProcessor 我们动态创建了 xxxActivity_MKViewBinding.java文件,在我们的反射里面我们需要调用bindView完成View的绑定
public class MkButterKnife {
    public static void bindView(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class<?> bindViewClass = Class.forName(clazz.getName() + "_MkViewBinding");
            Method method = bindViewClass.getMethod("bindView", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
  • 添加apt-annotation 注解依赖
dependencies { 
    implementation project(':apt-annotation')
}
第五步: 在应用程序使用注解处理器

配置完整的配置脚本内容

dependencies {
 
    implementation project(':apt-annotation')
    implementation project(':apt-sdk')
    annotationProcessor project(':apt-processor')
}

注解处理器依赖的模块是 annotationProcessor,最后在Activity里使用 @MkBindView

public class MainActivity extends AppCompatActivity {
    @MkBindView(R.id.button)
    AppCompatButton button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MkButterKnife.bindView(this);
        button.setText("New Text");
    }
}

1681569468288.png

第六步: 利用 javapoet 将 .Bind 文件生成 .class 文件
6.1 添加依赖关系
dependencies {
    implementation project(':apt-annotation')
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.10.0'
}

6.2 添加构建生成java代码

1681569517112.png

6.3 生成java文件

       // 使用 javapoet 创建java文件
        for (String key : mClassCreatorFactoryMap.keySet()) {
            MkClassCreatorFactory proxyInfo = mClassCreatorFactoryMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCodeWithJavapoet()).build();
            try {
                // 生成文件
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

总结

APT的核心用一句话总结就是:

理解Annotation Processing原理: 编译过程中读源码,然后生成代码,再编译

本文以ButterKnife为例,通过Processor自动生成findViewById模板代码,然后通过 square公司的 javapoet 插件将 Binding.java 文件转换成 java代码,APT应用范围非常广泛,尤其是组件化开发领域,所以小伙伴们务必掌握。

本文相关代码已经上传MkButterKnife,有帮助的话Star一波吧。



相关文章
|
JSON 安全 关系型数据库
MySQL 7.0 功能特性
MySQL 是一款广泛应用于各种 Web 应用程序和企业级系统的关系型数据库管理系统。MySQL 7.0 是 MySQL 数据库的一个重要版本,引入了许多令人兴奋的功能特性,提升了性能、安全性和可用性。本篇博客将介绍 MySQL 7.0 的一些主要功能特性。
428 0
|
芯片
STM32F103标准外设库——中断应用/事件控制器(七)
STM32F103标准外设库——中断应用/事件控制器(七)
1167 0
STM32F103标准外设库——中断应用/事件控制器(七)
|
4月前
|
Kubernetes 安全 Devops
「迁移急救包」全云平台无缝迁移云效实操手册
阿里云云效是国内领先的一站式DevOps平台,提供代码全生命周期管理、智能化交付流水线及精细化研发管控,支持多种开发场景。本文详细介绍了从其他平台(如Coding)向云效迁移的完整方案,包括代码仓库、流水线、制品仓库及项目数据的迁移步骤,帮助用户实现高效、安全的平滑迁移,提升研发效率与协作能力。
586 29
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
10月前
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
1324 7
|
关系型数据库 MySQL Java
MySQL索引优化与Java应用实践
【11月更文挑战第25天】在大数据量和高并发的业务场景下,MySQL数据库的索引优化是提升查询性能的关键。本文将深入探讨MySQL索引的多种类型、优化策略及其在Java应用中的实践,通过历史背景、业务场景、底层原理的介绍,并结合Java示例代码,帮助Java架构师更好地理解并应用这些技术。
337 2
|
Kubernetes Cloud Native Linux
CKAD考试实操指南(一)--- 登顶CKAD:征服考试的完美蓝图
Certified Kubernetes Application Developer (CKAD)是由Linux Foundation和Cloud Native Computing Foundation (CNCF)联合推出的一项专业认证,旨在验证候选人是否具备Kubernetes应用开发者所需的技能、知识和能力。CKAD考试是一个在线的、监考的、基于性能的考试,由一系列需要在命令行中解决的性能型任务(问题)组成,考试时间为2小时。
1602 0
CKAD考试实操指南(一)--- 登顶CKAD:征服考试的完美蓝图
|
Java Maven
解决Maven中CANNOT Resolve XXX错误
解决Maven中CANNOT Resolve XXX错误
1166 0
|
JavaScript 前端开发 程序员
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
260 0
|
机器学习/深度学习 并行计算 算法框架/工具
Anaconda+Cuda+Cudnn+Pytorch(GPU版)+Pycharm+Win11深度学习环境配置
Anaconda+Cuda+Cudnn+Pytorch(GPU版)+Pycharm+Win11深度学习环境配置