主目录见:Android高级进阶知识 我们在开发的时候为了提高效率往往会选择一个基于注解的框架,但是有时使用反射通常被认为是性能的收割机,所以我们会青睐编译期注解的使用,其实早在前面我们分析了[EventBus3.0源码解析]中我们就有看到,还有我们接下来要讲的ButterKnife也会用到,当然我今天要用来讲的例子[LRouter]这个项目也会使用这个。
上篇文章已经讨论了注解的作用和注解的生命周期,那么具体注解是如何简化我们工作,减少我们编写重复的代码的呢?接下我们将学习使用APT,在编译期生成代码。
- 什么是注解
- 注解分类
- 注解作用分类
- 元注解
- Java内置注解
- 自定义注解
- 自定义注解实现及使用
- 编译时注解
- 注解处理器
- 注解处理器基本代码
- 注解处理器一般处理逻辑
- 注解处理器
- 如何编写基于编译时注解的项目
- 项目结构划分
- 注解模块的实现
- 注解处理器的实现
- Glide遇坑记之分析
- 收集信息
- 生成代理类
- 生成Java代码
- Glide遇坑记之分析
- API模块的实现
- ButterKnife工作流程解析
- ButterKnife 有哪些优势?
- ButterKnife工作流程
- Java注解的工作流程
- 参考资料
现在编译期注解这么火热,我们有理由要去学习一下,我们今天这篇文章是为了扫盲一下,使得我们下面分析这类型的框架更加得心应手。所以今天目标是:1.自己能编写一个编译期注解项目;2.在实际开发中能使用到这项技术;3.能学习此类型的框架的源码,然后收为己用。
APT是Annotation-Processing-tool的简写,称为注解处理器,一般来说,自定义注解是在运行时使用的,通过反射获取class上的注解,并进行解析处理,使用apt可以让我们在编译时处理注解(其实不仅仅可以处理注解,而是所有的类信息都可以处理,下面会有演示),我们这一篇一起来学习一下基本的套路。
什么是注解
在Java语法中,使用@符号作为开头,并在@后面紧跟注解名。被运用于类,接口,方法和字段之上。
注解也叫
元数据
,是一种代码级别的说明
,与类,接口。枚举是在用一个层次上,他可以声明在包,类,字段,方法,局部变量,方法参数等的前面,用来对这些变量进行说明,注释。注解可以提高代码的可读性,它可以向编译器,虚拟机等解释说明一些事情。降低项目的耦合度,自动生成Java代码,自动完成一些规律性的代码,减少开发者的工作量。
在编写这个框架之前,我们需要一些准备,因为此类框架需要的模块有点多:
我们使用Android studio编写一个基本的例子。
注解分类
- Java内置注解
- 元注解
- 自定义注解
- 运行时注解
- 编译时注解
目录结构
1.创建注解工程
同样我们先创建一个Java工程,编写一个注解类MyAnnotation
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
String value() default "MyAnnotation";
}
然而这一次我们就不是在反射的时候获取这个注解的信息了,我们要在编译的时候获取这个注解的信息,如何获取,就要使用到注解处理器Processor。
注解作用分类
- 编写文档
- 通过代码里标识的元数据生成文档【生成文档doc文档】
- 代码分析
- 通过代码里标识的元数据对代码进行分析【使用反射】
- 编译检查
- 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】
Java字段(类成员)和属性
属性只局限于类中方法声明,并不与类中其他的成员相关
Java中的属性通常可以理解为get和set方法;而字段通常叫做类成员
字段通常是在类中定义的类成员变量
因为这是个完整的项目,我们只是挑出其中的编译期注解部分来讲:
注解处理器Processor
了解一下注解处理器,注解处理器有什么作用呢,首先它会在编译期被调用,可以扫描特定注解的信息,你可以为你自己的的注解注册处理器(如何注册后面会讲),一个特定的注解处理器以java源码作为输入,然后生成一些文件作(通常为java)为输出,这些java文件同样会被编译。这意味着,你可以根据注解的信息和被注解类的信息生成你想生成的代码!
要生成怎么样的文件,我们简单定一下需求:
定义一个注解MyAnnotation,去注解MainActivity,然后处理器扫描生成一个java文件,这个java文件有个输出Hello
MyAnnotation的方法,运行的我们的MainAcitivity,然后调用这个java文件的方法。
元注解(负责注解其他的注解)
@Target
- 表示该注解用于什么地方,可能的ElementType参数包括:
- CONSTRUCTOR:构造器的声明
- FIELD:域声明
- LOCAL_VARIABLE:局部变量声明
- METHOD:方法声明
- PACKAGE:包声明
- PARAMETER:参数声明
- TYPE:类,接口或enum声明
- 表示该注解用于什么地方,可能的ElementType参数包括:
@Retention
- 表示在什么级别保留此信息,可选的RetentionPolicy参数包括:
- SOURCE:注解仅存在代码中,注解会被编译器丢弃
- CLASS:注解会在class文件中保留,但会被VM丢弃
- RUNTIME:VM运行期间也会保留该注解,因此可以通过反射来获得该注解
- 表示在什么级别保留此信息,可选的RetentionPolicy参数包括:
@Documented
- 将注解包含在javadoc中
@Inherited
- 允许子类继承父类的注解
- lrouter-annotation:用于放注解部分,是个java的模块
- lrouter-compiler:用于编写注解处理器,是个java模块
- lrouter-api:用于提供用户使用的api的,是个android模块
- app:这个是使用的实例,注解会在这里使用,也是android模块
2.创建Android工程
之前我们已经定义了我们注解MyAnnotation,并且有个默认值value是字符串MyAnnotation。接下来,我们要用这个去注解MainActivity,现在我们是在Java工程,那么我们新创建一个Android工程,里面有个MainActivity,这个工程依赖我们MyAnnotation所在的工程。
@MyAnnotation
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
接下来我们就要通过自己定义的注解处理器去扫描这个注解进而生成java文件,但是在此之前,我们需要先了解注解处理的工作流程和相关API。
Java内置注解
@Override,表示当前的方法定义将覆盖超类中的方法,如果出现错误,编译器就会报错。
- 当我们的子类覆写父类中的方法的时候,我们使用这个注解,这一定程度的提高了程序的可读性也避免了维护中的一些问题,比如说,当修改父类方法签名(方法名和参数)的时候,你有很多个子类方法签名也必须修改,否则编译器就会报错,当你的类越来越多的时候,那么这个注解确实会帮上你的忙。如果你没有使用这个注解,那么你就很难追踪到这个问题。
@Deprecated:如果使用此注解,编译器会出现警告信息。
- 一个弃用的元素(类,方法和字段)在java中表示不再重要,它表示了该元素将会被取代或者在将来被删除。
当我们弃用(deprecate)某些元素的时候我们使用这个注解。所以当程序使用该弃用的元素的时候编译器会弹出警告。当然我们也需要在注释中使用@deprecated标签来标示该注解元素。
- 一个弃用的元素(类,方法和字段)在java中表示不再重要,它表示了该元素将会被取代或者在将来被删除。
@SuppressWarnings:忽略编译器的警告信息
- 当我们想让编译器忽略一些警告信息的时候,我们使用这个注解。比如在下面这个示例中,我们的deprecatedMethod()方法被标记了@Deprecated注解,所以编译器会报警告信息,但是我们使用了@SuppressWarnings("deprecation")也就让编译器不在报这个警告信息了
当然了,目录不一定就是要分这么多个,大家可以根据自己的需要,有时候还可以进行合并。当然这些模块之间是有模块依赖的:
3.创建Compiler工程
自定义注解
- 运行时注解大多数时候实时运行时使用反射来实现所需效果,这很大程度上影响效率
- 编译时注解在编译时生成对应Java代码实现代码注入
lrouter-compiler依赖lrouter-annotation模块
AbstractProcessor
AbstractProcessor就是系统抽象出来的处理器类,编写一个MyProcessor继承AbstractProcessor,查看一下里面的方法
public class MyProcessor extends AbstractProcessor{
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
我们需要重写的方法有这么几个,
init(ProcessingEnvironment processingEnv)
:所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init(),它会被注解处理工具调用,以ProcessingEnvironment作为参数。ProcessingEnvironment
提供了一些实用的工具类Elements, Types和Filer。我们在后面将会使用到它们。
process(Set<? extends TypeElement> annoations, RoundEnvironment env) :这类似于每个处理器的main()方法。你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。使用RoundEnvironment 参数,你可以查询被特定注解标注的元素。
getSupportedAnnotationTypes():在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。
getSupportedSourceVersion() : 用来指定你使用的 java 版本,建议使用SourceVersion.latestSupported()。
接下来你必须知道的事情是:注解处理器运行在它自己的 JVM 中。是的,你没看错。javac 启动了一个完整的 java 虚拟机来运行注解处理器。这意味着什么?你可以使用任何你在普通 java 程序中使用的东西。使用 guava! 你可以使用依赖注入工具,比如dagger或者任何其他你想使用的类库。但不要忘记,即使只是一个小小的处理器,你也应该注意使用高效的算法及设计模式,就像你在开发其他java程序中所做的一样。
注意:在此之前我相信很多同学和我一样把MyProcessor写在了跟MyAnnotation中一样的工程里面,这里有一个compiler工程的分离原则,processor就是我们的compiler工程,这个Processor对于目标工程也就是我们上面的MainActivity所在的Android工程是不需要的,Android主工程所需要的是生成java文件。所以我们需要新建一个java工程作为存放我们MyProcessor的compiler工程,这个工程同样要依赖MyAnnotation所在的注解工程。
自定义注解实现及使用
自定义注解使用@interface来声明一个注解。创建一个自定义注解遵循:
public @interface 注解名 {方法参数}
自定义注解示例一
@Documented
@Target(ElementType.METHOD)
@Inherited @Retention(RetentionPolicy.RUNTIME)
public @interface Annotation{
int studentAge() default 18; //定义默认值
String studentName();
String stuAddress();
String stuStream() default "CSE";
}
@Annotation(studentName = "Chaitanya", stuAddress = "Agra, India")
public class Class {
...
}
自定义注解示例二
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface getViewTo {
int value() default -1;
}
public class MainActivity extends AppCompatActivity {
@getViewTo(R.id.textview)
private TextView mTv;
/**
* 解析注解,获取控件
*/
private void getAllAnnotationView() {
//获得成员变量
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
try {
//判断注解
if (field.getAnnotations() != null) {
//确定注解类型
if (field.isAnnotationPresent(GetViewTo.class)) {
//允许修改反射属性
field.setAccessible(true);
GetViewTo getViewTo = field.getAnnotation(GetViewTo.class);
//findViewById将注解的id,找到View注入成员变量中
field.set(this, findViewById(getViewTo.value()));
}
}
} catch (Exception e) {
}
}
}
}
在使用这个注解和api的时候,我们也要添加一些依赖:
注册处理器
我们在编译好的META-INF/services添加我们的处理器路径,谷歌已经提供一个很方便的库,帮助我们做这些东西,我们只需要在处理器工程添加依赖
compile 'com.google.auto.service:auto-service:1.0-rc2'
然后在Myprocessor中添加@AutoService(Processor.class)的注解,这样就完成了我们处理器的注册。
编译成生成的META-INF/services中就注册了我们的MyProcessor
接下来,我们编写一个我们自己的处理器,生成java文件,来讲解一下相关API,以及要注意的事项。
处理后的MyProcessor
public class MyProcessor extends AbstractProcessor{
//处理Element的工具类
private Elements mElementUtils;
//生成文件的工具
private Filer mFiler;
//日志信息的输出
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
@Override
public Set<String> getSupportedAnnotationTypes(){
Set<String> supportedType = new LinkedHashSet<String>();
supportedType.add(MyAnnotation.class.getCanonicalName());
return supportedType;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//do something
return false
}
接下来重点讲一下process方法的实现,之前提到,这个方法中会扫描文件,可以获取到带我们自定义注解的类,通过如下这个方法
//获取MyAnnotation注解的类的集合
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
编译时注解
说到编译时注解,就不得不说注解处理器 AbstractProcessor,如果你有注意,一般第三方注解相关的类库(基于注解的框架),如bufferKnike、ARouter,都有一个Compiler命名的Module,如下图X2.3,这里面一般都是注解处理器,用于编译时处理对应的注解。
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以对自定义注解注册相应的注解处理器,用于处理注解逻辑。
javac是收录于JDK中的Java语言编译器。该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。
本文由美高梅平台下载发布于美高梅平台下载,转载请注明出处:注解处理器,接下我们将学习使用APT
关键词: