【Java 基础】Java SPI 二 之 Java APT原理及APT实战 - 一步步教你写ButterKnife

简介: Java APT 是 Java 技术设计的一个 APT 架构,APT(Annotation Processing Tool)即注解处理器,它是一种处理注解的工具,也是javac中的一个工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。APT可以用来在编译时扫描和处理注解, 它可以用来获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。 在Android中有如ButterKnife、Dagger、EventBus等第三方框架,都采用了APT。

一、定义

Java APT 是 Java 技术设计的一个 APT 架构,

APT(Annotation Processing Tool)即注解处理器,它是一种处理注解的工具,也是javac中的一个工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。

APT可以用来在编译时扫描和处理注解, 它可以用来获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。 在Android中有如ButterKnife、Dagger、EventBus等第三方框架,都采用了APT。

常用的使用方式是这样的:

APT + 注解处理器(AbstractProcess)+ 代码处理(javaPoet)+ 处理器注册(AutoService)

这里我们了解下相关概念:

APT

是一个工具,可以用于检查源代码中的注解,并生成相应的代码。

AbstractProcess:

是 Java 中的一个抽象类,用于定义一个处理程序或者一个抽象的执行环境。在 Java 的编程过程中,经常需要在类的开头或者方法中加入注解,来标记这个方法或者类是抽象的,或者是实现了某个接口。这时候,AbstractProcess 类就可以用来定义注解处理器,用来处理这些注解。

APT框架中的核心类是AbstractProcessor,它定义了如何在代码中处理注解。当APT工具扫描文件时,它会调用实现AbstractProcessor类的一个或多个类的process()方法,以捕获和处理注解。

二、APT工作原理

Java Annotations Processing Tool(APT)是一个预处理器,可以在Java代码编译期间读取注解,并生成相关的代码。

它的工作原理如下:

    1. Java编译器会将源文件传递给APT进行处理;
    2. APT会扫描源文件中所有的注解,并找到对应的处理器;
    3. 处理器会对注解进行处理,并生成新的Java代码文件(或其他文件);
    4. 生成的Java代码文件被编译成字节码文件;
    5. 编译器将生成的字节码文件和原始Java代码文件一起打包成jar包或class文件。

    APT通过Java标准类库中的javax.annotation.processing包提供注解处理的框架。注解处理器必须实现该包中的特定接口,这些接口定义了APT框架的核心功能。通过实现接口,注解处理器能够直接访问来自编译器的数据,以及用于注解处理的元数据信息。注解处理器总是运行在 Java 编译环境中。

    APT的使用可以帮助简化一些重复、冗杂的代码生成工作。

    三、APT实战1(运行时注解) - 一步步教你写ButterKnife

    使用运行时注解的方式实战简单APT,这个比较简单,就是用反射的方式来实现,

    void injectLayout(Context context) {
            // 1. 获取当前class
            Class<?> clazz = context.getClass();
            // 2. 根据class获取class上面的注解
            InjectContenttLayout annotation = clazz.getAnnotation(InjectContenttLayout.class);
            // 3. 获取注解中布局文件的id的值
            int layoutId = annotation.value();
            try {
                // 4. 获取activity中的setContentView方法
                Method method = clazz.getMethod("setContentView", int.class);
                // 5. 执行setContentView方法,传入layoutId参数
                method.invoke(context, layoutId);
            } catch (Exception e) {
            }
        }

    image.gif

    四、APT实战2(编译时注解) - 一步步教你写ButterKnife

    使用编译时注解的方式实战简单APT,手写ButterKnife框架,我们来写一下布局文件view的注入,比如我们不想写烦人的findViewById方法,直接用个注解来搞定,

    本 Demo 下载

    其核心思想是java的ioc(inversion of control),也叫di(dependency injection,依赖注入),是一种面向对象编程中的设计模式。下面我们开始

    4.1 创建一个项目,如下图

    image.gif编辑

    4.2 新建自定义注解

    创建一个Java Library Module名称叫 apt-annotation

    在这个module中创建自定义注解 @BindView,如下图

    image.gif编辑

    4.3 实现APT Compiler处理注解

    创建一个Java Library Module名称叫 apt-compiler-processor,并添加注解module依赖

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

    image.gif

    这个module的作用主要是用来处理注解,并生成java帮助类文件,拆解步骤为

    1、扫描所有被注解标记的Element,获得注解标记的element

    2、遍历Element,并根据不同的页面进行分类

    3、按规则进行字符串拼接,用于拼接生成帮助类代码

    4、使用JavaFileObject写入文件生成java代码

    (输出的文件在build->generated-ap_generated_sources->debug->out->包名目录下)

    如下图所示

    image.gif编辑

    该module在处理注解时,必须继承AbstractProcessor抽象类,入口类为process(Set set, RoundEnvironment roundEnv)

    /**
         * 注解处理方法,
         *
         * @param set      注解的类型集合
         * @param roundEnv 运行环境
         * @return
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
            System.out.println("start process");
            if (set != null && set.size() != 0) {
                //1、 扫描所有被注解标记的Element,获得被BindView注解标记的element
                Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
                categories(elements);
                for (TypeElement typeElement : mToBindMap.keySet()) {
                    // 获取帮助类所有代码
                    String code = generateCode(typeElement);
                    // 构建要生成的帮助类的类名
                    String helperClassName = typeElement.getQualifiedName() + "_ButterKnifeTest"; //
                    System.out.println("start process 帮助类的类名= " + helperClassName);
                    // 输出帮助类的java文件,
                    // 在本例中就是MainActivity_ButterKnifeTest.java文件
                    // 输出的文件在build->generated-ap_generated_sources->debug->out->包名目录下
                    try {
                        System.out.println("生成帮助类 ");
                        JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
                        Writer writer = jfo.openWriter();
                        writer.write(code);
                        writer.flush();
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return true;
            }
            return false;
        }

    image.gif

    当代码写完后,就需要注册APT(如上图所示)

    注册一个APT需要以下步骤:

      1. 在main 目录下新建 resources 资源文件夹;
      2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
      3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
      4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

      4.4 以上步骤全部完成后, 还需要对外提供API(当然也可以不拆分)

      创建一个Android Library Module,名称叫apt-api,并添加依赖

      dependencies {
          ...
          api project(':apt-annotation')
      }

      image.gif

      image.gif编辑

      实现很简单,就是通过反射去调用APT生成的帮助类的方法去实现View的自动绑定,部分代码如下:

      public void inject(Object target) {
              String className = target.getClass().getCanonicalName();
              String helperName = className + "_ButterKnifeTest";
              System.out.println("ButterKnifeTest inject" + helperName);
              try {
                  IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance());
                  helper.inject(target);
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }

      image.gif

      最后就是使用了,在app module里添加依赖

      dependencies {
          ...
          annotationProcessor project(':apt-compiler-processor')
          implementation project(':apt-api')
      }

      image.gif

      4.5 使用如下:

      public class MainActivity extends AppCompatActivity {
          @BindView(value = R.id.test_textview)
          public TextView testTextView;
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              ButterKnifeTest.getInstance().inject(this);
              testTextView.setText("手写 butterknife demo");
          }
      }

      image.gif

      运行代码截图

      image.gif编辑

      五、痛点及优化

      我们可以看到,按官方文档一步步来写apt比较繁琐,

        • 生成代码时需要字符串拼接,代码量多的时候容易出错,排查困难
        • 需要继承AbstrctProcessor并重写多个方法,写入注解容易遗漏
        • 注册APT的步骤繁琐,需手动创建文件

        针对以上问题:

        1、我们可以使用JavaPoet来替代拼接字符串( JavaPoet是一个用来生成Java代码的框架,对JavaPoet不了解的请自行学习)

        官网地址GitHub - square/javapoet: A Java API for generating .java source files.

        2、使用Auto-Service来自动注册APT

        这是谷歌官方出品的一个开源库,可以省去注册APT的步骤,只需要一行注释

        先在apt-compiler模块中添加依赖

        dependencies {
            ...
            implementation 'com.google.auto.service:auto-service:1.0-rc2'
        }

        image.gif

        然后添加注释即可,如下图所示:

        image.gif编辑

        六、一些疑问

        5.1 手写注解处理器时,注解处理器processor为什么要在META-INFO注册?

        META-INFO相当于一个信息包,用于存放一些meta information相关的信息,用来配置应用程序、扩展程序、类加载器和服务manifest.mf文件,在编译时,java编译器回去该文件中查找实现了AbstractProcess的子类,就相当于注册。

        5.2  APT(Annotation Processing Tool)如何调用AbstractProcess的呢?(注解处理器是如何被系统调用的?)

        annotationProcessor 指定apt处理器。

        1、创建一个类并继承自 AbstractProcessor

        2、通过注解 @AutoService(Processor.class) 将创建的新类注册为 APT 处理器。

        也可以手动创建,详细见下面demo说明

        3、在 build.gradle 文件或项目的构建文件中指定 APT 处理器。

        annotationProcessor project(':apt-compiler-processor')
        image.gif

        4、运行项目来激活 APT 处理器并让其开始处理。

           在 Eclipse 中你可以设置 Java Compiler > Annotation Processing 选项卡中的 Enable annotation processing 复选框来启用它,

           在 Intellij IDEA 中,你需要选择菜单中的 "Build" > "Rebuild project" 来刷新生成器任务列表。完成这些步骤后,就可以开始使用 AbstractProcessor 创建自己的注解处理器。

           在安卓中,直接构建项目即可。

        5.3 安卓中,APT项目会不会增加apk的体积?

        不会,processor的作用是在编译器解析注解、生成文件等,只在编译器用到,是不会打包进apk的。

        更高级的用法可自行阅读开源项目,向大佬学习~

        Demo 下载 :https://download.csdn.net/download/fumeidonga/87767415


        相关文章
        |
        19天前
        |
        存储 Java 关系型数据库
        高效连接之道:Java连接池原理与最佳实践
        在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
        38 5
        |
        26天前
        |
        存储 Java 开发者
        Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
        【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
        57 2
        |
        9天前
        |
        存储 算法 Java
        大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
        本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
        大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
        |
        9天前
        |
        Java
        Java之CountDownLatch原理浅析
        本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
        Java之CountDownLatch原理浅析
        |
        11天前
        |
        Java 索引 容器
        Java ArrayList扩容的原理
        Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
        Java ArrayList扩容的原理
        |
        11天前
        |
        Java 大数据 API
        14天Java基础学习——第1天:Java入门和环境搭建
        本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
        |
        17天前
        |
        存储 Java 关系型数据库
        在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
        在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
        34 2
        |
        20天前
        |
        Java 数据格式 索引
        使用 Java 字节码工具检查类文件完整性的原理是什么
        Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
        |
        17天前
        |
        算法 Java 数据库连接
        Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
        本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
        31 1
        |
        23天前
        |
        存储 安全 Java
        深入理解Java中的FutureTask:用法和原理
        【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。