Android修炼系列(五),写一篇超全面的annotation讲解(1)
不学注解,也许是因为平时根本不需要没事自定义个这玩意玩,可随着Android形势越来越内卷,不学点东西是真不行了。而通过本文的学习,可以让你对于注解有个全面的认识,你会发现,小小的注解,大有可为,编不下去了..
注解不同于注释,注释的作用是为了方便自己或者别人的阅读,能够利用 javadoc 提取源文件里的注释来生成人们所期望的文档,对于代码本身的运行是没有任何影响的。
而注解的功能就要强大很多,不但能够生成描述符文件,而且有助于减轻编写“样板”代码的负担,使代码干净易读。通过使用扩展的注解(annotation)API 我们能够在 编译期 和 运行期 对代码进行操控。
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后的某个时刻非常方便的使用这些数据。 —Jeremy Meyer
本文主要对于下面几个方面进行讲解,篇幅很长,建议收藏查看:
Java 最初内置的三种标准注解
注解是 java SE5中的重要的语言变化之一,你可能对注解的原理不太理解,但你每天的开发中可能无时无刻不在跟注解打交道,最常见的就是 @Override 注解,所以注解并没有那么神秘,也没有那么冷僻,不要害怕使用注解(虽然使用的注解大部分情况都是根据需要自定义的注解),用的多了自然就熟了。为什么说最初的三种标准注解呢,因为在后续的 java 版本中又陆陆续续的增加了一些注解,不过原理都是一样的。
java SE5内置的标准注解 | 含义 |
---|---|
@Override | 表示当前的方法定义将覆盖超类中的方法,如果方法拼写错误或者方法签名不匹配,编译器便会提出错误提示 |
@Deprecated | 表示当前方法已经被弃用,如果开发者使用了注解为它的元素,编译器便会发出警告信息 |
@SuppressWarnings | 可以关闭不当的编译器警告信息 |
Java 提供的四种元注解和一般注解
所谓元注解(meta-annotation)也是一种注解,只不过这种注解负责注解其他的注解。所以再说元注解之前我们来看一下普通的注解:
public @interface LogClassMessage {}
这是一个最普通的注解,注解的定义看起来很像一个接口,在 interface 前加上 @ 符号。事实上在语言级别上,注解也和 java 中的接口、类、枚举是同一个级别的,都会被编译成 class 文件。而前面提到的元注解存在的目的就是为了修饰这些普通注解,但是要明确一点,元注解只是给普通注解提供了作用,并不是必须存在的。
java 提供的元注解 | 作用 |
---|---|
@Target | 定义你的注解应用到什么地方(详见下文解释) |
@Retention | 定义该注解在哪个级别可用(详见下文解释) |
@Documented | 将此注解包含在 javadoc 中 |
@Inherited | 允许子类继承超类中的注解 |
〔1〕@Target使用的时候添加一个 ElementType 参数,表示当前注解可以应用到什么地方,即可以指定一种,也可以同时指定多种,使用方法如下:
// 表示当前的注解只能应用到类、接口(包括注解)、enum上面
@Target(ElementType.TYPE)
public @interface LogClassMessage {}
复制代码
// 表示当前的注解只能应用到方法和成员变量上面
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {}
复制代码
下面来看一下 ElementType 的全部参数含义:
ElementType 参数 | 说明 |
---|---|
ElementType.CONSTRUCTOR | 构造器的声明 |
ElementType.FIELD | 域的声明(包括enum的实例) |
ElementType.LOCATION_VARLABLE | 局部变量的声明 |
ElementType.METHOD | 方法的声明 |
ElementType.PACKAGE | 包的声明 |
ElementType.PARAMETER | 参数的声明 |
ElementType.TYPE | 类、接口(包括注解类型)、enum声明 |
〔2〕@Retention用来注解在哪一个级别可用,需要添加一个 RetentionPolicy 参数,用来表示在源代码中(SOURCE),在类文件中(CLASS)或者运行时(RUNTIME):
// 表示当前注解运行时可用
@Retention(RetentionPolicy.RUNTIME)
public @interface LogClassMessage {}
复制代码
下面来看一下 RetentionPolicy 的全部参数含义:
RetentionPolicy 参数 | 说明 |
---|---|
RetentionPolicy.SOURCE | 注解将被编译器丢弃,只能存于源代码中 |
RetentionPolicy.CLASS | 注解在class文件中可用,能够存于编译之后的字节码之中,但会被VM丢弃 |
RetentionPolicy.RUNTIME | VM在运行期也会保留注解,因此运行期注解可以通过反射获取注解的相关信息 |
在注解中,一般都会包含一些元素表示某些值,并且可以为这些元素设置默认值,没有元素的注解也称为标记注解(marker annotation)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {
public int id () default -1;
public String message() default "";
}
复制代码
注:虽然上面的 id 和 message 定义和接口的方法定义很类似,但是在注解中将 id 和 message 称为:int 元素 id , String 元素 message。而且注解元素的类型是有限制的,并不是任何类型都可以,主要包括:基本数据类型(理论上是没有基本类型的包装类型的,但是由于自动封装箱,所以也不会报错)、String 类型、enum 类型、Class 类型、Annotation 类型、以及以上类型的数组,(没有等字,说明目前注解的元素类型只支持上面列出的这几种),否则编译器便会提示错误。
invalid type 'void ' for annotation member // 例如注解类型为void的错误信息
对于默认值限制 ,Bruce Eckel 在其书中是这样描述的:编译器对元素的默认值有些过分挑剔,首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供注解的值。其次,对于非基本类型的元素,无论在源代码声明中,或者在注解接口中定义默认值时,都不能以 null 作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有元素都存在,并且都具有相应的值。为了绕开这个约束,我们只能自己定义一些特殊的值,例如空字符串或者负数,以此表示某个元素的不存在,这算得上是一个习惯用法。
参考系统的标准注解
怎么说呢,接触一种知识的途径有很多,可能每一种的结果都是大同小异的,都能让你学到东西,但是实现的方式、实现过程中的规范、方法和思路却并不一定是最佳的。
上文讲到的是注解的基本语法,那么系统是怎么用的呢?首先让我们来看一下使用频率最高的 @Override :
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}
复制代码
〔1〕首先系统定义一个没有元素的标记注解 Override ,随后使用元注解 @Target 指明 Override 注解只能应用于方法之上(你可以细想想,是不是在我们实际使用这个注解的时候,只能是重写方法,没有见过重写类或者字段的吧),使用注解 @Retention 表示当前注解只能存在源代码中,并不会出现在编译之后的 class 文件之中。
@Override
protected void onResume() {
super.onResume();
}
复制代码
〔2〕如在 Activity 中我们可以重写 onResume() 方法,添加注解 @override 之后编译器便会去检查父类中是否存在相同方法,如果不存在便会报错。
〔3〕也许到这里你会感到很疑惑,注解到底是怎么工作的,怎么系统这样定义一个注解 Override 它就能工作了?黑魔法吗,擦擦,完成看不到实现过程嘛(泪流满面),经过查阅了一些资料(非权威)了解到,其实处理过程都编写在了编译器里面,也就是说编译器已经给我们写好了处理方法,当编译器进行检查的时候就会调用相应的处理方法。
注解处理器
介绍之前,先引用 Jeremy Meyer 的一段话:如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5 扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具 apt帮助程序员解析带有注解的 java 源代码。
根据上面描述我们可以知道,注解处理器并不是一个特定格式,并不是只有继承了 AbstractProcessor 这个抽象类才叫注解处理器,凡是根据相关API 来读取注解的类或者方法都可以称为注解处理器。
反射机制下的处理器
最简单的注解处理器莫过于,直接使用反射机制的 getDeclaredMethods 方法获取类上所有方法(字段原理是一样的),再通过调用 getAnnotation 获取每个方法上的特定注解,有了注解便可以获取注解之上的元素值,方法如下:
public void getAnnoUtil(Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
LogClassMessage logClassMessage = m.getAnnotation(LogClassMessage .class);
if(null != logClassMessage) {
int id = logClassMessage.id();
String method = logClassMessage.message();
}
}
}
复制代码
由于反射对性能会有一定的损耗,所以上述类型的注解处理器并不占主流,现在使用最多的还是 AbstractProcessor 自定义注解处理器,因为后者并不需要通过反射实现,效率和直接调用普通方法没有区别,这也是为什么编译期注解比运行时注解更受欢迎。
但是并不是说为了性能运行期注解就不能用了,只能说不能滥用,要在性能方面给予考虑。目前主要的用到运行期注解的框架差不多都有缓存机制,只有在第一次使用时通过反射机制,当再次使用时直接从缓存中取出。
好了,说着说着就跑题,还是来聊一下这个 AbstractProcessor 类吧,到底有何魅力让这么多人为她沉迷,方法如下:
public class MyFirstProcessor extends AbstractProcessor {
/**
* 做一些初始化工作,注释处理工具框架调用了这个方法,给我们传递一个 ProcessingEnvironment 类型的实参。
*
* <p>如果在同一个对象多次调用此方法,则抛出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);
}
/**
* @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理。
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
/**
* TypeElement 这表示一个类或者接口元素集合常用方法不多,TypeMirror getSuperclass()返回直接超类。
*
* <p>详细介绍下 RoundEnvironment 这个类,常用方法:
* boolean errorRaised() 如果在以前的处理round中发生错误,则返回true
* Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
* 这里的 a 即你自定义的注解class类,返回使用给定注解类型注解的元素的集合
* Set<? extends Element> getElementsAnnotatedWith(TypeElement a)
*
* <p>Element 的用法:
* TypeMirror asType() 返回此元素定义的类型 如int
* ElementKind getKind() 返回元素的类型 如 e.getkind() = ElementKind.FIELD 字段
* boolean equals(Object obj) 如果参数表示与此元素相同的元素,则返回true
* Name getSimpleName() 返回此元素的简单名称
* List<? extends Elements> getEncloseElements 返回元素直接封装的元素
* Element getEnclosingElements 返回此元素的最里层元素,如果这个元素是个字段等,则返回为类
*/
return false;
}
/**
* 指出注解处理器 处理哪种注解
* 在 jdk1.7 中,我们可以使用注解 {@SupportedAnnotationTypes()} 代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 指定当前注解器使用的Jdk版本。
* 在 jdk1.7 中,我们可以使用注解{@SupportedSourceVersion()}代替
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
复制代码
自定义运行期注解(RUNTIME)
我们在开发中经常会需要计算一个方法所要执行的时间,以此来直观的比较哪个实现方式最优,常用方法是开始结束时间相减
System.currentTimeMillis()
但是当方法多的时候,是不是减来减去都要减的怀疑人生啦,哈哈,那么下面我就来写一个运行时注解来打印方法执行的时间。
1.首先我们先定义一个注解,并给注解添加我们需要的元注解:
/**
* 这是一个自定义的计算方法执行时间的注解。
* 只能作用于方法之上,属于运行时注解,能被VM处理,可以通过反射得到注解信息。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CalculateMethodRunningTime {
// 要计算时间的方法的名字
String methodName() default "no method to set";
}
复制代码
2.利用反射方法在程序运行时,获取被添加注解的类的信息:
public class AnnotationUtils {
// 使用反射通过类名获取类的相关信息。
public static void getClassInfo(String className) {
try {
Class c = Class.forName(className);
// 获取所有公共的方法
Method[] methods = c.getMethods();
for (Method m : methods) {
Class<CalculateMethodRunningTime> ctClass = CalculateMethodRunningTime.class;
if (m.isAnnotationPresent(ctClass)) {
CalculateMethodRunningTime anno = m.getAnnotation(ctClass);
// 当前方法包含查询时间的注解时
if (anno != null) {
final long beginTime = System.currentTimeMillis();
m.invoke(c.newInstance(), null);
final long time = System.currentTimeMillis() - beginTime;
Log.i("Tag", anno.methodName() + "方法执行所需要时间:" + time + "ms");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
3.在 activity 中使用注解,注意咱们的注解是作用于方法之上的:
public class ActivityAnnotattion extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anno);
AnnotationUtils.getClassInfo("com.annotation.zmj.annotationtest.ActivityAnnotattion");
}
@CalculateMethodRunningTime(methodName = "method1")
public void method1() {
long i = 100000000L;
while (i > 0) { i--; }
}
}
复制代码
4.运行结果:
作者:矛盾的阿呆i
链接:https://juejin.cn/post/6936609673416015908
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。