4053 字
20 分钟
0x00 概述

注解系列

注解基础

APT

JavaPoet

0x00 概述#

前一篇介绍了注解的基本知识以及常见用法,由于运行期(RunTime)利用反射去获取信息还是比较损耗性能的,本篇将介绍一种使用注解更加优雅的方式,编译期(Compile time)注解,以及处理编译期注解的手段APT和Javapoet,限于篇幅,本篇着重介绍APT 首先你的注解需要声明为CLASS @Retention(RetentionPolicy.CLASS)

编译期解析注解基本原理: 在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是ButterKnife等开源库的基本原理。

0x01 APT#

在处理编译器注解的第一个手段就是APT(Annotation Processor Tool),即注解处理器。在java5的时候已经存在,但是java6开始的时候才有可用的API,最近才随着butterknife这些库流行起来。本章将阐述什么是注解处理器,以及如何使用这个强大的工具。

什么是APT#

APT是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解,一个注解的注解处理器,以java代码(或者编译过的字节码)作为输入,生成.java文件作为输出,核心是交给自己定义的处理器去处理,

如何使用#

每个自定义的处理器都要继承虚处理器,实现其关键的几个方法

继承虚处理器 AbstractProcessor#

public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}

下面重点介绍下这几个函数:

  1. init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer
  2. process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。这是一个布尔值,表明注解是否已经被处理器处理完成,官方原文whether or not the set of annotations are claimed by this processor,通常在处理出现异常直接返回false、处理完成返回true。

image.png

  1. getSupportedAnnotationTypes(): 必须要实现;用来表示这个注解处理器是注册给哪个注解的。返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
  2. getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported(),你也可以使用SourceVersion_RELEASE_6、7、8

注册 处理器#

由于处理器是javac的工具,因此我们必须将我们自己的处理器注册到javac中,在以前我们需要提供一个.jar文件,打包你的注解处理器到此文件中,并在在你的jar中,需要打包一个特定的文件 javax.annotation.processing.Processor到META-INF/services路径下 把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器。

超级麻烦有木有,不过不要慌,谷歌baba给我们开发了AutoService注解,你只需要引入这个依赖,然后在你的解释器第一行加上

@AutoService(Processor.class)

然后就可以自动生成META-INF/services/javax.annotation.processing.Processor文件的。省去了打jar包这些繁琐的步骤。

Elements#

在前面的init()中我们可以获取如下引用

  • Elements:一个用来处理Element的工具类
  • Types:一个用来处理TypeMirror的工具类
  • Filer:正如这个名字所示,使用Filer你可以创建文件(通常与javapoet结合)

在注解处理过程中,我们扫面所有的Java源文件。

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
...
}
return false;
}

源文件的每一个部分都是一个特定类型的Element,先来看一下Element

对于编译器来说 代码中的元素结构是基本不变的,如,组成代码的基本元素包括包、类、函数、字段、变量的等,JDK为这些元素定义了一个基类也就是**Element**

Element在Java中以接口形式存在

定义如下: 详细请查阅 Element详细API

**
* 表示一个程序元素,比如包、类或者方法,有如下几种子接口:
* ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 ;
* PackageElement:表示一个包程序元素;
* TypeElement:表示一个类或接口程序元素;
* TypeParameterElement:表示一般类、接口、方法或构造方法元素的形式类型参数;
* VariableElement:表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
*/
public interface Element extends AnnotatedConstruct {
/**
* 返回此元素定义的类型
* 例如,对于一般类元素 C<N extends Number>,返回参数化类型 C<N>
*/
TypeMirror asType();
/**
* 返回此元素的种类:包、类、接口、方法、字段...,如下枚举值
* PACKAGE, ENUM, CLASS, ANNOTATION_TYPE, INTERFACE, ENUM_CONSTANT, FIELD, PARAMETER, LOCAL_VARIABLE, EXCEPTION_PARAMETER,
* METHOD, CONSTRUCTOR, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, OTHER, RESOURCE_VARIABLE;
*/
ElementKind getKind();
/**
* 返回此元素的修饰符,如下枚举值
* PUBLIC, PROTECTED, PRIVATE, ABSTRACT, DEFAULT, STATIC, FINAL,
* TRANSIENT, VOLATILE, SYNCHRONIZED, NATIVE, STRICTFP;
*/
Set<Modifier> getModifiers();
/**
* 返回此元素的简单名称,例如
* 类型元素 java.util.Set<E> 的简单名称是 "Set";
* 如果此元素表示一个未指定的包,则返回一个空名称;
* 如果它表示一个构造方法,则返回名称 "<init>";
* 如果它表示一个静态初始化程序,则返回名称 "<clinit>";
* 如果它表示一个匿名类或者实例初始化程序,则返回一个空名称
*/
Name getSimpleName();
/**
* 返回封装此元素的最里层元素。
* 如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素;
* 如果此元素是顶层类型,则返回它的包;
* 如果此元素是一个包,则返回 null;
* 如果此元素是一个泛型参数,则返回 null.
*/
Element getEnclosingElement();
/**
* 返回此元素直接封装的子元素
*/
List<? extends Element> getEnclosedElements();
boolean equals(Object var1);
int hashCode();
/**
* 返回直接存在于此元素上的注解
* 要获得继承的注解,可使用 getAllAnnotationMirrors
*/
List<? extends AnnotationMirror> getAnnotationMirrors();
/**
* 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的
*/
<A extends Annotation> A getAnnotation(Class<A> annotationType);
//接受访问者的访问 (??)
<R, P> R accept(ElementVisitor<R, P> var1, P var2);
}

因此,代码在APT眼中只是一个结构化的文本而已。Element代表的是源代码。TypeElement代表的是源代码中的类型元素,例如类。

Element有五个直接子类,分别代表一种特定类型

PackageElement表示一个包程序元素,可以获取到包名等
TypeParameterElement表示一般类、接口、方法或构造方法元素的泛型参数
TypeElement表示一个类或接口程序元素
VariableElement表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
ExecutableElement表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素

开发中Element可根据实际情况强转为以上5种中的一种,它们都带有各自独有的方法,如下所示

package com.example; // PackageElement
public class Test { // TypeElement
private int a; // VariableElement
private Test other; // VariableElement
public Test () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}

TypeElement 核心api#

public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
List<? extends Element> getEnclosedElements();
NestingKind getNestingKind();
Name getQualifiedName();
Name getSimpleName();
TypeMirror getSuperclass();
List<? extends TypeMirror> getInterfaces();
List<? extends TypeParameterElement> getTypeParameters();
Element getEnclosingElement();
}
方法注释
getNestingKind返回此类型元素的嵌套种类
getQualifiedName返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称.
getSuperclass返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType
getInterfaces返回直接由此类实现或直接由此接口扩展的接口类型
getTypeParameters按照声明顺序返回此类型元素的形式类型参数

VariableElement 核心api#

public interface VariableElement extends Element {
Object getConstantValue();
Name getSimpleName();
Element getEnclosingElement();
}

这里VariableElement除了拥有Element的方法以外还有以下两个方法

方法解释
getConstantValue变量初始化的值
getEnclosingElement获取相关类信息

TypeMirror#

然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的 TypeMirror 用于描述Java程序中元素的信息,即Element 的元信息。通过Element.asType()接口可以获取Element的TypeMirror

PrimitiveType原始数据类型,boolean,byte,short int,long,float,char,double
ReferenceType引用类型
ArrayType数组类型
DeclaredType声明的类型,例如类、接口、枚举、注解类型
AnnotationType注解类型
ClassType类类型
EnumType枚举类型
InterfaceType接口类型
TypeVariable类型变量类型
VoidTypevoid 类型
WildcardType通配符类型

其中 当TypeMirror是DeclaredType或者TypeVariable时,TypeMirror可以转化成Element:

Element element = processingEviroment.getTypeUtils().asElement(typeMirror);

通过asType获取需要知晓类名全路径

实践#

再举个栗子🌰:

当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。 那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值。 如何获取?

for (Element element : roundEnv.getElementsAnnotatedWith(OnceClick.class)) {
//对于Element直接强转
ExecutableElement executableElement = (ExecutableElement) element;
//非对应的Element,通过getEnclosingElement转换获取
TypeElement classElement = (TypeElement) element
.getEnclosingElement();
//当(ExecutableElement) element成立时,使用(PackageElement) element
// .getEnclosingElement();将报错。
//需要使用elementUtils来获取
Elements elementUtils = processingEnv.getElementUtils();
PackageElement packageElement = elementUtils.getPackageOf(classElement);
//全类名
String fullClassName = classElement.getQualifiedName().toString();
//类名
String className = classElement.getSimpleName().toString();
//包名
String packageName = packageElement.getQualifiedName().toString();
//方法名
String methodName = executableElement.getSimpleName().toString();
//取得方法参数列表
List<? extends VariableElement> methodParameters = executableElement.getParameters();
//参数类型列表
List<String> types = new ArrayList<>();
for (VariableElement variableElement : methodParameters) {
TypeMirror methodParameterType = variableElement.asType();
if (methodParameterType instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) methodParameterType;
methodParameterType = typeVariable.getUpperBound();
}
//参数名
String parameterName = variableElement.getSimpleName().toString();
//参数类型
String parameteKind = methodParameterType.toString();
types.add(methodParameterType.toString());
}
}

同理FILED、 但是当你有一个注解是以@Target(ElementType.TYPE)定义时,表示该注解只能修饰类、接口、枚举。 那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、全类名、父类。 如何获取:

for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) {
//ElementType.TYPE注解可以直接强转TypeElement
TypeElement classElement = (TypeElement) element;
PackageElement packageElement = (PackageElement) element
.getEnclosingElement();
//全类名
String fullClassName = classElement.getQualifiedName().toString();
//类名
String className = classElement.getSimpleName().toString();
//包名
String packageName = packageElement.getQualifiedName().toString();
//父类名
String superClassName = classElement.getSuperclass().toString();
}

0x02 辅助接口#

在自定义注解器的初始化时候,可以获取以下4个辅助接口

public class MyProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
}

Filer#

一般配合JavaPoet来生成需要的java文件(下一篇将详细介绍javaPoet)

Messager#

Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的。在官方文档中描述了消息的不同级别中非常重要的是Kind.ERROR,因为这种类型的信息用来表示我们的注解处理器处理失败了。很有可能是第三方开发者错误的使用了注解。这个概念和传统的Java应用有点不一样,在传统Java应用中我们可能就抛出一个异常Exception。如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),使用我们注解处理器第三方开发者将会从javac中得到非常难懂的出错信息,因为它包含注解处理器的堆栈跟踪(Stacktace)信息。因此,注解处理器就有一个Messager类,它能够打印非常优美的错误信息。除此之外,你还可以连接到出错的元素。在像现在的IDE(集成开发环境)中,第三方开发者可以直接点击错误信息,IDE将会直接跳转到第三方开发者项目的出错的源文件的相应的行。

通常我们封装一个Messager的日志类在Processor关键位置记录当前的状态,也方便调试

  • Types

Types是一个用来处理TypeMirror的工具

  • Elements

Elements是一个用来处理Element的工具

0x03 优缺点#

优点(结合javapoet)

  • 对代码进行标记、在编译时收集信息并做处理
  • 生成一套独立代码,辅助代码运行

缺点

  • 可以自动生成代码,但在运行时需要主动调用
  • 如果要生成代码需要编写模板函数

0x04 其他#

  1. 通常我们需要分离处理器和注解 这样做的原因是,在发布程序时注解及生成的代码会被打包到用户程序中,而注解处理器则不会(注解处理器是在编译期在JVM上运行跟运行时无关)。要是不分离的话,假如注解处理器中使用到了其他第三方库,那就会占用系统资源,特别是方法数,
  2. 该技术可以让我们在设计自己框架时候多了一种技术选择,更加的优雅
  3. 反射优化

运行时注解的使用可以减少很多代码的编写,但是谁都知道这是有性能损耗的,不过权衡利弊,我们选择了妥协,这个技术手段可以处理这个问题

0x05 debug APT#

AS 中 debug Java 源代码很简单,但是如果你想调试 APT 代码(AbstractProcessor),就必须做一些配置了。 第一步:配置 gradle.properties 文件 首先找到本地电脑的 gradle home,它一般在当前用户的目录下,比如我的 Mac 电脑位置在: ~/用户名/.gradle。 打开(如果没有,新建一个)gradle.properties 文件,增加下面两行:

org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

第二步:增加 Remote 编译选项 1、找到:Select Run/Debug Configration -> Edit Configrations…,如下图 2、左侧面板,点击“+”,选择 Remote,如下图

3、随便填入一个名字,比如 APT,点击 Apply、Ok,使用默认配置完成设置

4、切换到 APT,点击 Debug 按钮,建立到本地5005端口的连接

成功之后,会有如下提示

这样,我们已经可以开始调试 APT 代码了。 第三步:开始调试 设置好断点,然后选择 AS 菜单栏:Build -> Rebuild Project,

然后,开始调试你的 APT 代码吧~

因为 AS 启动一个项目的时候,默认也会去占用上面配置的 5005 端口。所以,你如果你发现 AS 提示你端口被占用,请先杀掉本地占用 5005 端口的进程。 另外,这一步使用 Rebuild Project 也是我尝试多次后得出最靠谱的方案。网上有说 gradle clean assembleDebug 命令也可以开启 debug APT,我这里发现不太稳定,有时候不可以,不知道原因出在哪里,知道的同学麻烦告知下~

0x06 参考文献#

0x00 概述
作者
强人自传
发布于
2024-02-21
许可协议
CC BY-NC-SA 4.0