APT
APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额
外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容
由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
像第三方的 DBflow Dagger2 ButterKnife 等,在编译时根据Annotation生成相关的代码,利用此高大上的简单技术,给开发带来很大的便利.
Annotation(注解)
(一)概念
Java提供了Annotations来对代码提供一些注解,以解释、约束代码,也可以称为编程的元数据。
eg: @Override(重载) @Deprecated(方法不被推荐使用)
Android比Java多提供了一个库android-support:support-annotations,这也是Java注解的扩展.所以也称之为Android Annotations.
官方定义为:表示为程序语言的元数据格式,可以添加如Java的源代码中,类、方法、变量、参数、包都可以被注解。不同于Javadoc的标签,Java的注解如果设置为留存到VM可识别的运行时态,那么它是可以通过反射获取出来的。注解本身对他们所注解的代码没有直接的影响。
(二)分类
1.内置注解
@Deprecated , @Override , @SuppressWarnings @SafeVarargs(这个是java8添加的) ,@Functionsallnterface(这个是java8添加的)
这些都定义在 java.lang中
2.元注解
用来描述注解的注解
@Doucmented 是否默认通过javadoc或者类似的进行文档化
@Inherited 注解类型被自动继承
@Retention 该注解的保留时间
如:RetentionPolicy.SOURCE
该注解只保留到代码层,编译器将对其忽略。因此其不会出现在生成的class文件中。
RetentionPolicy.CLASS
该注解保留在编译器中,因此能出现在生成的class文件中,但是被JVM忽略,所以不能在运行时获取注解内容。
RetentionPolicy.RUNTIME
该注解能保留在JVM中,可以在运行时通过反射的方法获取具体内容。
如果自定义注解时不进行指定,默认为RetentionPolicy.CLASS。
@Target 制定注解的目标,给什么类型注解的
ElementType.ANNOTATION_TYPE
–can be applied to an annotation type.给注解注解
ElementType.CONSTRUCTOR
–can be applied to a constructor.给构造方法注解
ElementType.FIELD
–can be applied to a field or property. 给字段注解,不要忘了,字段可以是对象
ElementType.LOCAL_VARIABLE
–can be applied to a local variable. 给局部变量注解
ElementType.METHOD
–can be applied to a method-level annotation.给方法注解
ElementType.PACKAGE
–can be applied to a package declaration.给包注解
ElementType.PARAMETER
–can be applied to the parameters of a method.给参数注解
ElementType.TYPE
–can be applied to any element of a class. 给类(型)注解
@Repeatable @Native (后两个是java8新增加的)
3. Android 扩展注解
1)NUllness Annotations 指定参数是否为空
2)资源类型注解 指定参数是何种资源类型
@StringRes @DrawableRes @ColorRes @AnyRes这个特殊表示任何类型
3)IntDef / StringDef 类型定义注解
4)线程注解
@UiThread @WorkerThread @MainThread @BinderThread
5)RGB颜色整型
6)值约束 @Size @IntRange
7)权限注解 @RequiresPermission
8)方法重写 @CallSuper
9)返回值 @CheckResult
10) 测试注解
@VisibleForTesting 标注到类 方法 字段上,方便测试时候使用
11) @Keep 标记式注解
4 自定义注解
(三)解析
解析就是对注解进行分析提取, 获取具体的内容过程,根据生命周期的不同,分为运行时解析和编译时解析.
编译时解析
编译时 Annotation 指 @Retention 为 Class的 Annotation, 自动解析。需要做的
1、自定义类集成自 AbstractProcessor
2、重写其中的 process 函数
关于编译时解析,主要问题在于自己如何实现apt(Annotation Processor Tool)的processor,编译环境如何使用它。
1、自定义Annotation;
2、通过apt提供的接口,自定义Annotation processor类,实现主要的处理方法;
3、注册自定的Processor,以便apt识别;
4、在使用的module内指定使用apt来编译该processor。
三 如何实现?(步骤)
1)编写api
可以是Android Library 或者是 Java Library . 定义api,也就是让别人知道如何使用这个项目. 例如:在此处我们自定义注解.
2)注解器的处理(compiler)
注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。
这里我们需要把项目定义成Java Library ,因为Android 不支持apt接口,想要支持必须插件化处理.太麻烦.
主要就是对Annotation的解析.所以这里需要引入 api(Annotion)项目.
这里最主要的就是继承AbstractProcessor,自定义Processor,我们还要注册这个services.
方法一:在resources资源文件夹下面创建
META-INF/services/javax.annotation.processing.Processor,并将自定义的processor名称加入
文件的内容是一个列表,每一行是一个注解处理器的全称
方法二 :通过注解的方式自动添加services注册。
javapoet 是方块公司出的,提供了各种 API 让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。
auto-service 是 Google 家的,主要用于注解 Processor,对其生成 META-INF 配置信息。
3)项目app具体使用
在根目录的 build.gradle 文件中的 dependencies 节点下面添加如下代码:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
//git地址 https://github.com/JakeWharton/butterknife
app的 build.gradle 中添加如下代码:
apply plugin: 'com.neenbedankt.android-apt'
dependencies { compile project(':annotation') apt project(':compiler') }
这里我们可以看到该模块不仅依赖api模块,还需要告诉编译器编译时需要apt对compiler进行解析(apt procject(‘:compiler’))。
总结整理如下:
1、标记,用于告诉编译器一些信息
2、编译时动态处理,如动态生成代码
3、运行时动态处理,如得到注解信息
4、参数/返回值检查(类型、范围、权限等)
5、定义程序实现标准(Callsuper等)
6、描述项目框架的配置信息(类似XML的作用)
参考 :http://m.blog.csdn.net/article/details?id=51444426
四 实践(activity跳转的处理)
1.定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Router {
String[] value();
String[] intExtra() default "";
String[] longExtra() default "";
String[] boolExtra() default "";
String[] shortExtra() default "";
String[] floatExtra() default "";
String[] doubleExtra() default "";
String[] byteExtra() default "";
String[] charExtra() default "";
String[] transfer() default "";
}
可以看到的是,这是编译时期的注解,主要作用于Class。之后,在调用的地方就是需要使用我们的这个注解。
这里把注解定义为Annotation Library,而且是Java工程
2.创建注解处理器
@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {
private Messager messager;
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(Router.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> activities = roundEnv.getElementsAnnotatedWith(Router.class);
//roundEnv.getElementsAnnotatedWith(Factory.class) 返回一个被@Router 注解的元素列表。
//Element可以是类,方法,变量等。所以,我们下一步需要做的是检查这个元素是否是一个类:
if (activity.getKind() != ElementKind.CLASS) {
error("Router can only apply on class");
}
//这里是自已业务的具体实现处理
return true;
}
}
需要注意的点
a)
其中使用 AutoSerivce 的注解,这样就不用再手动配置 META-INF文件了。
b)方法 getSupportedAnnotationTypes 则是定义我们针对生成的注解类,方法 process 则是我们的重头戏,其中则是我们生成代码
的主要逻辑之处
c)我们要确保只有class类型的元素被我们的处理器处理。前面我们已经学过,类是一种TypeElement
元素。那我们为什么不使用
if (! (annotatedElement instanceof TypeElement))
来检查呢?这是错误的判断,因为接口也是一种TypeElement
类型。所以在注解处理器中,你应该避免使用
instanceof
,应该用ElementKind
或者配合TypeMirror
使用TypeKind
。
常用方法
常用Element子类
TypeElement:类
ExecutableElement:成员方法
VariableElement:成员变量
通过包名和类名获取TypeName
TypeName targetClassName = ClassName.get("PackageName", "ClassName");
通过Element获取TypeName
TypeName type = TypeName.get(element.asType());
获取TypeElement的包名
String packageName = processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString();
获取TypeElement的所有成员变量和成员方法
List<? extends Element> members = processingEnv.getElementUtils().getAllMembers(typeElement);
自定义的类实现解读
Element
元素,源代码中的每一部分都是一个特定的元素类型,分别代表了包、类、方法等等,相当于 XML 中的 DOM树,可以通过一个元素去访问它的父元素或者子元素。
private Filer mFiler; //文件相关的辅助类
private Elements mElementUtils; //元素相关的辅助类
private Messager mMessager; //日志相关的辅助类
i)错误的处理
Messager
为注解处理器提供了一种报告错误消息,警告信息和其他消息的方式。它不是注解处理器开发者的日志工具。Messager
是用来给那些使用了你的注解处理器的第三方开发者显示信息的。非常重要的是Kind.ERROR
,因为这种消息类型是用来表明我们的注解处理器在处理过程中出错了。有可能是第三方开发者误使用了我们的注解。这个概念与传统的 java 应用程序有一点区别。传统的 java 应用程序出现了错误,你可以抛出一个异常。如果你在process()
中抛出了一个异常,那jvm 就会崩溃。注解处理器的使用者将会得到一个从 javac 给出的非常难懂的异常错误信息。因为它包含了注解处理器的堆栈信息。因此注解处理器提供了Messager
类。它能打印漂亮的错误信息,而且你可以链接到引起这个错误的元素上。为了能够获取Messager
显示的信息,非常重要的是注解处理器必须不崩溃地完成运行。这就是我们在调用error()
后执行return true
的原因。如果我们在这里没有返回的话,process()
就会继续运行,因为messager.printMessage( Diagnostic.Kind.ERROR)
并不会终止进程。如果我们没有在打印完错误信息后返回的话,我们就可能会运行到一个空指针异常等等。就像前面所说的,如果我们继续运行process()
,一旦有处理的异常在process()
中被抛出,javac 就会打印注解处理器的空指针异常堆栈信息,而不是Messager
显示的信息。
ii)读取注解类里面的方法属性等
注解处理器在编译 java 源码之前执行,我们需要考虑已编译和未编译两种情况.
最好的解决办法是: MirroredTypeException
包含一个TypeMirror
,它表示我们未被编译类。因为我们知道它一定是一个Class
类型(我们前面有检查过),所以我们可以将它转换为DeclaredType
, 然后获取TypeElement
来读取合法名称。
所有的注解器必须有无参构造方法
在 init()
可以初始化拿到一些实用的工具类。
在 getSupportedAnnotationTypes()
方法中返回所要处理的注解的集合。
在 getSupportedSourceVersion()
方法中返回 Java 版本。
通常你应该返回SourceVersion.latestSupported()
。不过,如果你有足够的理由坚持用 java 6 的话,你也可以返回SourceVersion.RELEASE_6
。我建议使用SourceVersion.latestSupported()
。在 Java 7 中,你也可以使用注解的方式来替代重写getSupportedAnnotationTypes()
和 getSupportedSourceVersion().
由于兼容性问题,特别是对于 android ,建议重写getSupportedAnnotationTypes()
和 getSupportedSourceVersion()
注解处理器运行在它自己的 JVM 中( 注解处理器的整个处理过程跟普通的 Java 程序没什么区别)。javac 启动了一个完整的java 虚拟机来运行注解处理器。这意味着什么?你可以使用任何你在普通 java 程序中使用的东西。使用 guava
!你可以使用依赖注入工具,比如dagger
或者任何其他你想使用的类库。但不要忘记,即使只是一个小小的处理器,你也应该注意使用高效的算法及设计模式,就像你在开发其他 java 程序中所做的一样。我们可以使用面向对象的思想和设计模式,将相关逻辑封装到 model 中,使得流程更清晰简洁。分别将注解的成员变量、点击方法和整个注解类封装成不同的 model。
3)Annotation-Processing-Tool详解
http://qiushao.net/2015/07/07/Annotation-Processing-Tool%E8%AF%A6%E8%A7%A3/
4)代码生成
执行完成之后,在app的 build/generated/source/apt 目录下