转载请注明出处:https://lizhaoxuan.github.io
概述
本章内容主要对APT一些语法进行简单讲解。apt的学习资料真的太少了,我的学习方法基本上只能通过看开源库的源码猜、看源码注释猜、自己运行着猜……
这里对猜对的结果进行一个总结,让后来者可以更快的上手。
第一次写这种类型的博客,总结的可能有些分散,建议结合开源库源码学习。
自定义注解相关
定义注解格式: public @interface 注解名 {定义体}
Annotation里面的参数该设定:
第一,只能用public或默认(default)这两个访问权修饰.例如,String value();不能是private;
第二,参数只能使用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数类型就为String;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface GetMsg {
int id(); //注解参数
String name() default "default";
}
//使用
@GetMsg(id = 1,name = "asd")
class Test{
}
如果只有一个参数,建议设置为value
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Println {
int value();
}
//使用
@Println(1)
class Test{
}
参数为value时,可以直接写入参数,使用时不在需要key=value写法。
但当有多个参数时,不可以再使用value。
@Retention
这个在第一章有讲。申明该注解属于什么类型注解
@Retention(RetentionPolicy.SOURCE)
源码时注解,一般用来作为编译器标记。就比如Override, Deprecated, SuppressWarnings这样的注解。(这个我们一般都很少自定义的)
@Retention(RetentionPolicy.RUNTIME)
运行时注解,一般在运行时通过反射去识别的注解。
@Retention(RetentionPolicy.CLASS)
编译时注解,在编译时处理。
@Target(ElementType.TYPE)
表示该注解用来修饰哪些元素。并可以修饰多个
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.LOCAL_VARIABLE,ElementType.METHOD})
public @interface GetMsg {
int id();
String name() default "default";
}
例如 GetMsg只能用在局部变量和方法上,如果修饰到类上编译器会报错。
@GetMsg(1)
public void printError(){
//TODO ~
}
@GetMsg(1) //编译器会报错
class Test{
//TODO ~
}
@Target(ElementType.TYPE)
接口、类、枚举、注解
@Target(ElementType.FIELD)
字段、枚举的常量
@Target(ElementType.METHOD)
方法
@Target(ElementType.PARAMETER)
方法参数
@Target(ElementType.CONSTRUCTOR)
构造函数
@Target(ElementType.LOCAL_VARIABLE)
局部变量
@Target(ElementType.ANNOTATION_TYPE)
注解
@Target(ElementType.package)
包
@Inherited
该注解的字面意识是继承,但你要知道注解是不可以继承的。
@Inherited是在继承结构中使用的注解。
如果你的注解是这样定义的:
@Inherited
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Test {
//...
}
当你的注解定义到类A上,此时,有个B类继承A,且没使用该注解。但是扫描的时候,会把A类设置的注解,扫描到B类上。
这里感谢 豪哥 @刘志豪 的排疑解惑~
注解的默认值
注解可以设置默认值,有默认值的参数可以不写。
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface GetMsg {
int id(); //注解参数
String name() default "default";
}
//使用
@GetMsg(id = 1) //name有默认值可以不写
class Test{
}
“注解的继承”(依赖倒置?)
这里讲的继承并不是通过@Inherited修饰的注解。
这个“继承”是一个注解的使用技巧,使用上的感觉类似于依赖倒置,来自于ButterKnife源码。
先看代码。
@Target(METHOD)
@Retention(CLASS)
@ListenerClass(
targetType = "android.view.View",
setter = "setOnClickListener",
type = "butterknife.internal.DebouncingOnClickListener",
method = @ListenerMethod(
name = "doClick",
parameters = "android.view.View"
)
)
public @interface OnClick {
/** View IDs to which the method will be bound. */
int[] value() default { View.NO_ID };
}
这是ButterKnife的OnClick 注解。特殊的地方在于@OnClick修饰了注解@ListenerClass,并且设置了一些只属于@OnClick的属性。
那这样的作用是什么呢?
凡是修饰了@OnClick的地方,也就自动修饰了@ListenerClass。类似于@OnClick是@ListenerClass的子类。而ButterKnife有很多的监听注解@OnItemClick、@OnLongClick等等。
这样在做代码生成时,不需要再单独考虑每一个监听注解,只需要处理@ListenerClass就OK。
处理器类Processor编写
自定义注解后,需要编写Processor类处理注解。Processor继承自AbstractProcessor的类。
AbstractProcessor有两个重要的方法需要重写。
重写getSupportedAnnotationTypes方法:
通过重写该方法,告知Processor哪些注解需要处理。
返回一个Set集合,集合内容为自定义注解的包名+类名。
建议项目中这样编写:
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
//需要全类名
types.add(GetMsg.class.getCanonicalName());
types.add(Println.class.getCanonicalName());
return types;
}
另外如果注解数量很少的话,可以通过另一种方式实现:
//在只有一到两个注解需要处理时,可以这样编写:
@SupportedAnnotationTypes("com.example.annotation.SetContentView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ContentViewProcessor extends AbstractProcessor {
}
重写process方法:
所有的注解处理都是从这个方法开始的,你可以理解为,当APT找到所有需要处理的注解后,会回调这个方法,你可以通过这个方法的参数,拿到你所需要的信息。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
先简单解释下这个方法的参数和返回值。
参数 Set<? extends TypeElement> annotations :将返回所有的由该Processor处理,并待处理的 Annotations。(属于该Processor处理的注解,但并未被使用,不存在与这个集合里)
参数 RoundEnvironment roundEnv :表示当前或是之前的运行环境,可以通过该对象查找找到的注解。
例:
for (Element element : env.getElementsAnnotatedWith(GetMsg.class)) {
//所有被使用的@GetMsg
}
返回值 表示这组 annotations 是否被这个 Processor 接受,如果接受(true)后续子的 pocessor 不会再对这个 Annotations 进行处理
输出Log
虽然是编译时执行Processor,但也是可以输入日志信息用于调试的。
Processor日志输出的位置在编译器下方的Messages窗口中。
Processor支持最基础的System.out方法。
同样Processor也有自己的Log输出工具: Messager。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
//取得Messager对象
Messager messager = processingEnv.getMessager();
//输出日志
messager.printMessage(Diagnostic.Kind.NOTE,
"Annotation class : className = " + element.getSimpleName().toString());
}
同Log类似,Messager也有日志级别的选择。
- Diagnostic.Kind.ERROR
- Diagnostic.Kind.WARNING
- Diagnostic.Kind.MANDATORY_WARNING
- Diagnostic.Kind.NOTE
- Diagnostic.Kind.OTHER
他们的输出样式如图:
注意:当没有属于该Process处理的注解被使用时,process不会执行。
注意:如果发现替换jar后,apt代码并没有执行,尝试clean项目。
这里你会发现输出了两次日志信息。其原因在于APT扫描了源码两次,可为什么要扫描两次?
用生成的代码来生成代码
APT可以扫描源码中的所有注解,依据这些注解来生成代码,那么生成的代码中如果也有注解呢?
同样可以被扫描到,并且用于代码生成。其过程如下:
APT第一次扫描源码中的所有注解,扫描结束后生成代码,之后再扫描一次,以保证生成的代码中的注解也可以被扫描到,第二次扫描到注解后继续生成代码,类似于递归一样的【扫描 - 代码生成 - 扫描 - 代码生成 - 扫描 - 代码生成 - 扫描 - 代码生成】。一直到扫描到的注解为0时停止。
同样你肯定也会发现一个问题,这不很容易会变成死循环吗?
没错,所以在生成的代码中一定要慎重出现编译时注解,把控好你的代码逻辑!
Element
Element也是APT的重点之一,所有通过注解取得元素都将以Element类型等待处理,也可以理解为Element的子类类型与自定义注解时用到的@Target是有对应关系的。
Element的官方注释:
Represents a program element such as a package, class, or method.
Each element represents a static, language-level construct (and not, for example, a runtime construct of the virtual machine).
表示一个程序元素,比如包、类或者方法。
例如:取得所有修饰了@OnceClick的元素。
for (Element element : roundEnv.getElementsAnnotatedWith(OnceClick.class)){
//OnceClick.class是@Target(METHOD)
//则该element是可以强转为表示方法的ExecutableElement
ExecutableElement method = (ExecutableElement)element;
//如果需要用到其他类型的Element,则不可以直接强转,需要通过下面方法转换
//但有例外情况,我们稍后列举
TypeElement classElement = (TypeElement) element
.getEnclosingElement();
}
Element的子类有:
ExecutableElement
表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
对应@Target(ElementType.METHOD) @Target(ElementType.CONSTRUCTOR)
PackageElement;
表示一个包程序元素。提供对有关包极其成员的信息访问。
对应@Target(ElementType.PACKAGE)
TypeElement;
表示一个类或接口程序元素。提供对有关类型极其成员的信息访问。
对应@Target(ElementType.TYPE)
注意:枚举类型是一种类,而注解类型是一种接口。
TypeParameterElement;
表示一般类、接口、方法或构造方法元素的类型参数。
对应@Target(ElementType.PARAMETER)
VariableElement;
表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。
对应@Target(ElementType.LOCAL_VARIABLE)
例如:@OnceClick的@Target(METHOD)。其修饰方法,那么在这个情况下:
Element 可以直接强制转换为ExecutableElement。而其他类型的Element不能直接强制转,需要其他办法。
for (Element element : roundEnv.getElementsAnnotatedWith(OnceClick.class)){
ExecutableElement method = (ExecutableElement)element;
}
接下来我们将以@Target()分类进行讲解,不同Element的信息获取方式不同。
修饰方法的注解和ExecutableElement
当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。
那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值。
如何获取:
//OnceClick.class 以 @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());
}
}
修饰属性、类成员的注解和VariableElement
当你有一个注解是以@Target(ElementType.FIELD)定义时,表示该注解只能修饰属性、类成员。
那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、类成员类型、类成员名
如何获取:
for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) {
//ElementType.FIELD注解可以直接强转VariableElement
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) element
.getEnclosingElement();
PackageElement packageElement = elementUtils.getPackageOf(classElement);
//类名
String className = classElement.getSimpleName().toString();
//包名
String packageName = packageElement.getQualifiedName().toString();
//类成员名
String variableName = variableElement.getSimpleName().toString();
//类成员类型
TypeMirror typeMirror = variableElement.asType();
String type = typeMirror.toString();
}
修饰类的注解和TypeElement
当你有一个注解是以@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();
}
《Android编译时注解框架-Run Project:OnceClick》
《Android编译时注解框架-语法讲解》