如何成为T型人才,垂直在一个行业中,必须要有一整套知识体系,在这里,就一个字坚持~
前言
Annotation意思代表注解,其实留意平时代码,你会发现我们一直跟注解打交道。Android主流框架框架大量用到注解,要想看懂框架源码,弄懂实现原理,在注解、反射、泛型必须下点功夫,这些知识点我会单独讲解。注解的功能就要强大很多,不但能够生成描述符文件,而且有助于减轻编写“样板”代码的负担,使代码干净易读并且有利于减低耦合度。接下来分点介绍:
1、Annotation概念介绍和实用注解
2、了解注解处理器
2、什么是编译时注解,如何自定义,如何通过APT进行解析
3、什么是运行时注解,如何自定义,如何通过反射进行解析
Contonts
一、Annotation概念介绍和实用注解
1.1、什么是注解(Annotation)
Java提供的一种元程序中的元素关联任何信息或者任何元数据(metadata)的途径和方法。基本规则:Annotation(注解)不能影响程序代码的执行,无论增加,删除注解,代码都始终如一的执行。
1.2、什么是元数据(Metadata)
- 以标签的形式存在于Java代码中
- 描述的信息是类型安全的
- 需要编译器之外的工具额外的处理用来生成其他的程序部件
- 可以存在于Java源代码级别,也可以存在于Java代码编译之后的class文件内部
1.3、什么是元注解
用来定义其他注解的注解。元注解分别四种:@Retention、 @Target、@Inherited、@Documented。每种元注解,有多种值可以选择,具体功能如下。
-
@Retention:表示什么级别保存注解信息,参数值通过RetentionPolicy枚举获取。
参数 说明 SOURCE 注解将被编译器丢弃,只能存于源代码中 CLASS 注解在class文件中可用,能够存于编译之后的字节码之中,但会被VM丢弃 RUNTIME VM在运行期也会保留注解,因此运行期注解可以通过反射获取注解的相关信息 -
@Target:表示注解使用范围,参数值通过ElementType枚举获取。
参数 说明 TYPE 类,接口(包括注释类型)或enum声明 FIELD 域声明(包括enum实例) METHOD 方法声明 PARAMETER 参数声明 CONSTRUCTOR 构造器的声明 LOCAL_VARIABLE 局部变量声明 ANNOTATION_TYPE 注解类型声明 PACKAGE 包声明 TYPE_PARAMETER 输入参数声明 TYPE_USE 使用类型声明 -
@Inherited:表明我们标记的注解是被继承的。如果一个父类使用了@Inherited修饰的注解,则允许子类继承该父类的注解。同时该声明注解只对类有效,对与方法、属性无效。
-
@Documented:标记的注解可以被javadoc此类的工具文档化。
@Documented @Inherited @Retention(RetentionPolicy.CLASS) //编译时时注解 @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) //应用于类、接口(包括注解)、enum、方法、成员变量上 public @interface Point { int id() default -1; //埋点Id String message() default ""; //埋点描述 }
解析说明
- 通过 @interface 定义,注解名即为自定义注解名,这里注解名为Point
- 注解中将 id 和 message称为:int元素id , String 元素 message。而且注解元素的类型是有限制的,并不是任何类型都可以,主要包括:基本数据类型(理论上是没有基本类型的包装类型的,但是由于自动封装箱,所以也不会报错)、String 类型、enum 类型、Class 类型、Annotation 类型、以及以上类型的数组,否则编译器便会提示错误。
1.4、常用Java注解
-
@Override:表示当前的方法定义将覆盖超类中的方法,如果方法拼写错误或者方法签名不匹配,编译器便会提出错误提示。
@Target(ElementType.METHOD) // 限制方法上使用 @Retention(RetentionPolicy.SOURCE) //源码上 public @interface Override { }
-
@Deprecated: 表示当前方法已经被弃用,如果开发者使用了注解为它的元素,编译器便会发出警告信息。
@Documented @Retention(RetentionPolicy.RUNTIME) //运行时 @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
-
@SuppressWarnings:用来抑制编译时的警告信息。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) //源码上 public @interface SuppressWarnings { String[] value(); 定义一个value类型为String参数 }
参数 说明 deprecation 过滤使用了过时的类或方法的警告 unchecked 过滤执行了未检查的转换警告 fallthrough 过滤switch语句发生case穿透的警告 path 过滤类路径,源文件路径等路径不存在的警告 serial 过滤可序列化类上缺少serialVersionUID定义时的警告 finally 过滤任何finally子句不能完成时的警告 all 过滤所有情况的警告
1.5、常用Android注解
-
@NonNull:声明方法的参数不能为空
-
@Nullable:表示一个方法的参数或者返回值可以是Null的
-
@CallSuper:重写的方法必须要调用super方法
-
@RequiresPermission:辅助发现需要申请的权限
-
@CheckResult:用来注解方法,如果一个方法得到了结果,却没有使用这个结果,就会有错误出现
-
@Keep:不混淆
-
@ColorInt:限定颜色资源id
-
@UiThread:通常可以等同于主线程,标注方法需要在UIThread执行
-
@MainThread:主线程,经常启动后创建的第一个线程
-
@WorkerThread:工作者线程
-
@BinderThread:注解方法必须要在BinderThread线程中执行,使用较少
二、运行时注解
介绍之前,先引用 Jeremy Meyer 的一段话:如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5 扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具 apt帮助程序员解析带有注解的 java 源代码。
根据上面描述我们可以知道,注解处理器并不是一个特定格式,并不是只有继承了 AbstractProcessor 这个抽象类才叫注解处理器,凡是根据相关API 来读取注解的类或者方法都可以称为注解处理器。
2.1、反射
在解析注解时,我可以通过反射机制获取对象,再通过调用类Class对象获取特定注解,便可以获取注解之上的元素值,主要方法如下。
-
forName(className):获取全限定类名,获取Class对象
Class c = Class.forName(className);
-
getDeclaredMethods():获取该对象内部所有方法
Method[] methods = clasz.getDeclaredMethods();
-
getDeclaredConstructors():获取该对象内部所有构造方法
Constructor[] constructors clasz.getDeclaredConstructors();
-
getAnnotation(AnnotationName.class) :表示得到该 Target 某个 Annotation 的信息,因为一个 Target 可以被多个 Annotation 修饰
/* * 根据注解类型返回方法的指定类型注解 */ Point annotation = (Point) constructor .getAnnotation(Point.class);
-
getAnnotations() :则表示得到该 Target 所有 Annotation
/* * 获取该类所以注解数组 */ Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { Point point = (Point) annotation }
-
isAnnotationPresent(AnnotationName.class) 表示该 Target 是否被某个 Annotation 修饰
/* * 判断构造方法中是否有指定注解类型的注解 */ boolean hasAnnotation = constructor .isAnnotationPresent(Point.class); if (hasAnnotation) { /* * 根据注解类型返回方法的指定类型注解 */ Point annotation = (Point) constructor .getAnnotation(Point.class); }
2.2、自定义运行时注解(RUNTIME)
通过@Retention(RetentionPolicy.RUNTIME)定义注解,当前的注解在运行时是可见,采用反射方式进行解析,并获取注解信息。
-
如何创建运行时注解
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD,ElementType.CONSTRUCTOR}) public @interface Point { String key() default ""; //埋点key int message() default -1; //埋点描述 }
-
如何使用自定义运行时注解
public class RuntimePoint { @Point(key = "00", message = R.string.send_message_point) private void sendMessage() { System.out.println("发送数据.."); } @Point(key = "11", message = R.string.receive_message_point) private void receiveMessage() { System.out.println("接受数据.."); } }
-
如何使用反射机制解析运行时注解
public class RuntimeParser { /** * 使用到类注解 该方法只打印了 Type 类型的注解 * * @param className * @throws ClassNotFoundException */ public static void parseTypeAnnotation(String className) throws ClassNotFoundException { if (TextUtils.isEmpty(className)) return; Class clazz = Class.forName(className); //读取该类所有注解信息 Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { if (annotation instanceof Point) { Point point = (Point) annotation; System.out.println("key=" + point.key() + ";message=" + point.message()); } } } /** * 使用到构造注解,该方法只打印了 Method类型的注解 */ public static void parseConstructAnnotation(){ //获取改类所有构造对象 Constructor[] constructors = RuntimePoint.class.getDeclaredConstructors(); for(Constructor constructor:constructors){ //判断该构造对象是否存在@Point注解 boolean hasAnnotation = constructor.isAnnotationPresent(Point.class); if (hasAnnotation) { Point point = (Point) constructor.getAnnotation(Point.class); System.out.println("key=" + point.key() + ";message=" + point.message()); } } } /** * 使用到方法注解 该方法只打印了 Method类型的注解 */ public static void parseMethodAnnotation() { //获取该类中所有方法 Method[] methods = RuntimePoint.class.getDeclaredMethods(); for (Method method : methods) { //判断该方法是否存在@Point注解 boolean hasAnnotation = method.isAnnotationPresent(Point.class); if (hasAnnotation) { Point point = method.getAnnotation(Point.class); System.out.println("key=" + point.key() + ";message=" + point.message()); } } } }
三、编译时注解
3.1、AnnotationProcessor
由于反射对性能会有一定的损耗,现在使用最多的还是 AbstractProcessor 自定义注解处理器,因为后者并不需要通过反射实现,效率和直接调用普通方法没有区别,这也是为什么编译期注解比运行时注解更受欢迎,但是并不是说为了性能运行期注解就不能用了,只能说不能滥用,要在性能方面给予考虑。目前主要的用到运行期注解的框架差不多都有缓存机制,只有在第一次使用时通过反射机制,当再次使用时直接从缓存中取出。
3.1.1、创建注解处理器
/**
* Created by zmj on 2017/6/19.
*/
@AutoService(javax.annotation.processing.Processor.class)
public class BuriedPointProcessor extends AbstractProcessor {
/**
* 做一些初始化工作,注释处理工具框架调用了这个方法,
* 当我们使用这个方法的时候会给我们传递一个 ProcessingEnvironment
* 类型的实参。
* 如果在同一个对象多次调用此方法,则抛出IllegalStateException异常
* @param processingEnvironment 这个参数里面包含了很多工具方法
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
/**
* 返回用来在元素上进行操作的某些工具方法的实现
*/
Elements es = processingEnvironment.getElementUtils();
/**
* 返回用来创建新源、类或辅助文件的Filer
*/
Filer filer = processingEnvironment.getFiler();
/**
* 返回用来在类型上进行操作的某些实用工具方法的实现
*/
Types types = processingEnvironment.getTypeUtils();
/**
* 这是提供给开发者日志工具,我们可以用来报告错误和警告以及提示信息
* 注意 message 使用后并不会结束过程
* Kind 参数表示日志级别
*/
Messager messager = processingEnvironment.getMessager();
messager.printMessage(Diagnostic.Kind.ERROR,"例如当默认值为空则提示一个错误");
/**
* 返回任何生成的源和类文件应该符合的源版本
*/
SourceVersion version = processingEnvironment.getSourceVersion();
super.init(processingEnvironment);
}
/**
* 注解的处理逻辑
* @param set
* @param roundEnvironment
* @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
/**
* TypeElement 这表示一个类或者接口元素集合
* 常用方法不多,TypeMirror getSuperclass()返回直接超类
*
* 详细介绍下 RoundEnvironment 这个类
* 常用方法:
* boolean errorRaised() 如果在以前的处理round中发生错误,则返回true
* Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
* 这里的 a 即你自定义的注解class类,返回使用给定注解类型注解的元素的集合
* Set<? extends Element> getElementsAnnotatedWith(TypeElement a)
*
* Element 的用法:
* TypeMirror asType() 返回此元素定义的类型 如int
* ElementKind getKind() 返回元素的类型 如 e.getkind() = ElementKind.FIELD 字段
* boolean equals(Object obj) 如果参数表示与此元素相同的元素,则返回true
* Name getSimpleName() 返回此元素的简单名称
* List<? extends Elements> getEncloseElements 返回元素直接封装的元素
* Element getEnclosingElements 返回此元素的最里层元素,如果这个元素是个字段等,则返回为类
*
* 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)
*/
return false;
}
/**
* 指出注解处理器 处理哪种注解
* 在 jdk1.7 中,我们可以使用注解 @SupportedAnnotationTypes()代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 指定当前注解器使用的Jdk版本
* 在 jdk1.7 中,我们可以使用注解 @SupportedSourceVersion()代替
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
-
init(ProcessingEnvironment processingEnvironment): 做一些初始化工作,注释处理工具框架调用了这个方法,当我们使用这个方法的时候会给我们传递一个 ProcessingEnvironment 类型的实参。
-
getSupportedAnnotationTypes():告知Processor哪些注解需要处理。在 jdk1.7 中,我们可以使用注解 @SupportedAnnotationTypes()代替,需要考虑jdk版本问题。
@Override public Set<String> getSupportedAnnotationTypes() { //返回一个Set集合,集合内容为 自定义注解的包名+类名。 LinkedHashSet<String> types = new LinkedHashSet<>(); types.add(Point.class.getCanonicalName()); return types; }
@SupportedAnnotationTypes({"com.linwei.buriedpointlibrary.Point"})
注:@SupportedAnnotationTypes修饰AbstractProcessor类
-
process(Set<? extends TypeElement >annotations,RoundEnvironment roundEnv):这个方法是AbstractProcessor 中最核心,主要进行注解解析操作,该方法放回true,不要求后续Processor处理它们,反之,则继续执行处理。
-
getSupportedSourceVersion():指定当前注解器使用的Jdk版本, 在 jdk1.7 中,我们可以使用注解 @SupportedSourceVersion()代替,需要考虑jdk版本问题。
3.1.2、使用android-apt插件
我们的主工程项目(app)中引用了 processor 库,但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而主工程项目添加了这个库会引入很多不必要的文件。为了处理这个问题我们需 要引入插件android-apt。它主要有两个作用:
-
仅仅在编译时期去依赖注解处理器所在的函数库并进行工作,但不会打包到APK中。
-
为注解处理器生成的代码设置好路径,以便Android Studio能够找到它。 接下来介绍如何使用它。首先需要在整个工程(Progect)的 build.gradle 中添加如下语句:
apply plugin: 'com.neenbedankt.android-apt' dependencies { classpath 'com.android.tools.build:gradle:3.6.1' //gradle3.0.0出现不兼容问题 classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }
接下来在主工程项目(app)的 build.gradle 中以apt的方式引入注解处理器processor,如下所示
dependencies {
//引入注解
implementation project(':annotation')
//引入注解处理器
apt project(':annotation-processor')
//annotationProcessor project(':annotation-processor')
}
3.1.3、注册注解处理器
为了能使用注解处理器,需要用一个服务文件来注册它。现在我们就来创建这个服务文件。首先在 anotation-processor 库的 main 目录下新建 resources 资源文件夹,接下来在 resources 中再建立META-INF/services目录 文件夹。最后在META-INF/services中创建 javax.annotation.processing.Processor文件,这个文件中的内容是 注解处理器的名称。这里我们的javax.annotation.processing.Processor文件的内容为 com.linwei.buriedpointlibrary.BuriedPointProcessor,完整目录如下:
-annotation-processor(注解处理器java moule)
-main
-BuriedPointProcessor.java
-resources
-META-INF
-services
-javax.annotation.processing.Processor
如果你觉得前面创建服务文件的步骤比较麻烦,也可以使用 Google 开源的 AutoService,它用来帮助开 发者生成META-INF/services/javax.annotation.processing.Processor文件。首先我们添加该开源库,可以在 File→Project Structure 搜索“auto-service”查找该库并添加,也可以在anotation-processor的build.gradle中 直接添加如下代码:
dependencies {
implementation 'com.google.auto.service:auto-service:1.0-rc3'
}
最后在注解处理器BuriedPointProcessor中添加@AutoService(Processor.class)就可以了:
@AutoService(javax.annotation.processing.Processor.class)
public class BuriedPointProcessor extends AbstractProcessor {
}
3.2、自定义编译时注解(CLASS)
通过@Retention(RetentionPolicy.CLASS)定义注解,当前的注解在编译时是可见,一般通过自定义AbstractProcessor 处理器。在使用AbstractProcessor 创建自定义注解处理器时,只需要继承AbstractProcssor类。
在自定义编译时注解时,注解处理器必须单独创建java module,因为Android环境是不支持。而且,注解定义(创建)单独抽取到一个java module中,其实这样处理是有好处,在主工程项目(app)的build.gradle中以apt方式引入注解处理器时,因为只有在编译处理期间需要用到,编译处理完成就没有实际作用,避免主项目引入多余文件。
-
项目目录结构,module项目引入,build.gradle配置,代码如下:
-BuriedPointFactory(Android Project) -app -build.gradle==>dependencies { //引入注解 implementation project(':annotation') //引入注解处理器 annotationProcessor project(':annotation-processor') } -annotation(定义注解java module)==>dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } -annotation-processor(注解处理器java moule)==>dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //注册注解处理器 implementation 'com.google.auto.service:auto-service:1.0-rc3' //生成java文件 implementation 'com.squareup:javapoet:1.11.1' //引入注解 implementation project(':annotation') } -build.gradle==>dependencies { classpath 'com.android.tools.build:gradle:3.6.1' //gradle3.0.0出现不兼容问题 //classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }
2.创建埋点注解,在annotation(java module)项目进行创建,代码如下:
@Retention(RetentionPolicy.CLASS) //编译时注解
@Target({ElementType.TYPE, ElementType.METHOD,ElementType.CONSTRUCTOR})
public @interface Point {
String key() default ""; //定义埋点标识key
int message() default -1; //定义埋点描述
}
3.编写我们的解析器,继承 AbstractProcessor ,并重写 process 方法,处理相关逻辑。
//@SupportedAnnotationTypes({"com.linwei.buriedpointlibrary.Point"})
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(javax.annotation.processing.Processor.class)
public class BuriedPointProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
/**
* 注解的处理逻辑
*
* @param set
* @param roundEnvironment
* @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Messager messager = processingEnv.getMessager();
//第一步,根据我们自定义的注解拿到elememts set 集合
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Point.class);
TypeElement typeElement;
VariableElement variableElement;
ExecutableElement executableElement;
// 第二步: 根据 element 的类型做相应的处理
for (Element element : elements) {
//第三步:返回此元素定义的类型
ElementKind kind = element.getKind();
messager.printMessage(Diagnostic.Kind.NOTE, "kind=" + kind );
if (kind == ElementKind.CLASS) {
//判断该元素是否为类
typeElement = (TypeElement) element;
String qualifiedName = typeElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "Name=" + qualifiedName);
} else if (kind == ElementKind.FIELD) {
// 判断该元素是否为成员变量
variableElement = (VariableElement) element;
//获取该元素的封装类型
typeElement = (TypeElement) variableElement.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "Name=" + qualifiedName);
}else if(kind==ElementKind.METHOD){
executableElement = (ExecutableElement) element;
typeElement = (TypeElement) executableElement.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "Name=" + qualifiedName);
}
}
return true;
}
/**
* 指出注解处理器 处理哪种注解
* 在 jdk1.7 中,我们可以使用注解 @SupportedAnnotationTypes()代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
//返回一个Set集合,集合内容为 自定义注解的包名+类名。
LinkedHashSet<String> types = new LinkedHashSet<>();
types.add(Point.class.getCanonicalName());
return types;
}
/**
* 指定当前注解器使用的Jdk版本
* 在 jdk1.7 中,我们可以使用注解 @SupportedSourceVersion()代替
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
4、注解使用方式,代码如下
@Point(key = "00", message = R.string.send_message_point)
public void apply() {
//增加编译时埋点
System.out.println("point");
}
增加注解后,点击Rebuild Project按钮进行项目编译,编译成功后,在Build Output控制台输出注解信息,信息如下:
注: kind=METHOD
注: Name=com.linwei.buriedpointfactory.MainActivity
四、总结
该篇幅比较长,能坚持看完,恭喜您离成功又近了一步。该篇主要讲解注解一些名词概念,一些api使用,如何创建运行时注解,编译时注解,并进行简单使用。弄懂注解知识点,对于框架原理理解有很大帮助,在下片文章我会带领大家进行简单手写ButterKnife框架。文章有哪些知识点有出入,希望大家"举报"作者,谢谢!
参考资料
- Android 自定义注解详解
- Android注解快速入门和实用解析
- Android进阶之光
About me
-
Email:linwei9605@gmail.com
-
Blog: offer.github.io/
-
Github: github.com/WeiShuaiDev