注解
注解的定义
Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。
注解即标签
如果把代码想象成一个具有生命的个体,注解就是给这些代码的某些个体打标签。
注解的本质
oracle 官网对注解的定义为:
Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
注解是元数据的一种形式,它提供有关程序的数据,该数据不属于程序本身。 注解对其注释的代码操作没有直接影响。
从这个定义里我们可以看出,首先注解携带的是元数据,其次,它可能会引起一些和元数据相关的操作,但不会对被注释的代码逻辑产生影响。
而在JDK的Annotation接口中有一行注释如此写到:
/**
* The common interface extended by all annotation types.
* ...
*/
public interface Annotation {...}
复制代码
这说明其他注解都扩展自 Annotation 这个接口,也就是说注解的本质就是一个接口。
Java中常见3个注解
Java 内置注解
Java 中有三个常用的内置注解,其实相信大家都用过或者见过。不过在了解了注解的真实面貌以后,不妨重新认识一下吧!
@Override
它的定义为:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
复制代码
可见这个注解没有任何取值,只能修饰方法,而且RetentionPolicy 为 SOURCE,说明这是一个仅在编译阶段起作用的注解。
它的真实作用想必大家一定知道,就是在编译阶段,如果一个类的方法被 @Override 修饰,编译器会在其父类中查找是否有同签名函数,如果没有则编译报错。可见这确实是一个除了在编译阶段就没什么用的注解。
@Deprecated
它的定义为:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
复制代码
这个注解也没有任何取值,能修饰所有的类型,永久存在。这个注解的作用是,告诉使用者被修饰的代码不推荐使用了,可能会在下一个软件版本中移除。这个注解仅仅起到一个通知机制,如果代码调用了被@Deprecated 修饰的代码,编译器在编译时输出一个编译告警。
@SuppressWarnings
它的定义为:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* The set of warnings that are to be suppressed by the compiler in the
* annotated element. Duplicate names are permitted. The second and
* successive occurrences of a name are ignored. The presence of
* unrecognized warning names is <i>not</i> an error: Compilers must
* ignore any warning names they do not recognize. They are, however,
* free to emit a warning if an annotation contains an unrecognized
* warning name.
*
* <p> The string {@code "unchecked"} is used to suppress
* unchecked warnings. Compiler vendors should document the
* additional warning names they support in conjunction with this
* annotation type. They are encouraged to cooperate to ensure
* that the same names work across multiple compilers.
* @return the set of warnings to be suppressed
*/
String[] value();
}
复制代码
这个注解有一个字符串数组的值,需要我们使用注解的时候传递。可以在类型、属性、方法、参数、构造函数和局部变量前使用,声明周期是编译期。
这个注解的主要作用是压制编译告警的。
例如
public static void main(String[] args) {
Date date = new Date(2020, 5, 22);
}
复制代码
我们可以看到,Date 的这个构造函数是被@Deprecated 修饰的:
@Deprecated
public Date(int year, int month, int date) {
this(year, month, date, 0, 0, 0);
}
复制代码
所以上面的代码在编译时会报一个Warning:
java: java.util.Date 中的 Date(int,int,int) 已过时
复制代码
为了不让编译器输出这个 Warning, 就需要在上述的 main 方法前面增加一个 @SuppressWarnings 注解:
@SuppressWarning(value = "deprecated")
public static void main(String[] args) {
Date date = new Date(2020, 5, 22);
}
复制代码
注解输入的字符串deprecated 表明让编译器忽略 @Deprecated注解引发的编译告警。
获取注解的数据
不忘初心,牢记使命。
还记得我们为什么要搞个注解出来么?元-数-据!
注解本质上是要给代码带来数据的。
前面我们已经看到,数据是通过注解的成员变量(很多时候是用 value() )来存储的,那么数据来了,该如何使用呢?
首先很显然,如果我们的代码需要在业务中使用注解传递的元数据,那么这个注解的一定是RUNTIME 的,否则数据在编译或者类加载阶段就被丢弃了。
那么我们该如何获取到注解所携带的数据的呢?答案是:通过 反射。
首先我们看一下 Java 反射中经常用的那么几个类(Class, Method, Field)的定义:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
...
}
复制代码
可以看到 Class 类实现了 AnnotatedElement 这个接口。
public final class Method extends Executable {
...
}
复制代码
class Field extends AccessibleObject implements Member {
...
}
复制代码
而 Executable 继承自AccessibleObject, AccessibleObject 则是实现了**AnnotatedElement**!
盲生,你发现华点了吗!
这些反射中常用的类,都实现了 AnnotatedElement 这个接口!
那我们来看下 AnnotatedElement 这个接口都定义了哪些方法 (jdk 1.8):
public interface AnnotatedElement {
// 是否有注解
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
...
}
// 获取指定类型的注解
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// 获取所有注解
Annotation[] getAnnotations();
// 根据类型获得注解
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
...
}
// 获取声明的注解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
...
}
// 通过类型获取声明的注解
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
...
}
// 获取声明注解列表
Annotation[] getDeclaredAnnotations();
}
复制代码
可以看到,这个接口中都是获取注解的方法!
那么接下来就让我们动动双手,写一个基于注解的 Hello World 吧!
首先自定义一个注解,注意 Retention 设置为 RUNTIME:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target(METHOD)
@Retention(RUNTIME)
public @interface TestAnno {
String value() default "abc";
}
复制代码
我们这个注解时修饰方法的,并且有一个 value 的属性值,默认为 "abc"。
然后写一个 main 函数:
import java.lang.reflect.Method;
public class AnnotationTest {
@TestAnno("Hello World")
public static void main(String[] args) {
try {
Class cls = AnnotationTest.class;
Method method = cls.getMethod("main", String[].class);
TestAnno anno = method.getAnnotation(TestAnno.class);
System.out.println(anno.value());
} catch (Exception ignore) {}
}
}
复制代码
首先,我们通过反射的方式,获取 main 函数的 Method 对象;
然后调用 Method 对象的 getAnnotation 方法,入参为 TestAnno 的类型;
这样就可以拿到我们在 main 函数上面写的那个注解了,然后调用 value 函数即可获取 "Hello World":
Hello World
Process finished with exit code 0
复制代码
看起来大功告成是不是?但是,等等!好像哪里不对啊!
前面我们讲过,注解的本质是什么来着?接口啊!一个接口又没有被实现,我们是怎么通过调用它的 value() 方法,获取到 "Hello World" 的呢?
这种不写实现,而在运行时通过某种机制自动实现的方式,那些熟悉 mybatis 的同学,是不是有点面熟呢?没错!就是 动-态-代-理!
如何自定义注解
- 注解通过 @interface关键字进行定义。
public @interface Test {
}
它的形式跟接口很类似,不过前面多了一个 @ 符号。上面的代码就创建了一个名字为 Test 的注解。 你可以简单理解为创建了一张名字为 Test的标签。
- 使用注解
@Test
public class TestAnnotation {
}
创建一个类 TestAnnotation,然后在类定义的地方加上 @Test就可以用 Test注解这个类了
你可以简单理解为将 Test 这张标签贴到 TestAnnotation这个类上面。
元注解
元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
如果难于理解的话,你可以这样理解。元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的。
元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
- @Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
SOURCE表示注解编译时可见,编译完后就被丢弃。这种注解一般用于在编译器做一些事情;CLASS表示在编译完后写入 class 文件,但在类加载后被丢弃。这种注解一般用于在类加载阶段做一些事情;RUNTIME则表示注解会一直起作用。
-
@Target
Target 是目标的意思,@Target 指定了注解运用的地方 你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。 类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE //接口、类、枚举
-
@Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
-
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。(感受不到使用场景)
-
@Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
注解的属性
注解的属性也叫做成员变量。注解只有成员变量,没有方法。 需要注意的是,在注解中定义属性时它的类型必须是 8 种基本数据类型(byte、short、int、long、float、double、boolean、char)外加String、 类、接口、注解及它们的数组 注解中属性可以有默认值,默认值需要用 default 关键值指定
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{
int id() default -1;
String msg() default "Hello";
}
上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。 赋值的方式是在注解的括号内以 value="" 形式,多个属性之间用 ,隔开
@Test(id=1,msg="hello annotation")
public class TestAnnotation {
}
注解的提取
注解与反射。 注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
然后通过 getAnnotation() 方法来获取 Annotation 对象。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
或者是 getAnnotations() 方法。
public Annotation[] getAnnotations() {}
前一种方法返回指定类型的注解,后一种方法返回注解到这个元素上的所有注解。
如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如
@Test()
public class TestDemo{
public static void main(String[] args) {
boolean hasAnnotation = TestDemo.class.isAnnotationPresent(Test.class);
if ( hasAnnotation ) {
TestAnnotation testAnnotation = TestDemo.class.getAnnotation(Test.class);
System.out.println("id:"+testAnnotation.id());
System.out.println("msg:"+testAnnotation.msg());
}
}
}
元注解的实例
- @Inherited的例子
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TuHao {
String value() default "土豪";
}
@TuHao
public class Lance {
private String name;
public Lance(String name){
this.name = name;
}
}
public class ChildLance extends Lance {
public ChildLance(String name) {
super(name);
}
public static void main(String... args) {
Class<ChildLance> childLanceClass = ChildLance.class;
if (childLanceClass.isAnnotationPresent(TuHao.class)) {
TuHao annotation = childLanceClass.getAnnotation(TuHao.class);
String value = annotation.value();
System.out.println("xxxx"+value);
}
}
}
xxxx土豪
- @Repeatable的例子
@interface Persons{
Person[] value();
}
@Repeatable(Persons.class)
@interface Person{
String role() default "";//八种基本的类型 Class 注解
// Lance test();
}
@Person(role = "第一骚")
@Person(role = "闷骚")
@Person(role = "温文雅尔")
public class Human {
private String name ="Av";
}
什么是APT
APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码
annotationProcessor和android-apt的功能是一样的,他们是替代关系,在认识他们之前先来看看APT,annotationProcessor是APT工具的一种,他是google开发的内置框架,不需要引入
APT的处理要素
注解处理器(AbstractProcess)+代码处理(javaPoet)+处理器注册(AutoService)+apt(annotationProcessor )
annotation是定义注解 api是定义接口 app是使用地方 processor是注解扫描程序
(aapt2 是编译成资源的)
bindview的实例(有实际的demo但是怎么生成我还没弄明白 后续如果有机会研究)
注解的使用场景
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息 减少重复且易出错的样板代码
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取 值得注意的是,注解不是代码本身的一部分。
参考文章
注解三种场景更全面的讲解 首推!
Java Annotaions (注解)的本质和实现原理(上) 值得推荐 注解的本质讲解的很清楚!(有上下2篇文章)
Android筑基——深入理解注解的使用场景及实战场景 实战讲解的不错
轻松打造一个自己的注解框架 纯粹的实战 每行代码都讲解的很清楚
注解和注解处理器 也是纯实战 跟上篇文章 随便看一篇即可