玩转java(Android)注解
1. java标准(原生)注解概览
Java API 中,在java.lang、java.lang.annotation和javax.anotation包中定义了原生注解,分为三类:
- 编译注解
- 资源注解
- 元注解
1.1 编译相关注解
注解 | 释义 |
---|---|
这个不用多说了,天天看到,是重写检查。 | |
@Deprecated | 这个相信在阅读源码的时候会看到,表示方法过期或者不推荐了。 |
@SuppressWarnings | 用于包之外的其他声明项中,用来抑制某种类型的警告。(这让我想起了项目中handle的⚠️) |
@SafeVarargs | 用来断言函数的不定长参数可以放心使用。 |
@Generated | 用来告诉开发者,下面的代码是自动生成的,不建议手动去修改。 |
@FuncationInterface | 用来修饰接口,表示对应的接口是带单个方法的函数式接口。 |
1.2 资源相关注解
注解 | 释义 |
---|---|
@PostConstruct | 用在控制对象生命周期的环境中,例如Web服务器中,表示需要在构造方法之后应该立即调用被该注解修饰的方法。 |
@PreDestroy | 表示在删除一个被注入的对象之前应该先调用被该注解修饰的方法。 |
@Resource | 用于对Web容器注入资源。(我的博客就用到了,一开始还以为是SpringBoot的注解?。 |
@Resources | 同上,表示注入一个资源数组。 |
1.3 元注解
-
@Target 用在编写注解类时,用来指定该注解所适用的对象范围,范围如下:
| 类型 | 释义 | | --- | --- | |ANNOTATION|注解类型声明| |CONSTRUCTOR|构造函数| |FIELD|实例变量| |LOCAL_VARIABLE|局部变量| |METHOD|方法| |PACKAGE|包| |PARAMETER|方法参数| |TYPE|类,包涵枚举、接口、注解(是否包含struct?) |TYPE_PARAMETER|类型参数,是范型还是Class对象?| |TYPE_USE|类型用途?| 注意: 如果注解不使用@Target标注,那么就不能用在TYPE_PARAMETER和TYPE_USE这两种类型。
例子:
@Target({ElementType.TYPE,ElementType.PACKAGE})public @interface CrashReport
-
@Retention 用来指明注解的访问范围,也就是在什么时候保留注解。 范围|释义 -|- SOURCE|源码注解,当源码被编译后就会失效。 CLASS|编译时注解,会被编译进.class文件,但程序运行时失效。能够自动处理java源文件,并且生成更多源码、配置文件、脚本或其他玩意,这种神操作是通过在编译期间执行
javac -processor
调起Java编译器内置的注解处理器来实现的,比如黄油刀、GreenDao、Databinding这些玩意。 RUNTIME|运行时注解,一直保留有效性,并可通过反射读取注解信息。相对编译时注解,性能较低,但是灵活性好。注意: 不指定@Retention注解时默认可访问到CLASS范围。
注意: 以上三种范围的注解都可以使用注解处理器进行处理
例子:
@Retention(RetentionPolicy.CLASS)public @interface CrashReport
另注:编译时注解的好处当然就是自动生成代码,减少了编码逻辑,但是坏处就是每次修改都需要重新编译生效,让我的小mbp风扇狂转的元凶终于找到了?。
- @Document 表示被修饰的注解应该被包含在「被注解项」的文档中,如JavaDoc生成的文档。
- @Inherited 表示该注解可以被子类继承。
- @Repeatable 表示这个注解可以在同一个项上面应用多次。
2.玩玩注解处理器
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation),它诞生自java5,从java6开始开放API。注解处理器是运行在自己的jvm中的,在编译期间,javac会启动一个完整的jvm来单独跑注解处理器!
一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这具体的含义什么呢?你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。
2.1定义注解处理器
注意:在Android中自定义注解处理器,不能放在AndroidLibrary,因为Javac不能识别AndroidLibrary,而应该放在JavaLibrary中。
由于注解处理器的一大特色就是可以在编译时生成新的java文件,所以我们先来看一下java文件的相关知识。 所谓的java文件,只不过是个以.java为拓展名的文件而已,其中也存在类似xml的dom结构一样的语法树。在这个「语法树中」,包名、类生命、类的成员都是一个个「元素」Element。如:
package com.example; // PackageElementpublic class Foo { // TypeElement(包括接口、枚举、结构体) private int a; // VariableElement private Foo other; // VariableElement public Foo () {} // ExecuteableElement public void setA ( // ExecuteableElement int newA //形参 TypeElement ) {}}
那么也就意味着,如果我们得到了一个类,就可以遍历这个类的成员:
TypeElement fooClass = ... ; for (Element e : fooClass.getEnclosedElements()){ // iterate over children Element parent = e.getEnclosingElement(); // parent == fooClass }
现在我们认识了注解处理器中三个重要的定义:
元素 | 释义 |
---|---|
Element | 代表源代码文本结构。 |
TypeElement | 代表源代码中的类型信息,我先理解成范型。 |
TypeMirror | 类文件的主要信息,通过elements.asType()获取。 |
注意:注解不能被继承,即给父类添加了注解,但是其子类是继承不到的。
自定义注解处理器,首先要继承自AbstractProcessor类。
public class FactoryProcessor extends AbstractProcessor { private Types types; private Elements elements; private Filer filer;//这个不是过滤器filter,这个是用来创建文件的 private Messager messager;//Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。 /** * init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。 * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer * * @param processingEnvironment 提供给 processor 用来访问工具框架的环境 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); types = processingEnvironment.getTypeUtils(); elements = processingEnvironment.getElementUtils(); filer = processingEnvironment.getFiler(); messager = processingEnvironment.getMessager(); } /** * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称 * * @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合 */ @Override public SetgetSupportedAnnotationTypes() { Set set = new LinkedHashSet(); set.add(Factory.class.getCanonicalName()); return set; } /** * 指定注解处理器使用的java版本 * * @return */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported();//一般返回java的最后版本即可 } /** * 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。 * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素 * * @param set 请求处理的注解类型 * @param roundEnvironment 有关当前和以前的信息环境 * @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们; * 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们 */ @Override public boolean process(Set set, RoundEnvironment roundEnvironment) { ... }}
我们来关注一下getSupportedSourceVersion()
和getSupportedAnnotationTypes()
。这两个方法不要求强制重写,是因为在java7中添加了对应的注解,我们可以直接通过两个注解来替代重写这两个方法,如:
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)@SupportedAnnotationTypes( "com.example.myannotation.Factory")public class FactoryProcessor extends AbstractProcessor { ...}
网上很多文章都说不推荐使用注解方式来设置java版本和支持的注解类型,理由是提高兼容性。但愚以为...Java6才开放注解处理器的API,而Java7立刻就推出了这两个注解,之间仅相隔一个版本!而且现在java6跑的全都是古董级的软件。所以我就喜欢用注解的方式?。但是相应的,注解方式在java8上不能动态获取java版本,只能以版本常量做value,这点不是很方便,果然说java就是个c++--是有一定道理的!
2.2注册注解处理器
我们有了自定义的注解处理器,但他现在仅仅是一个类,并没有被调用,所以不会产生任何作用,所以我们还需要把它注册进javac,让javac在编译期间来调用它。
2.2.1 正规方法
在java的同级目录新建resources目录, 新建META-INF/services/javax.annotation.processing.Processor文件, 文件中填写你自定义的Processor全类名,注意大小写。在这个文件里写入自定义的注解处理器全名,以换行分隔:
com.example.MyProcessor com.foo.OtherProcessor net.blabla.SpecialProcessor
把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器。
2.2.2 简单方法
上面的方法很简单,却比较麻烦,所以谷歌为懒人提供了福音,通过一个注解一句代码直接搞定!
首先在注解处理所在的module中添加一个依赖,我喜欢Gradle:
implementation 'com.google.auto.service:auto-service:1.0-rc2'
然后在注解处理器头部,添加注解:
@AutoService(Processor.class)public class FactoryProcessor extends AbstractProcessor { ...}
就这样简单明了。AutoService注解可以自动在javac中注册服务,在这里我们注册的就是注解处理器,所以AutoService的Processor.class值是固定的。添加好之后,编译时javac就会自动调起注解处理器了,但是到目前为止自定义的注解处理器还是不会起作用,因为我们还没有真正使用它。
2.3 通过APT把注解处理器挂载到项目
我们之前只是在javaLibrary(即.jar,或者俗称的‘炸包?’)中定义了注解处理器,但还没有把这个炸包引入工程,但这里的引入并不是添加依赖,而是使用APT工具来挂载。
APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等
项目的Gradle中代码如下:
buildscript { repositories { jcenter() mavenCentral() // add } dependencies { classpath 'com.android.tools.build:gradle:2.1.2' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add } }
然后在module的Gradle中:
apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' // add // ... dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.4.0' compile project(':annotations') // compile project(':processors') 替换为下面 apt project(':processors') }
全部搞定之后发现AS提升报错,意思是说现在已经不用手动添加APT插件了,只需要在module的gradle中添加:
annotationProcessor project(':MyClassProcessor')
就可以了, 其他的什么都不用做!
最后make一下项目,注解使用成功,完活!
3. 反射的一些知识
关于使用「类对象」
try { Class clazz = annotation.type(); qualifiedGroupClassName = clazz.getCanonicalName(); simpleFactoryGroupName = clazz.getSimpleName();} catch (MirroredTypeException mte) { DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror(); TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement(); qualifiedGroupClassName = classTypeElement.getQualifiedName().toString(); simpleFactoryGroupName = classTypeElement.getSimpleName().toString();}
我们看上面的这段代码,其中的Class<?> clazz
就是类对象。处理类对象时有两种不同的情况,分别对应两种不同的处理方式:
- 这个类已被编译 如我们的其他.jar中包涵已经被我们的注解编译过的的.class文件。这种情况下,注解处理器可以直接获取注解的类对象。
- 如果还没有被编译 直接获取类对象会抛出MirroredTypeException异常,所以我们需要try-catch去捕获这个异常,从中获取
TypeMirror
,再经过一系列强转,最终获得TypeElement
类型,从中读取类对象信息。
元素检查
为了提高注解的健壮性,避免一些不必要的麻烦,当我们获取到TypeElement时,应该适时的做的一些元素的检查。(ps:如果不通过message输出错误信息,我们可能只会看到程序里报了个空指针,这时很容易一头雾水无法定位bug,但是message的错误信息毕竟需要手动去定义,所以还是推荐用在重要的错误上,那么元素检查就是必不可少的了!)
- 作用域检查:
if (!typeElement.getModifiers().contains(Modifier.PUBLIC)) { error(classElement, "The class %s is not public.", classElement.getQualifiedName().toString()); return false; }
- 是否是抽象类:
if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { error(classElement, "The class %s is abstract. You can't annotate abstract classes with @%", classElement.getQualifiedName().toString(), Factory.class.getSimpleName()); return false; }
- 继承关系检查:(注意,整个检查也可以使用typeUtils.isSubtype()来实现)
// 检查继承关系: 必须是@Factory.type()指定的类型子类 TypeElement superClassElement = elementUtils.getTypeElement(item.getQualifiedFactoryGroupName()); if (superClassElement.getKind() == ElementKind.INTERFACE) { // 检查接口是否实现了 if(!classElement.getInterfaces().contains(superClassElement.asType())) { error(classElement, "The class %s annotated with @%s must implement the interface %s", classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), item.getQualifiedFactoryGroupName()); return false; } } else { // 检查子类 TypeElement currentClass = classElement; while (true) { TypeMirror superClassType = currentClass.getSuperclass(); if (superClassType.getKind() == TypeKind.NONE) { // 到达了基本类型(java.lang.Object), 所以退出 error(classElement, "The class %s annotated with @%s must inherit from %s", classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), item.getQualifiedFactoryGroupName()); return false; } if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) { // 找到了要求的父类 break; } // 在继承树上继续向上搜寻 currentClass = (TypeElement) typeUtils.asElement(superClassType); } }
- 构造方法检查:
// 检查是否提供了默认公开构造函数 for (Element enclosed : typeElement.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { ExecutableElement constructorElement = (ExecutableElement) enclosed; if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers() .contains(Modifier.PUBLIC)) { // 找到了默认构造函数 return true; } } } // 没有找到默认构造函数 error(classElement, "The class %s must provide an public empty default constructor", classElement.getQualifiedName().toString()); return false; }
4. 代码生成
接着就是生成java的源码文件.java。说白了就是前面遇到的filer
对象提供了一个writer
用来向文件写入字符串,想想就累?,好在业界驰名Square公司首先注意到了这个问题,提供了和两个开源库。我们注意到前者已经4years没有维护了(版本才1.11.0就另起山头了。。)所以我们只来说JavaPoet的用法?。
/** * 生成代码 * * @param elements * @param filer * @throws IOException */ public void generateCode(Elements elements, Filer filer) throws IOException { TypeElement typeElement = elements.getTypeElement(qualifiedClassName); //得到生成的类的名称 String factoryClassName = typeElement.getSimpleName() + SUFFIX;// String factoryClassName = "Meal_Factory"; //得到包名 PackageElement packageElement = elements.getPackageOf(typeElement); String packageName = packageElement.isUnnamed() ? "" : packageElement.getQualifiedName().toString(); ClassName meal = ClassName.get("com.example.william.entity", "Meal"); ClassName margheritaPizza = ClassName.get("com.example.william.entity", "MargheritaPizza"); ClassName calzonePizza = ClassName.get("com.example.william.entity", "CalzonePizza"); ClassName tiramisu = ClassName.get("com.example.william.entity", "Tiramisu"); //梳理要生成的代码结构 MethodSpec create = MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(meal) .addParameter(Integer.class, "id") .addCode( " if (id == 0) { \n" + " return new $T();\n" + " } else if (id == 1) { \n" + " return new $T();\n" + " } else if (id == 2) { \n" + " return new $T(); \n" + " } else{\n" + " throw new IllegalArgumentException(\"id超出范围: \" + id);\n" + " }\n " , margheritaPizza, calzonePizza, tiramisu) .build(); TypeSpec helloWorld = TypeSpec.classBuilder(factoryClassName) .addModifiers(Modifier.PUBLIC) .addMethod(create) .build(); JavaFile javaFile = JavaFile.builder(packageName, helloWorld) .addFileComment(" This codes are generated automatically. Do not modify!") .build(); javaFile.writeTo(filer); }
附录:Android支持库support-Annotation注解
-
nullness注解
|注解|释义| | --- | --- | |@Nullable|标记函数的参数或者返回值可以为空。| |@NonNull|与上面相反。|
-
类型定义注解(经常用来替换枚举) |注解|释义| | --- | --- | |@IntDef|整形。| |@StringDef|字符串。|
-
线程注解 |注解|释义| | --- | --- | |@UiThread|标记运行在UI线程,一个App可以有多个UI线程。| |@MainThread|标记运行在主线程,一个App只能有一个主线程。| |@WorkerThread|标记运行在后台线程。| |@BinderThread|标记运行在binder线程。|
-
值范围注解 |注解|释义| | --- | --- | |@Size(min=1)|标记集合不可为空。| |@Size(max=23)|标记字符串最大字符数是23。| |@Size(2)|标记数组元素个数为2.| |@Size(multiple=2)|标记数组大小是2的整数倍。|
-
强制调用父级方法 |注解|释义| | --- | --- | |@CallSuper|必须调用父级中被重写的方法。|
-
返回值检查 @CheckResult(suggest="#enforcePermission(string,int,int,string)") 提示开发者对返回值进行检查和处理
-
单元测试注解 |注解|释义| | --- | --- | |@VisibleForTesting|单元测试可能需要访问一些不可见的类、函数或者变量,这时可以使用这个注解使其对测试可见。|
-
避免混淆注解 注解|释义 | --- | --- | |@Keep|用来标记在Proguard混淆过程中不需要被混淆的类或方法。|
-
权限注解 @RequiresPermission
-
只需一个权限:
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
-
需多个权限:
@RequiresPermission(allOf = {Manifest.permission.SET_WALLPAPER,Manifest.permission.SET_WALLPAPER})
-
需要至少一个权限:
@RequiresPermission(anyOf = {Manifest.permission.SET_WALLPAPER,Manifest.permission.SET_WALLPAPER})
-
Intent可在Action字符串定义上添加注解:
@RequiresPermission(anyOf = {Manifest.permission.SET_WALLPAPER,Manifest.permission.SET_WALLPAPER})String action = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
-
对于ContentProvider可能会同时用到读写两个操作,可以分别定义权限
@RequiresPermission.Read(Manifest.permission.SET_WALLPAPER)@RequiresPermission.Write(Manifest.permission.SET_WALLPAPER)Uri BOOKMARKS_URI = Uri.parse("content://brower/bookmarks");
-
-
资源注解
注解 释义 @StringRes 字符串资源 @ColorRes 这个对应的是颜色资源 @AnimationRes 动画资源 @DimensionRes 尺寸资源 @DimensionPixelOffsetRes 同上,为了获取尺寸资源,但这个是会尺寸资源的单位转换为像素,并且返回的是一个int型,如有小数,则全部舍去。 @DimensionPixelSizeRes 依然同上,但这个对小数的处理是四舍五入。 @BooleanRes @ColorStateListRes @DrawableRes @IntArrayRes @IntegerRes @LayoutRes @MovieRes @TextRes @TextArrayRes @StringArrayRes @UiThread 约束方法只能运行在UI线程 @MainThread 约束方法只能在主线程运行 @WorkerThread 约束方法只能在工作线程运行 @BinderThread 约束方法只能在Binder线程运行
鸣谢: