Java-注解的使用、实战

465 阅读12分钟

注解

注解的定义

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 有下面的取值

    1. ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
    2. ElementType.CONSTRUCTOR 可以给构造方法进行注解
    3. ElementType.FIELD 可以给属性进行注解
    4. ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
    5. ElementType.METHOD 可以给方法进行注解
    6. ElementType.PACKAGE 可以给一个包进行注解
    7. ElementType.PARAMETER 可以给一个方法内的参数进行注解
    8. 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筑基——深入理解注解的使用场景及实战场景 实战讲解的不错

轻松打造一个自己的注解框架 纯粹的实战 每行代码都讲解的很清楚

注解和注解处理器 也是纯实战 跟上篇文章 随便看一篇即可