理论
&npsb;&npsb;&npsb;&npsb;在开发中经常使用到注解,最近在研究DDComponent插件化时又碰到了。因此,也再次深入了解了注解,记录下在学习过程中学到的一些知识。目前比较主流的框架也多数用到了注解技术,
如:ButterKnife、Dagger2、Retrofit、Glide等。
&npsb;&npsb;&npsb;&npsb;注解一般分为两种:运行时注解;编译时注解。
运行时注解:一般配合反射机制使用,相对编译时注解性能比较低,但灵活性好。例如:Retrofit用的就是运行时注解。
编译时注解:编译时注解能够自动处理java源文件,并可以根据需要生成新的文件。
基础知识点-元注解
&npsb;&npsb;&npsb;&npsb;元注解的作用是负责注解其他注解,Java5.0定义了 4个标准的meta-annotation类型,它们被
用来提供对其它 annotation类型作说明。Java5.0定义的元注解:@Target、@Retention、@Documented、@Inherited。
- @Target:说明了Annotation所修饰的对象范围,其取值为枚举类java.lang.annotation.ElementType。
取值类型有:- TYPE:Class, interface (including annotation type), or enum declaration(用于描述类、接口(包括注解类型) 或enum声明);
- FIELD:Field declaration (includes enum constants)(用于描述域)
- METHOD:Method declaration(用于描述方法);
- PARAMETER:Formal parameter declaration(用于描述参数);
- CONSTRUCTOR:Constructor declaration(用于描述构造器);
- LOCAL_VARIABLE:Local variable declaration(用于描述局部变量);
- ANNOTATION_TYPE:Annotation type declaration(用于注解类型声明);
- PACKAGE:Package declaration(用于描述包);
- TYPE_PARAMETER:Type parameter declaration(用于类型定义),since 1.8;
- TYPE_USE:Use of a type(?????),since 1.8;
- @Retention:定义了Annotation被保留的时间长短,其取值为枚举类java.lang.annotation.RetentionPolicy。
取值类型有:- SOURCE:Annotations are to be discarded by the compiler(注解仅存在于源码中,在class字节码文件中不包含);
- CLASS:默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得;
- RUNTIME:注解会在class字节码文件中存在,在运行时可以通过反射获取到;
- @Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类
的工具文档化。Documented是一个标记注解,没有成员。 - @Inherited:阐述了某个被标注的类型是被继承的,同样是一个标记注解,没有成员。
自定义注解
&npsb;&npsb;&npsb;&npsb;使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
定义注解格式:
public @interface 注解名 {定义体}
注解参数的可支持数据类型:
1. 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2. String类型
3. Class类型
4. enum类型
5. Annotation类型
6. 以上所有类型的数组
Annotation类型里面的参数该怎么设定:
第一,只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;
第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;
第三,如果只有一个参数成员,最好把参数名称设为”value”,后加小括号。
一个简单的自定义注解:1
2
3
4
5
6
7
8
9
10
11
12import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
(ElementType.FIELD)
(RetentionPolicy.RUNTIME)
public FruitName {
String value() default "";
}
注解处理器
&npsb;&npsb;&npsb;&npsb;如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。
注解处理器类库(java.lang.reflect.AnnotatedElement
&npsb;&npsb;&npsb;&npsb;Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
- Class:类定义
- Constructor:构造器定义
- Field:累的成员变量定义
- Method:类的方法定义
- Package:类的包定义
&npsb;&npsb;&npsb;&npsb;java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
&npsb;&npsb;&npsb;&npsb;AnnotatedElement接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:
T getAnnotation(Class annotationClass): 返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。 - Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
- boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。
Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。
示例程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public static void getFruitInfo(Class<?> clazz){
String strFruitName=" 水果名称:";
Field[] fields = clazz.getDeclaredFields();
for(Field field :fields){
if(field.isAnnotationPresent(FruitName.class)){
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName=strFruitName+fruitName.value();
System.out.println(strFruitName);
}
}
}&npsb;&npsb;&npsb;&npsb;以上代码是理论部分提到的运行时注解,在运行时配合反射机制使用。早期的butterknife确实是使用的动态注解的方式。可是后来,静态注解出现了,如燎原之火般席卷而来。接下来是静态注解。
Android自定义注解处理器
&npsb;&npsb;&npsb;&npsb;在Android中自定义注解处理器一般通过继承AbstractProcessor类来实现,通过process方法进行处理。需要注意的是,注解处理器只能生成新的文件,不能修改已存在的源文件。AbstractProcessor类是接口Processor类,其在Java1.7加入,用于处理编译时的注解。
自定义注解处理其主要分为6个步骤:
第一步 创建Java Library项目
首先我们新建一个Java Library项目,来作为注解处理器模块。注意是Java Library,不是Android Library。因为我们要用到的是javax包中的类,而Android Library中的JDK不包含这些类。
第二步 新建自定义注解
1 |
|
第三步 新建自定义注解处理器
1 | public class RouteNodeProcessor extends AbstractProcessor { |
通过新建的AutowiredProcessor,知道要实现抽象类
AbstractProcessor的process方法,在这个方法中处理注解与生成新文件的。另外需要注意的是:
1.需要配置注解处理器支持处理的注解;2.需要指定支持java的源码版本,这两点我们可以通过Override
抽象类AbstractProcessor的getSupportedAnnotationTypes()和getSupportedSourceVersion()方法类配置,或者通过注解的形式来配置,这个稍后再升级下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class RouteNodeProcessor extends AbstractProcessor {
/**
* 该处理器支持的所有注解类集合,在这里可以添加自定义注解
*/
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new HashSet<>();
// 添加自定义注解
set.add(RouteNode.class.getCanonicalName());
return set;
}
/**
* 该处理器支持的JDK版本,例如:SourceVersion.RELEASE_7
* 一般返回SourceVersion.latestSupported()
*/
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
第四步 处理process()方法,并生成文件
1 |
|
通过以上我们自定义注解处理器已经写好了,下一步就是如何集成到具体项目中。在这里我们通过StringBuilder的方式一点一点来拼写Java代码,较为繁琐还容易写错,目前有第三方的开源库来处理java代码的生成,如:javapoet。
第五步 声明自定义注解处理器
在使用前,还需要声明注解处理器,也即广而告之当前项目下有那些注解处理器。
其声明主要在 main目录 下,新建 resource/META-INF/services目录,并在目录下新建一个 javax.annotation.processing.Processor 文件,这个也可以通过注解的形式来配置,这个稍后再升级下。
在文件中使用文本声明已经编写好的注解处理器,每个注解处理器各占一行。
最后执行 Make Project,就可以在build目录下看到生成好的jar包。可以直接拿jar包集成到项目中或者依赖该项目亦可。
第六步 集成到项目中
通过将生成的jar包放到项目的libs中或者使项目依赖注解处理器的项目,就可以使用注解处理器了,执行Make Project,就会在项目的build/generated/source/apt/debug下看到通过注解处理器生成的java文件。
如果没有生成java文件,请检查注解器的声明是否写错。
总结
通过上面自定义注解处理器已经可以使用了,其主要是通过继承抽象AbstractProcessor类并实现process方法,同时配置注解器支持的java版本和注解,最后对外声明注解器本身在哪里即可。用注解处理器的好处是可以自动生成一些重复大量的代码,并且能让类变得干净、逻辑清晰。
升级
在第三步和第五步讲到可以通过注解的形式来配置注解器支持的java版本、注解和声明,为何要这样做呢,主要是在声明的时候容易人为的输入容易出错而且麻烦。使用注解配置前,需要引入注解所在的jar
包(com.google.auto.service:auto-service)。1
2
3dependencies {
implementation ‘com.google.auto.service:auto-service:1.0-rc2’
}
注意auto-service的不同版本(存在兼容性),然后通过注解来配置和声明:1
2
3
4
5
6
7
8
9//声明当前类为是注解处理器,Processor.class为静态注解入口也是一个接口
(Processor.class)
//支持的注解
"com.mugwort.annotation.RouteNode") (
//支持的java版本
(SourceVersion.RELEASE_7)
public class RouteNodeProcessor extends AbstractProcessor {
......
}
注意 这里有一点不好就是注解的支持只能手写。
调试注解器
编写代码不可避免会出现错误的,如何定位错误则需要调试的,而注解器如何调试呢?调试有两种方式:日志定位;断点调试;
日志定位,繁琐但方便快捷:
1
2
3
4// 1. sout
System.out.println(xxx)
// 2. messager
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");断点调试:
注解器的断点调试比较麻烦点,不像java那样,其主要分为三个步骤。- 配置debug后台服务,在在gradle.properties文件中加入下面两句话,然后sync一下项目(或者在控制台执行./gradlew –daemon),会开启一个远程debug_server。
1
2org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005配置Remote Debugger,在AS中创建一个Remote Debugger。
执行编译过程,在需要的位置打开断点,在控制台输入
1
./gradlew assembleDebug
或者,待清除功能的编译1
./gradlew clean assembleDebug
配置没问题的话就会走到打开的断点,然后就可以调试了。