- 前言
- 一、注解的分类
- 二、按照运行阶段
- 运行时注解和编译注解的区别
- 1.运行时注解
- 属性值使用注解
- 方法使用注解
- 2.编译时注解
- 注解处理器
- Processor
- Element
- 三、按照本身属性
- 1.标准注解
- 2.元注解
- 四、注解的定义和基本使用
- 1.基本定义和使用
- 2.注解成员变量的定义和使用
- 3. 使用元注解定义注解
前言
本文记录笔者对Java注解的相关学习,了解注解的相关分类;并学识如何书写注解的相关知识
一、注解的分类先说结论:
针对运行阶段,注解可分为
- 运行时注解
- 编译注解
针对本身属性,注解可分为
- 标准注解
- 元注解
-
保留阶段不同。运行时注解保留到运行时,可在运行时访问。而编译时注解保留到编译时,运行时无法访问。
-
原理不同。运行时注解是Java反射机制,Retrofit运行时注解,需要用的时候才用到,而编译时注解通过APT、AbstractProcessor。
-
性能不同。运行时注解由于使用Java反射,因此对性能上有影响。编译时注解对性能没影响。这也是为什么ButterKnife从运行时切换到了编译时的原因。
-
产物不同。运行时注解只需自定义注解处理器即可,不会产生其他文件。而编译时注解通常会产生新的Java源文件。
需要注意的是,注解虽然有作用,但注解永远是一个被动的行为
1.运行时注解使用到运行时注解的比较有代表的框架就是Android的Retrofit框架
声明运行时注解,需要用到RetentionPolicy.RUNTIME该元注解
使用运行时注解,可以完成默认值设定等一系列效果,从而提高开发效率,具体可看下方的例子
属性值使用注解首先我们设定两个相关注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface BindPort { String value() default "8080"; } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface BindAddress { String value() default "127.0.0.0"; }
通过设定元注解可以使其成为运行时注解并且具有默认值,具体的大家不懂可以看后文的例子
运行时使用
定义相关类
public class TestClass { @BindAddress() String address; @BindPort() private String port; private int number; public void printInfo() { System.out.println("info is " + address + ":" + port); } }
使用反射获取注解信息,并进行使用
//获取类 Class c = Class.forName(className); //实例化一个TestClass对象 TestClass tc= (TestClass) c.newInstance(); // 获取所有的属性 Field[] fields = c.getDeclaredFields(); for (Field field : fields) { if(field.isAnnotationPresent(BindPort.class)){ BindPort port = field.getAnnotation(BindPort.class); field.setAccessible(true); field.set(tc,port.value()); } if (field.isAnnotationPresent(BindAddress.class)) { BindAddress address = field.getAnnotation(BindAddress.class); field.setAccessible(true); field.set(tc,address.value()); } } tc.printInfo();方法使用注解
定义一个相关注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface BindGet { String value() default ""; }
定义相关类
public class TestClass { @BindAddress("http://www.google.com.cn") String address; @BindPort("8888") private String port; private int number; @BindGet("mike") void getHttp(String param){ String url="http://www.baidu.com/?username"+param; System.err.println("get------->"+url); } }
运行时使用
//获取类 Class c = Class.forName(className); TestClass tc= (TestClass) c.newInstance(); // 获取所有的方法 Method[] ms = c.getDeclaredMethods(); for (Method method : ms) { if(method.isAnnotationPresent(BindGet.class)){ BindGet bindGet = method.getAnnotation(BindGet.class); String param=bindGet.value(); method.invoke(tc, param); } }2.编译时注解
编译时注解应用一样十分广泛,除了之前提到ButterKnife,还有ARouter是通过编译时注解生成路由表,Tinker通过编译时注解生成Application的代理类。编译时注解和运行时注解定义的方式是完全一样的,不同的是它们对于注解的处理方式,运行时注解是在程序运行时通过反射获取注解然后处理的,编译时注解是程序在编译期间通过注解处理器处理的。
相关编译时注解处理流程如下
从上面的流程图我们也可以看出,编译时注解的处理过程是递归的,先扫描原文件,再扫描生成的文件,直到所有文件中都没有待处理的注解才会结束。其中扫描注解并不需要我们处理,我们需要关心的就是如何处理注解以及如何将处理后的结果写入到文件中。
注解处理器早在JDK1.5的时候就有这个功能了,只不过当时的注解处理器是apt,相关的api是在com.sun.mirror包下的。从JDK1.6开始,apt相关的功能已经包含在了javac中,并提供了新的api在javax.annotation.processing和javax.lang.model to process annotations这两个包中。旧版的注解处理器api在JDK1.7已经被标记为deprecated,并在JDK1.8中移除了apt和相关api。
ProcessorJDK中Processor类图如下,Processor就是用于处理编译器注解的类。通过继承AbstractProcessor就可以自定义处理注解。
下文我们以模拟ARouter处理过程进行说明其中的方法
- init(ProcessingEnvironment processingEnvironment):初始化方法,这个方法会被注解处理工具调用,并传入一个ProcessingEnvironment变量,这个变量非常重要,它会提供一些非常使用的工具如Elements, Filer, Messager,Types等,后面我们会单独介绍它们。
- getSupportedOptions: 这个方法允许我们自定义一些参数传给Processor,例如我们在getSupportOptions中加一个MODULE_NAME参数
@Override public SetgetSupportedOptions() { HashSet set = new HashSet<>(); set.add("MODULE_NAME"); return set; }
在gralde文件中的传入参数
javaCompileOptions { annotationProcessorOptions { arguments = [MODULE_NAME: "this module name is " + project.getName()] } }
最后在Processor的init方法中获取到相应参数即可
@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); System.out.println(processingEnvironment.getOptions().get("MODULE_NAME")); }
-
getSupportedSourceVersion:返回你目前使用的JDK版本,通常返回SourceVersion.latestSupported(),当然如果你没使用最新的JDK版本的话,也可以返回指定版本。
-
getSupportedAnnotationTypes: 这个方法用于注解的注册,只有在这个方法中注册过的注解才会被注解处理器所处理
@Override public SetgetSupportedAnnotationTypes() { Set types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); return types; }
- process(Set extends TypeElement> set, RoundEnvironment roundEnvironment):这个方法是Processor中最重要的方法,所有关于注解的处理和文件的生成都是在这个方法中完成的。它有两个参数,第一个Set extends TypeElement> set包含所有待处理的的注解,需要注意的是,如果你定义了一个注解但是没有在代码中使用它,这样是不会加到set中的。第二个参数roundEnvironment表示当前注解所处的环境,通过这个参数可以查询到当前这一轮注解处理的信息。第一个参数我们通常用不到它,最常用的是roundEnvironment中的getElementsAnnotatedWith方法,这个方法可以返回被特定注解标注的所有元素。process方法还有一个boolean类型的返回值,当返回值为true的时候表示这个Processor处理的注解不会再被后续的Processor处理。如果返回false,则表示这些注解还会被后续的Processor处理,类似拦截器模式。
Processor接口中定义了注解处理器中必要的方法,AbstractProcessor是实现Processor接口的一个抽象类,它在Processor的基础上提供了三个个注解功能,分别对应上面的三个方法。从下图中的名字也很容易看出对应的哪些方法。
添加链接描述
所有被注解标注的部分都会被解析成element,在上面介绍的process方法中,通过roundEnvironment的getElementsAnnotatedWith方法就可以获取到element的set,element既可能是类,也可能是类属性,还可能是方法,所以接下来我们还需要将element转换成对应的子类。
具体到element相关子类如下
- ExecutableElement: 可执行元素,包括类或者接口的方法。
- PackageElement: 包元素
- TypeElement:类,接口,或者枚举。
- VariableElement: 类属性,枚举常量,方法参数,局部变量或者异常参数。
- TypeParameterElement: 表示一个泛型元素
相关使用注解进行实战可以查看这篇博客
运行时注解
三、按照本身属性 1.标准注解
标准注解有以下几种:
- @Override:对覆盖超类中的方法进行标注,如果被标注的方法并没有实际覆盖超类中的方法,编译器会发错错误警告。
- @Deprecated:对不鼓励使用或已过时的方法进行标注,当开发人员对这些被标注的方法进行调用时,会显示该方法已过时的提示信息。
- @SuppressWarnings:选择性的取消特定代码段中的警告。
- @SafeVarargs:JDK 7 新增的注解,用来声明使用了可变长度参数的方法,其在与泛型类一起使用时会出现类型安全问题。
元注解是用来标注注解的注解,在注解定义时使用。有以下几种:
- @Targe:标注所修饰的对象范围。
- @Inherited:表示注解可以被继承。
- @Documented:表示注解应该被JavaDoc工具记录。
- @Retention:用来声明注解的保留策略。
- @Repeatable:JDK 8 新增的注解,允许一个注解在同一声明类型(类、属性或方法)中多次使用。
下面重点介绍下@Targe注解及@Retention注解:
@Targe注解
其中@Targe注解的取值是一个ElementType类型的数值。这里有以下几种取值,对应不用的对象范围。
- ElementType.TYPE:声明类、接口或枚举类型。
- ElementType.FIELD:声明成员变量。
- ElementType.METHOD:声明方法。
- ElementType.PARAMETER:声明参数。
- ElementType.CONSTRUCTOR:声明构造方法。
- ElementType.LOCAL_VARIABLE:声明局部变量。
- ElementType.ANNOTATION_TYPE:声明注解类型。
- ElementType.PACKAGE:声明包。
- ElementType.TYPE_PARAMETER:声明参数类型。
- ElementType.TYPE_USE:使用类型。
- ElementType.MODULE:声明模块。
@Retention注解
@Retention注解有3种类型,分别表示不同级别的保留策略。
- RetentionPolicy.SOURCE:源码级注解。注解信息只保留在.java源码中。源码在编译后,注解信息被丢弃,不会保留在.class中。
- RetentionPolicy.CLASS:编译时注解。注解信息会保留在.java源码以及.class中。当运行Java程序时,JVM会丢弃该注解信息,不会保留在JVM中。
- RetentionPolicy.RUNTIME:运行时注解。当运行Java程序时,JVM也会保留该注解信息,可以通过反射获取该注解信息。
定义新的注解类型使用@interface关键字,这与定义一个接口很像:
public @interface MyAnnotation { }
定义完成后,就可以在程序中使用注解
@MyAnnotation public class AnnotationTest { }2.注解成员变量的定义和使用
注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型:
public @interface MyAnnotation { String name(); int age(); }
上面的代码定义了name和age两个成员变量,使用该注解时就要给两个成员变量指定值:
@MyAnnotation(name = "droidYu",age = 0) public class AnnotationTest { }
也可以在定义成员变量时,用default关键字为其指定默认值:
public @interface MyAnnotation { String name() default "droidYu"; int age() default 0; }
在使用时就可以不进行赋值操作:
@MyAnnotation() public class AnnotationTest { }
定义成员变量时,有个一特殊的成员变量value,在使用时可以不用写 value = ,而直接传入value的值即可:
public @interface MyAnnotation { String name() default "droidYu"; int age() default 0; String value(); }
@MyAnnotation(value = "no value") public class AnnotationTest { }
此时value=可以省略:
@MyAnnotation("no value") public class AnnotationTest { }3. 使用元注解定义注解
定义注解时,还可以为注解添加元注解,例如使用@Target和@Retention元注解来定义只能用来注解方法的运行时注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyMethod { }
此时的MyMethod注解就只能标注在方法上,如果标注在类上,编译器就会提示错误警告,编译不能通过。
@MyAnnotation("no value") @MyMethod //这里会报错,编译不能通过 public class AnnotationTest { @MyMethod //正确的使用位置 public void method() { } }