不知道你们有没有一种感受,当你想要了解某个知识点的时候,就会发现好多技术类 APP 或者公众号在推一些关于这个知识点的文章。也许这就是大数据的做用,这也说明总有人比你抢先一步。学习不能停滞,要不你就会被别人越落越远。java
本文接着来回顾和总结 Java 基础中注解的知识点和简单的使用,一样本文将从如下几个方面来回顾注解知识:android
注解(Annotation),也叫元数据。一种代码级别的说明。它是 JDK 1.5 之后版本引入的一个特性,与类、接口、枚举是在同一个层次。它能够声明在包、类、字段、方法、局部变量、方法参数等元素上。它提供数据用来解释程序代码,可是注解并不是是所解释的代码自己的一部分。注解对于代码的运行效果没有直接影响。数组
注解有许多用处,主要以下:sass
如咱们所熟知的依赖注入框架 ButterKnife
就是在编译阶段来生成 findViewById
的代码(文件)的,而咱们所见过的 @Deprecated
就是提供信息给编辑器的RetentionPolicy.SOURCE
类型注解,说明这个属性已通过时的,对于运行时的注解在反射的文章的最后咱们也举了个小例子,说明了它的做用。app
在自定义了一个编译或者运行阶段的注解后,须要一个开发者编写相应的代码来解释这些注解,从而来发挥注解的做用。这些用来解释注解的代码被统称为是 APT(Annotation Processing Tool)
。换句话说注解实际上是给 APT 或者编辑器来使用的,而对于非框架开发人员的咱们咱们只须要关注注解的使用,并遵照规则便可,从而咱们节省了不少代码提升了效率。框架
可是凡事若是只知足于用上,就不算是一个合 (tong) 格 (guo) 程 (mian)序 (shi) 员 (de)! 可是不要慌,当你打开这篇文章的时候你已经离 offer 又进了一步。编辑器
注解的声明和声明一个接口十分相似,没错只是名字很相似~ 咱们使用@interface
来声明一个注解,如咱们最多见的Override
注解的声明ide
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } 复制代码
注解声明的修饰符,能够是 private
,public
, protected
或者默认 default
这一点跟定义一个类或者接口相同。函数
在声明一个注解的时候咱们经常须要一些其余注解来修饰和限制该定义注解的使用和运行方式。上述的 @Target
和 @Retention
就是如此,咱们称之为元注解,详细的元注解在下边说明。工具
注解跟一个类类似,它们并非都是像上面的 @Override
同样只有声明。一个类大概能够包含构造函数,成员变量,成员函数等,而一个注解只能包含注解成员,注解成员的声明格式为:
类型 参数名() default 默认值;
注解成员能够是:
基本类型 byte
,short
,int
,long
,float
,double
,boolean
八种基本类型及这些类型的数组, 注意这里没对应基本数据类型的包装类。
String
,Enum
,Class
,annotations
及这些类型的数组
注解的成员修饰符只能是 public
或默认(default)
注解元素必须有肯定的值,能够在注解中定义默认值,也可使用注解时指定。即咱们在定义注解的时候声明的成员,能够不赋值,可是就跟抽象函数同样,在使用的时候就必须指定。
如:
public @interface TestAnnotation { String value() default ""; String[] values(); int id() default -1; int[] ids(); // 错误的不能使用包装类 以及自定义类型 // Integer idInt(); // Apple apple(); enum Color {BULE, RED, GREEN} Color testEnum() default Color.BULE; Color[] testEnums(); //注解类型成员 注解元素必须有肯定的值,能够在注解中定义默认值,也可使用注解时指定 FruitName fruitName() default @FruitName("apple"); } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) protected @interface FruitName { String value(); String alias() default "no alias"; } 复制代码
咱们在 Override 注解的声明中能够看到还有注解修饰着如@Target(ElementType.METHOD)
,咱们讲元注解理解为修饰注解定义的注解。换句话说元注解为 JDK 提供给咱们的一些基本注解,咱们使用元注解来定义一个注解是如何工做的。
JDK 1.8 中存在的元注解有如下 5 种:
@Target, @Retention、@Documented、@Inherited、@Repeatable
下面咱们依次来讲明这几种类型的注解是如何使用的。
@Target 指定了被修饰的注解运用的地方,这些 "地方" 定义在 ElementType
类中,包括:
ElementType.ANNOTATION_TYPE
能够给一个注解进行注解ElementType.CONSTRUCTOR
能够给构造方法进行注解ElementType.FIELD
能够给属性进行注解ElementType.LOCAL_VARIABLE
能够给局部变量进行注解ElementType.METHOD
能够给方法进行注解ElementType.PACKAGE
能够给一个包进行注解ElementType.PARAMETER
能够给一个方法内的参数进行注解ElementType.TYPE
能够给一个类型进行注解,好比类、接口、枚举其中 METHOD
、PARAMETER
、FIELD
最为常见,如 Override
注解被 @Target(ElementType.METHOD)
修饰,若是咱们想要标记一个参数不能为空则可使用 @NonNull
去修饰一个 param, FIELD
用来指定注解只能用来修饰成员变量如咱们常用的 @BindView
。
值得注意的是 @Target 元注解定义以下,
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); } 复制代码
它内部的成员为ElementType[]
数组也就是说,咱们能够同时指定一个注解能够用于不少地方。如 @ColorRes 的注解的元注解为@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
。
Retention 翻译过来是保留期的意思。当@Retention
用于修饰一个注解上的时候,它规定了了被修饰的注解应用的时期,或者存活的时期
它能够有以下 3 种取值:
RetentionPolicy.SOURCE
注解只在源码阶段保留,在编译器进行编译时它将被丢弃。RetentionPolicy.RUNTIME
注解能够保留到程序运行的时候,它会被加载进入到 JVM 中,因此在程序运行时经过反射获取到它们,并解释他们。RetentionPolicy.CLASS
注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。对于第一种 RetentionPolicy.SOURCE
注解只在源码阶段保留,更多的效果时作一些编译检查,在 Android 中有个为 @IntDef
的注解,他能够和常量组合一块儿 代替枚举 enum 作参数限制做用,来优化内存使用。
这里说只是替代了参数限制做用,而 JDK 1.5 为咱们带来的 enum 的做用不仅是简单的参数限制做用做用,对于 Enum 更多优雅使用能够参考 《Effective Java》。
如 @IntDef
的注解定义以下:
@Retention(SOURCE) @Target({ANNOTATION_TYPE}) public @interface IntDef { long[] value() default {}; boolean flag() default false; } 复制代码
如咱们经常使用的设置一个 View 的可见属性就使用了 @IntDef
注解来保证使用者传入的参数是对的,以下:
@IntDef({VISIBLE, INVISIBLE, GONE}) @Retention(RetentionPolicy.SOURCE) public @interface Visibility {} @RemotableViewMethod public void setVisibility(@Visibility int visibility) { setFlags(visibility, VISIBILITY_MASK); } //设置一个 View 的属性: ... toolbar.setVisibility(View.VISIBLE);// it is Ok //toolbar.setVisibility(1000);// 若是咱们随便写一个数值 那么编辑器将会报错 复制代码
源码级别的注解对咱们的编码约束,运行期注解与之不一样的是,若是要是让该注解生效,咱们必需要编写必定的代码去将定义好的注解,在运行中"注入"应用中,看到运行时注入就能够应该能想得起反射,是的注入这个操做就是须要开发人员本身编写的。
另外,咱们也都了解,在运行反射的时候效率是没法保证的。由于反射将遍历对应类的 Class 文件来获取相应的信息。因此运行时注解,并非那么普遍被运用,而稍后咱们要说明的编译期注解则不会对程序的运行形成效率的影响,所以应用更普遍一些。
咱们来试着写一个 Dota 英雄名称的运行期注解来了解下他的运做方式:
/** * 定义一个注解表示英雄的名字 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) private @interface HeroName { String value(); String alias(); } /** * 定义一个类包含英雄名称的属性 */ public class Hero { // 定义注解的时候没有 deflaut 属性名称因此在使用的时候必须赋值 @HeroName(value = "Spirit Walker", alias = "SB") private String heroName; public void setHeroName(String heroName) { this.heroName = heroName; } public String getHeroName() { return heroName; } } 复制代码
ok 声明就是这么简单,那么如何让一个属性生效呢,这时候咱们就须要一个注解处理方法。为了方便观察运行注解的结果,因此咱们这个处理方法选择传递一个 Hero 对象,不过你为了更通用也能够不用这么作。
/** 运行时注解处理方法*/ public static void getHeroNameInfo(Hero hero) { try { Class<? extends Hero> clazz = hero.getClass(); Field field = clazz.getDeclaredField("heroName"); // Field isAnnotationPresent 判断一个属性是否被对应的注解修饰 if (field.isAnnotationPresent(HeroName.class)) { //field.getAnnotation 获取属性的注解 HeroName fruitNameAnno = field.getAnnotation(HeroName.class); hero.setHeroName("name = " +fruitNameAnno.value() +" alias = " + fruitNameAnno.alias()); } } catch (NoSuchFieldException e) { e.printStackTrace(); } } 复制代码
下面咱们来运行下程序测试下:
public static void main(String[] args) { Hero hero = new Hero(); getHeroNameInfo(hero); System.out.println("hero = " + hero); } 复制代码
运行结果:
hero = Hero{heroName='name = Spirit Walker alias = SB'} 复制代码
经过上述的例子,能够了解运行时注解就是这样声明和运用的。相信 SB 这个别名更容易让你们记得这个例子(白牛这个英雄其实很好玩,只是别名...)。
通过运行时注解的了解,相比对于注解应该都有一个大概的了解了。接下来到了编译时注解,这个注解类型,即是众多工具库中应用的注解类型,它不会影响运行时的效率问题,而是在编译期,或者打包过程当中就生成了对应的代码,在运行时将会生效。如咱们常见的 ButterKnife
和 EventBus
。
编译时注解与运行时注解不一样,编译时注解主要是帮助咱们在编译器编译期使用注解处理器生成相应的代码,帮咱们解放劳动力。
咱们知道运行时注解是经过反射来解释对应注解并使注解生效的,那么编译时如何解释对应的注解呢?这里就须要用到注解处理器的知识了。
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你能够自定义注解,并注册相应的注解处理器(自定义的注解处理器需继承自AbstractProcessor)。
Java 中提供给咱们了注解处理器实现方法,主要是经过实现一个名为 AbstractProcessor
的注解处理器基类。该抽象类要求咱们必须实现 process 方法来定义处理逻辑。下边咱们来看下注解处理器中的几个方法的做用:
public class NameProcessor extends AbstractProcessor { //会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供不少有用的工具类如Elements, Types和Filer等 @Override public synchronized void init(ProcessingEnvironment env){ } //返回最高所支持的java版本, 如返回 SourceVersion.latestSupported(); @Override public SourceVersion getSupportedSourceVersion() { } //一个注解处理器可能会处理多个注解逻辑,这个方法将返回待处理的注解类型集合,返回值做为参数传递给 process 方法。 @Override public Set<String> getSupportedAnnotationTypes() { } //process 函数就是咱们处理待处理注解的地方了,咱们须要在这里编写生成 java 文件的具体逻辑。 方法返回布尔值类型,表示注解是否已经处理完成。通常状况下咱们返回 true 便可。 @Override public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { } } 复制代码
注解处理器的处理步骤主要有如下:
ButterKnife.bind(this);
方法就是获取对应 Activity 的注解处理器生成的java 文件,并执行了构造函数。自定义编译时注解要比运行时注解要繁琐一些。下面咱们来举一个简单的例子,意在说明编译时注解是如何工做的。
在 Android 中为了实现一个编译时注解咱们通常须要借助两个三方库:
com.google.auto.service:auto-service:1.0-rc2
这是谷歌官方提供的一个注解处理注册插件能够帮助咱们更方便的注册注解处理器,只须要在自定义的 Processor 类上方添加@AutoService(Processor.class)
便可,不用本身动手执行注解处理器的注册工做(即编写 resource/META-INF/services/javax.annotation.processing.Processor文件)。
为了更方便的在 process 文件中生成 Java 类,须要依赖一个 Square 公司开源的 javapoet 库,com.squareup:javapoet:1.9.0
这个库中包装提供了一些好用的 API 帮助咱们更快更准确的构建 .java 文件。固然你也能够本身手写拼接字符串而后写入文件(若是你能保证正确)。
仿照 ButterKnife 的实现,咱们创建一个新的 Android project ,而后建立两个 Java Moudle,其中 processor
用来存放注解处理器,processor-lib
用来存放对应的注解,以下图所示:
在注解处理器存在的lib的 build.gradle 中添加依赖关系:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) compile project(':processor-lib') compile 'com.squareup:javapoet:1.9.0' compile 'com.google.auto.service:auto-service:1.0-rc3' } 复制代码
主 moudle 中也须要添加对 processor 和 processor -lib 的依赖:
dependencies { .... implementation project(':processor-lib') // 注意这里的注解处理器的依赖方式 annotationProcessor project(':processor') } 复制代码
好了通过上述的准备咱们终于可以编写咱们的编译时注解了:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface Name { String name(); String alias(); } 复制代码
public class SBHero { @Name(name = "Spirit Walker", alias = "SB") private String heroName; } public class PAHero { @Name(name = "Phantom Assassin", alias = "PA") private String heroName; } 复制代码
// @AutoService(Processor.class) 帮助咱们生成对应的注解处理器配置 @AutoService(Processor.class) @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("com.wangshijia.www.processor.Name") public class NamePorcessor extends AbstractProcessor { //文件写入工具类 private Filer filer; //能够帮助咱们在 gradle 控制台打印信息的类 private Messager messager; // 元素操做的辅助类 private Elements elementUtils; //自定义文件名的后缀 private static final String SUFFIX = "AutoGenerate"; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnv.getFiler(); messager = processingEnv.getMessager(); elementUtils = processingEnv.getElementUtils(); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * @return 你所须要处理的全部注解,该方法的返回值会被 process()方法所接收, 这里其实只有Name 注解, */ @Override public Set<String> getSupportedAnnotationTypes() { HashSet<String> set = new HashSet<>(); set.add(Name.class.getCanonicalName()); return set; } ... } 复制代码
最后咱们要编辑咱们的 process 方法了,process 方法中一共进行了下面这几件事:
~/app/build/generated/source/apt
目录下@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { String packageName= ""; // 得到被该注解声明的元素 Set<? extends Element> elememts = roundEnv.getElementsAnnotatedWith(Name.class); // 声明一个存放成员变量的列表 List<VariableElement> fields; //key 对应包含注解修饰元素的类的全类名 vaule 表明全部被注解修饰的变量 Map<String, List<VariableElement>> maps = new HashMap<>(); // 遍历程序中全部被该注解修饰器处理注解修饰的元素 for (Element ele : elememts) { // ele.getKind() 获取注解修饰的成员的类型,判断该元素是否为成员变量 if (ele.getKind() == ElementKind.FIELD) { VariableElement varELe = (VariableElement) ele; // 获取该元素封装类型 TypeElement enclosingElement = (TypeElement) varELe.getEnclosingElement(); // 拿到包含 enclosingElement 元素的类的名称 样式如 com.wangshijia.www.annotationapplication.Hero String key = enclosingElement.getQualifiedName().toString(); messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key); packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString(); fields = maps.get(key); if (fields == null) { maps.put(key, fields = new ArrayList<>()); } fields.add(varELe); } } /* * maps 包含有全部被 @Name 修饰的类 */ for (String key : maps.keySet()) { List<VariableElement> elementFileds = maps.get(key); messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key); messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + elementFileds); String className = key.substring(key.lastIndexOf(".") + 1); className += SUFFIX; // 建立 className 类 TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); // 建立方法 MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("printNameAnnotation") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class); //建立方法中的打印语句 for (VariableElement e : elementFileds) { Name annotation = e.getAnnotation(Name.class); // 建立 printNameAnnotation 方法 methodBuild .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.name()) .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.alias()); } //将方法中添加到类中 MethodSpec printNameMethodSpec = methodBuild.build(); TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build(); try { //构造的 java 文件 参数一 包名,参数二 上述构建的类描述 TypeSpec JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec) .addFileComment(" This codes are generated automatically. Do not modify!") .build(); javaFile.writeTo(filer); } catch (IOException exception) { exception.printStackTrace(); } } 复制代码
上述注释写的很详细了,这里但愿不熟悉的朋友,本身动手实现下,才能更好的理解是如何构建对应的文件的。生成的文件位于指定目录下:
使用咱们定义好的注解生成文件
使用注解生成器生成的 java 文件和普通的类没什么区别,经过编译后就放在上述文件夹中,咱们能够正常调用咱们构造类的方法,ButterKnife.bind(this)
实际上就是调用生成类的方法的过程。咱们是一个简单的 demo 就不这么复杂的调用了。直接在 App 目录下的任意文件调用,如在一个 Activity 中:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); PAHeroAutoGenerate.printNameAnnotation(); SBHeroAutoGenerate.printNameAnnotation(); } } 复制代码
对于 JavaPoet 生成 java 文件的过程若是想深刻了解的话能够查看该博客:JavaPoet - 优雅地生成代码
平常开发中,注解可以帮助咱们写出更好更优秀的代码,为了更好地支持 Android 开发,在已有的 android.annotation 基础上,Google 开发了 android.support.annotation 扩展包,共计50个注解,帮助开发者们写出更优秀的程序,这五十多种注解得以应用场景各不相同,常见的如 @IntDef @ColorInt @Nullable。
对于这些注解的用途这里再也不详细说明,感兴趣的能够去查看下一个朋友写的关于 Android 中注解的做用的文章: Android 注解指南
这篇文章写的时候遇到不少的困难,由于本人对于注解以前了解状况和大多数人同样,只停留在不多的使用阶段,在文章的构成方面也是一改再改。可是功夫不负有心人,在查阅了大量的资料后,学习到了不少注解的使用和原理的知识。也发现本身的知识掌握程度已经落下很多,好比鸿洋大神写的 Android 打造编译时注解解析框架 这只是一个开始 这篇文章在15年的时候就有了,想一想当时刚毕业,与大神的距离整整拉开了进3年,让我去哭一会。可是我的认为这是件好事。总比一直停留在用上好一些,每次深一步了解,就感受我跟大神之间的差距少了一些。