本文目录:
- 一. 注解概念和介绍
- 二. 注解的语法
- 三. 基本注解——五大元注解
- 四. AnnotatedElement 接口
- 五. AbstractProcessor (注解处理器)
- 六. javapoet(第三方库,生成.java源文件)
- 七. APT技术
- 八. demo实战
一.注解概念和介绍
- 注解(annotation)也叫元数据。JDK1.5版本后引入的特性,它可以声明在包,类,字段,方法,局部变量,方法参数等的前面,用来对这些元素进行说明和注释。
二.注解的语法
- 注解通过@interface关键字来定义。
三.基本注解——五大元注解
@Retention, @Documented, @Target, @Inherited, @Repeatable
元注解是指可以注解到注解上的注解,即使用了@Target(ElementType.ANNOTATION_TYPE)。
五大元注解 | 介绍 |
---|---|
@Retention | 指注解保留的时长 |
@Documented | 跟文档相关,其作用是能够将注解中的元素包含到javadoc中去 |
@Target | 指定注解可作用的目标 |
@Inherited | 父类被(Inherited修饰的注解)注解修饰,它的子类如果没有任何注解修饰,就会继承父类的这个注解(看下面例子更好理解) |
@Repeatable | 表明标记的注解可以多次应用于相同的声明或类型 |
注意:@Repeatable JDK1.8以上才有,Android系统7.0(对应SDK24)以上才有
-
3.1@Retention:中文保留的意思,指注解保留的时长。
三种取值如下:
@Retention三种取值 | 介绍 | 用途 |
---|---|---|
RetentionPolicy.SOURCE | 注解只在源码阶段保留,批注将被编译器丢弃。 | 主要用于提示开发者 |
RetentionPolicy.CLASS | 注解保留到编译期(注解将由编译器记录在类文件中),但在虚拟机VM运行时不需要保留他们。这是默认的。 | 主要用于自动生成代码 |
RetentionPolicy.RUNTIME | 注解会保留到程序运行时(注解由编译器记录在类文件中,并在运行时由虚拟机VM保留),因此可以通过反射方式读取它们。 | 主要用于自动注入 |
@Retention 源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
@RetentionPolicy 源码
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
-
3.2@ Documented: 中文文献的意思,可看出跟文档相关,其作用是能够将注解中的元素包含到javadoc中去。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
-
3.3@Target: 指定注解可作用的目标
10种取值如下:
@ Target十种取值 | 作用 |
---|---|
1. ElementType.TYPE | 可作用在类、接口、枚举上 |
2. ElementType.FIELD | 可作用在属性上 |
3. ElementType.METHOD | 可作用在方法上 |
4. ElementType.PARAMETER | 可作用在方法参数上 |
5. ElementType.CONSTRUCTOR | 可作用在构造方法上 |
6. ElementType.LOCAL_VARIABLE | 可作用在局部变量上,例如方法中定义的变量 |
7.ElementType.ANNOTATION_TYPE | 可以作用在注解上 |
8.ElementType.PACKAGE | 可作用在包上 |
9. ElementType.TYPE_PARAMETER | JDK1.8才有 |
10. ElementType. TYPE_USE | JDK1.8才有 |
@Target源码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
上面看@Target源码可知其取值为一个数组,举两个例子,一个取值和两个取值以上:@Target(ElementType.ANNOTATION_TYPE),@Target({ElementType.ANNOTATION_TYPE,ElementType.PACKAGE})
ElementType 源码
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE
}
-
3.4@Inherited: 继承的意思,但并不是注解本身可被继承,而是指一个父类SuperClass被该类注解修饰,那么它的子类SubClass如果没有任何注解修饰,就会继承父类的这个注解。
例子:
@Inherited
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface Money{}
@Money
public class Father{}
public class Son extends Father {}
解释:注解Money被@Inherited修饰,Father被Money修饰,Son 继承Father(Son 上又无其他注解),那么Son 就会拥有Money这个注解。
-
3.5@ Repeatable,这个词是可重复的意思,它是java1.8引入的,算一个新特性。
什么样的注解可以多次应用来呢,通常是注解可以取多个值
例子:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @Interface Teachers{
Teacher[] value();
}
@Repeatable(Teachers.class)
public @Interface Teacher{
String role() default ""
}
@Teacher(role="Chinese")
@Teacher(role="Math")
@Teacher(role="English")
public class Me {}
public static void main(String[] args) {
if(Me .class.isAnnotationPresent(Teachers.class)) {
Teachers teaches=Man.class.getAnnotation(Teachers.class);
for(Teacher teacher : teachers.value()){
System.out.println(teacher.role());
}
}
例子解析:@Teacher被@Repeatable修饰,所以Teacher可以多次作用在同一个对象Me上,而Repeatable接收一个参数,这个参数是个容器注解,用来存放多个@Person。
四. AnnotatedElement 接口
- 该接口表示此虚拟机VM中当前正在运行的程序的带注释元素。
- 所有实现了这个接口的“元素”都是可以“被注解的元素”。
- 使用这个接口中声明的方法可以读取(通过Java的反射机制)“被注解元素”的注解。 即该接口允许以反射方式读取注解。
- 该接口中方法返回的所有注释都是不可变的和可序列化的。 调用方可以修改此接口的方法返回的数组,而不会影响返回给其他调用方的数组。
1.AnnotatedElement 方法:
AnnotatedElement 方法 | 介绍 |
---|---|
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定类型的注解出现在当前元素上,则返回true,否则将返回false。这种方法主要是为了方便地访问一些已知的注解。 |
<T extends Annotation> T getAnnotation(Class<T> annotationClass) | 如果在当前元素上存在参数所指定类型(annotationClass)的注解,则返回对应的注解,否则将返回null。 |
Annotation[] getAnnotations() | 返回在这个元素上的所有注解。如果该元素没有注释,则返回值是长度为0的数组。 |
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) | 返回与该元素相关联的注解。如果没有与此元素相关联的注解,则返回值是长度为0的数组。这个方法与getAnnotation(Class)的区别在于,该方法检测其参数是否为可重复的注解类型,如果是,则尝试通过“looking through”容器注解来查找该类型的一个或多个注解。 |
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) | 如果参数中所指定类型的注解是直接存在于当前元素上的,则返回对应的注解,否则将返回null。这个方法忽略了继承的注解。(如果没有直接在此元素上显示注释,则返回null。) |
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) | 如果参数中所指定类型的注解是直接存在或间接存在于当前元素上的,则返回对应的注解。这种方法忽略了继承的注释。如果没有直接或间接地存在于此元素上的指定注解,则返回值是长度为0的数组。这个方法和getDeclaredAnnotation(Class)的区别在于,这个方法检测它的参数是否为可重复的注释类型(JLS 9.6),如果是,则尝试通过“looking through”容器注解来查找该类型的一个或多个注解。 |
Annotation[] getDeclaredAnnotations() | 返回直接出现在这个元素上的注解。这种方法忽略了继承的注解。如果在此元素上没有直接存在的注解,则返回值是长度为0的数组。 |
2.AnnotatedElement 的实现类:
AnnotatedElement的实现类 | 介绍 | 备注 |
---|---|---|
Class | 类,天天打交道的 | \ |
Package | 包 | \ |
Parameter | 参数,主要指方法或函数的参数,其实是这些参数的类型 | JDK1.8才有 |
AccessibleObject | 是Field、Method 和 Constructor 对象的基类。所以可访问对象,如:方法、构造器、属性等 | \ |
Field | 属性,类中属性的类型 | 继承于AccessibleObject |
Executable | 可执行的,如构造器和方法 | 继承于AccessibleObject;JDK1.8才有 |
Constructor | 构造器 | 继承于Executable |
Method | 方法 | 继承于Executable |
五. AbstractProcessor (注解处理器)
-
5.1AbstractProcessor介绍
注解处理器一共有七个方法,我们一般重写四个方法
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
5.1.1 init(ProcessingEnvironment env):
每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。
5.1.2 process(Set<? extends TypeElement> annotations, RoundEnvironment env):
这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。
5.1.3 getSupportedAnnotationTypes():
这里我们必须指定,这个注解处理器是注册给哪个注解的。它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,我们在这里定义我们的注解处理器注册到哪些注解上。
5.1.4 getSupportedSourceVersion():
用来指定使用的Java版本。通常这里返回SourceVersion.latestSupported()。
-
5.2 AbstractProcessor使用(Android需新建Java Library)
5.2.1 写一个注解
5.2.2 写一个类继承于AbstractProcessor,然后重写上述四个方法
5.2.3 添加SPI配置文件(手动或者使用google提供的auto-service库)
手动配置:在该Java Library下的main目录下创建resources/META-INF/services目录,然后在 META-INF/services 目录文件夹下创建javax.annotation.processing.Processor 文件;在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;包名+自定义处理器文件名)
自定义AbstractProcessor是APT技术的顶梁柱,这里介绍了基本的知识,更多的请看相关的APT知识。
六. javapoet(square公司出品的第三方库,生成java文件)
-
6.1 javapoet介绍:
JavaPoet是用于生成.java源文件的Java API。由square公司出品的第三方库。
在执行诸如注解处理或与元数据文件(例如,数据库模式,协议格式)交互之类的操作时,源文件的生成非常有用。 通过生成代码,无需编写样板文件,同时还保留了元数据的唯一真实来源。
-
6.2 javapoet使用(Android需新建Java Library)
javapoet在AndroidStudio中不能直接使用,需要新建一个Java Library。Java Library中依赖compile 'com.squareup:javapoet:1.12.1'
下面例子MyJavapoet是写在Java Library的,运行MyJavapoet,就可在目录下看到生成的名为HelloWorld的Java文件了
public class MyJavapoet {
public static void main(String[] args) {
generateJava();
}
private static void generateJava() {
//1. 生成一个字段
FieldSpec fieldSpec = FieldSpec.builder(String.class, "var", Modifier.PUBLIC).build();
//2. 生成一个方法
MethodSpec main = MethodSpec.methodBuilder("main")//设置方法名称
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)//设置方法名称
.returns(void.class)//添加返回值
.addParameter(String[].class, "args")//添加参数
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加代码语句 (结束语句的分号不需要, 注意与CodeBlock的区别)
.build();
//3. 生成类型(enum/class/annotation/interface)
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
//4. 构建Java源文件
JavaFile javaFile = JavaFile.builder("annotation.hsj.pri", helloWorld)
.build();
//5.生成到当前module的源文件目录下
try {
javaFile.writeTo(System.out);
//输出到和用例程序相同的源码目录下,targetDirectory后面会自动添加以上述包名为文件名的目录
String targetDirectory = "annotationlib/src/main/java/";
File dir = new File(targetDirectory);
if (!dir.exists()) dir.mkdirs();
javaFile.writeTo(dir); //JavaFile.write(), 参数为源码生成目录(源码的classpath目录)
} catch (IOException e) {
e.printStackTrace();
}
}
}
javapoet的详细信息请见javapoet的GitHub:https://github.com/square/javapoet
七. APT技术
APT技术主要是通过编译期解析注解,并且生成java代码的一种技术,熟练掌握了该技术,对于ButterKnife、Dagger、EventBus里面的注解就非常容易理解了。
其三大基础为:
- 一个注解(@Retention(RetentionPolicy.CLASS));
- 一个自定义的AbstractProcessor(注解解析器);
- 一个生成Java代码的Javapoet技术。
这里限于篇幅,但又在自定义注解里非常重要的一个综合知识面,建议大家先看完 八实战demo 最后再来看APT技术。这里我就不写了,给大家推荐一篇专门讲APT的文章:https://www.jianshu.com/p/7af58e8e3e18
八. 实战demo
8.1 按作用域上分(几种常见的)
8.1.1 作用在属性上(@Target(ElementType.FIELD)),如成员变量
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MyFieldAnnotation { @IdRes int id() default -1; } --------------------------------------------------------------------------------------------- public class FieldActivity extends AppCompatActivity { @FieldAnnotation(id = R.id.tv) TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_field); field(); } private void field() { Field[] fields = this.getClass().getDeclaredFields(); //通过该方法设置所有的字段都可访问,否则即使是反射,也不能访问private修饰的字段 Field.setAccessible(fields, true); for (Field field : fields) { if (field.isAnnotationPresent(FieldAnnotation.class)) { MyFieldAnnotation fieldAnnotation = field.getAnnotation(MyFieldAnnotation.class); int id = fieldAnnotation.id(); if (id == -1) continue; View view = this.findViewById(id); Class aClass = field.getType(); try { field.set(this, aClass.cast(view)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } textView.setText("提莫队长"); } }
例子分析:
- 作用在Field上即@Target(ElementType.FIELD)
- 这里是实现一个跟ButterKnife类似的功能,即自动注入功能,无需手动调用findViewById。
第一步:定义一个注解。我们这里定义了一个运行时注解,其取值id我们作为findVIewById的id值
第二步:定义注解解析工具。例子里是通过field方法代替解析工具的。通过Field[]获取Activity的field成员变量。然后 AccessibleObject.setAccessible()设置所有字段都可以访问,这里AccessibleObject也可以改为Field,AccessibleObject是Field的父类。然后循环遍历Field[]得到每个Field,判断指定的注解MyInjectViewAnnotation是否在当前Field元素上,如果在,则取出当前Field元素上的MyInjectViewAnnotation注解,然后得到注解上的id值,然后就可以进行findViewById操作了。
第三步:使用注解。例子里申明一个Field成员变量TextView,将注解作用于 TextView 上。
8.1.2 作用在方法上(@Target(ElementType.METHOD))
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyMethodAnnotation { String name() default ""; String sex() default ""; int age() default 0; } ----------------------------------------------------------------------------------------------------------------- public class MethodActivity extends AppCompatActivity { private TextView textView; private StringBuffer stringBuffer = new StringBuffer(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_method); textView = findViewById(R.id.tv_method); method(); } @MethodAnnotation(name = "德玛", sex = "男", age = 18) public void method() { Method[] methods = this.getClass().getMethods(); AccessibleObject.setAccessible(methods, true); for (Method method : methods) { if (method.isAnnotationPresent(MethodAnnotation.class)) { MyMethodAnnotation methodAnnotation = method.getAnnotation(MyMethodAnnotation.class); stringBuffer.append("姓名:" + methodAnnotation.name() + methodAnnotation.age() + " 性别:" + " 年龄:" + methodAnnotation.sex()); Log.d("MethodActivity-->", "姓名:" + methodAnnotation.name() + " 年龄:" + methodAnnotation.age() + " 性别:" + methodAnnotation.sex()); } } textView.setText(stringBuffer); } }
疑问?这里注解所作用的方法必须为public,即使设置AccessibleObject.setAccessible(methods, true);还是无法访问private修饰的方法,这里我不太明白,有小伙伴知道的可以教教我哦。
在8.1.3 作用在方法参数上(@Target(ElementType.PARAMETER))
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.SOURCE) @StringDef({ParameterAnnotationStringA.PRIMARY_SCHOOL, ParameterAnnotationStringA.JUNIOR_SCHOOL, >ParameterAnnotationStringA.SENIOR_SCHOOL}) public @interface ParameterAnnotationStringA { String PRIMARY_SCHOOL = "小学"; String JUNIOR_SCHOOL = "初中"; String SENIOR_SCHOOL = "高中"; } --------------------------------------------------------------------------------------------------------------- @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.SOURCE) @StringDef({"小学","初中","高中"}) public @interface ParameterAnnotationStringB { } --------------------------------------------------------------------------------------------------------------- @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.SOURCE) @IntRange(from = 5, to = 20) public @interface ParameterAnnotationIntRange { } --------------------------------------------------------------------------------------------------------------- public class ParameterActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_parameter); parameterStringA("小学");//报错:即使PRIMARY_SCHOOL也是小学,但这里必须使用PRIMARY_SCHOOL和下条一样 parameterStringA(ParameterAnnotationStringA.PRIMARY_SCHOOL); parameterStringB("小学"); parameterStringB("小学生");//报错:因为ParameterAnnotationStringB没有小学生 parameterStringB(ParameterAnnotationStringA.PRIMARY_SCHOOL); parameterIntRange(15); parameterIntRange(30);//报错:因为ParameterAnnotationIntRange的IntRange是从5到20 } private void parameterStringA(@ParameterAnnotationStringA String schoolName) { Log.d("ParameterActivity-->", "parameterStringA():" + "schoolName " + schoolName); } private void parameterStringB(@ParameterAnnotationStringB String schoolName) { Log.d("ParameterActivity-->", "parameterStringB():" + "schoolName " + schoolName); } private void parameterIntRange(@ParameterAnnotationIntRange int age) { Log.d("ParameterActivity-->", "parameterIntRange():" + "age " + age); } }
例子解析:
- 打印结果依次为:小学;小学;小学;小学生;小学;15;30 ;也就是这里写代码代码的时候会提示警告信息,但编译和运行不会出现任何错误。
- 作用在方法参数上即@Target(ElementType.PARAMETER)。案例中有写@StringDef,@IntRange,这是对方法参数进行约束。
- 如果注解中有写了变量并赋值,拿ParameterAnnotationStringA注解举例,那么就参数必须写StringDef里的ParameterAnnotationStringA.PRIMARY_SCHOOL等,而不能写ParameterAnnotationStringA.PRIMARY_SCHOOL的值“小学”。
- 但是反过来,却可以使用ParameterAnnotationStringA.PRIMARY_SCHOOL,如例子里的parameterStringA(ParameterAnnotationStringA.PRIMARY_SCHOOL);
- 对第三点和第四点进行总结,即可以使用“小学”的一定可以使用ParameterAnnotationStringA.PRIMARY_SCHOOL,但可以使用ParameterAnnotationStringA.PRIMARY_SCHOOL的不一定可以使用“小学”,即变量名称大于变量的值。这个跑一下demo更直观更容易理解。
- 作用在方法参数上,这个可以常用于对参数进行约束,比如开发中会常用到生产环境地址,测试环境地址以及其他地址,为了开发者更统一规范避免地址错误,就可以对这些地址进行注解来进行约束。
8.2 按注解保留时长分(源码期,编译期,运行期)
8.2.1 源码期@Retention(RetentionPolicy.SOURCE)
demo和8.1.3一样
8.2.2 编译期@Retention(RetentionPolicy.CLASS)
编译期很重要的一个是APT技术,具体请详见上面 七.APT技术
8.2.3 运行期@Retention(RetentionPolicy.RUNTIME)
demo和8.1.1一样
运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情
参考文章
https://www.jianshu.com/p/7454a933dcaf
https://www.race604.com/annotation-processing/
https://tool.oschina.net/apidocs/apidoc?api=jdk-zh