注解

272 阅读11分钟

什么是注解

Annotation 中文译过来就是注解的意思

注解也叫元数据,例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。

形象点描述:注解就如同标签。

标签是对事物行为的某些角度的评价与解释。

注解就是对于代码中某些鲜活个体的贴上去的一张标签。对代码没有直接的影响,仅仅只是一个标记,注解之所以起作用是对其解析后做了相应的处理。

注解作用

它主要的作用有以下四方面:

  • 生成文档,通过代码里标识的元数据生成javadoc文档。
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例

注解详解

元注解

元注解用于修饰注解的注解,通常用在注解的定义上。

形象点描述:元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的。

由比如元注解相当于基本数据类型,java对数据对操作最终也是在操作基本数据类型,依赖基本数据类型

元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。

Retention

Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。

它的取值如下:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。

@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}

Documented

注在其他的注解A上,A将会作为Javadoc产生的文档中的内容。注解都默认不会成为成为文档中的内容。

Target

@arget 是目标的意思,@Target 指定了注解运用的地方。

类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。

@Target 有下面的取值

  • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
  • ElementType.CONSTRUCTOR 可以给构造方法进行注解
  • ElementType.FIELD 可以给属性进行注解
  • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
  • ElementType.METHOD 可以给方法进行注解
  • ElementType.PACKAGE 可以给一个包进行注解
  • ElementType.PARAMETER 可以给一个方法内的参数进行注解
  • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

Inherited

Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}


@Test
public class A {}


public class B extends A {}


注解 Test 被 @Inherited 修饰,之后类 A 被 Test 注解,类 B 继承 A,类 B 也拥有 Test 这个注解。

@Repeatable

Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。

什么样的注解会多次应用呢?通常是注解的值可以同时取多个。

举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。


@interface Persons {
	Person[]  value();
}

@Repeatable(Persons.class)
@interface Person{
	String role default "";
}

@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
	
}

自定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
	//修饰符只能是public或者不写
	public int id();
	//可以使用default设置默认值
	String msg() default "hhh";

}

ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

RetentionPolicy.RUNTIME 注解保留到运行期

自定义Annotation若只有1个参数,使用value().

注解的属性

注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。

注解参数支持的数据类型

  • 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
  • String类型
  • Class类型
  • enum类型
  • Annotation类型
  • 以上所有类型的一维数组

自定义Annotation的注解参数的默认值

注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。

示例:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnotherAnnotation{
    String author() default "";
    int age() default 0;
}

使用的自定义注解

@AnotherAnnotation(author = "fanye", age = 17)
public class AnnotationTestClass{
    ***
}

内置注解

@Deprecated

这个元素是用来标记过时的元素,想必大家在日常开发中经常碰到。编译器在编译阶段遇到这个注解时会发出提醒警告,告诉开发者正在调用一个过时的元素比如过时的方法、过时的类、过时的成员变量。

@Override

提示子类要复写父类中被 @Override 修饰的方法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。

@SuppressWarnings

阻止警告的意思。之前说过调用被 @Deprecated 注解的方法后,编译器会警告提醒,而有时候开发者会忽略这种警告,他们可以在调用的地方通过 @SuppressWarnings 达到目的。

@SuppressWarnings("deprecation")
public void test1(){
	Hero hero = new Hero();
	hero.say();
	hero.speak();
}

@SafeVarargs

是在 Java 1.7 的版本中加入的

参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。

@FunctionalInterface

函数式接口注解,这个是 Java 1.8 版本引入的新特性。函数式编程很火,所以 Java 8 也及时添加了这个特性。

函数式接口 (Functional Interface) 就是一个具有一个方法的普通接口。

例如:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}


函数式接口可以很容易转换为 Lambda 表达式。

例如:

interface GreetingService {
      void sayMessage(String message);
   }
   
   public static void main(String args[]){
  
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
   }
   

注解原理及解析

所有的注解类型都继承自这个普通的接口(Annotation)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

本质上就是:

public interface Override extends Annotation{
    
}

注解只是一种注释,需要解析它的代码,才会起作用

解析一个注解有两种形式,一种是运行期反射解析,一种是编译期直接的直接扫描

典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。如果不是那就报错

解析注解

形象的比喻就是你把这些注解标签在合适的时候撕下来,然后检阅上面的内容信息,并做出某些行为。

运行期注解的解析

运行时 Annotation 指 @Retention 为 RUNTIME 的 Annotation

运行期注解的解析通过是反射的方式,Class相关方法如下:

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
判断它是否应用了某个注解

public A getAnnotation(Class annotationClass) {}
获取 Annotation 对象,返回注解到这个元素上的所有注解。

public Annotation[] getAnnotations() {} 获取 Annotation 对象,返回注解到这个元素上的所有注解。

举个例子:

定义Test注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
	
	int id() default -1;
	
	String msg() default "";

}

使用注解,并通过反射解析注解

@Test(msg="hhh")
public class AnnotationTest {
	
	public static void main(String[] args) {
		
		boolean hasAnnotation = AnnotationTest.class.isAnnotationPresent(Test.class);
		
		if ( hasAnnotation ) {
			Test testAnnotation = AnnotationTest.class.getAnnotation(Test.class);
			
			System.out.println("id:"+testAnnotation.id());
			System.out.println("msg:"+testAnnotation.msg());
		}

	}

}

它们的结果如下:

id:-1
msg:hhh

原理分析

AnnotationTest.class.getAnnotation(Test.class)获取注解声明的值,从上面的句子就可以看出,它是从class结构中获取出AnnotationTest注解的,所以肯定是在某个时候注解被加入到class结构中去了。

@TestAnnotation()
public class AnnotationTest {
}

从java源码到class字节码是由编译器完成的,编译器会对java源码进行解析并生成class文件,而注解也是在编译时由编译器进行处理,编译器会对注解符号处理并附加到class结构中,根据jvm规范,class文件结构是严格有序的格式,唯一可以附加信息到class结构中的方式就是保存到class结构的attributes属性中。我们知道对于类、字段、方法,在class结构中都有自己特定的表结构,而且各自都有自己的属性,而对于注解,作用的范围也可以不同,可以作用在类上,也可以作用在字段或方法上,这时编译器会对应将注解信息存放到类、字段、方法自己的属性上。

在我们的AnnotationTest类被编译后,在对应的AnnotationTest.class文件中会包含一个RuntimeVisibleAnnotations属性,由于这个注解是作用在类上,所以此属性被添加到类的属性集上。即Test注解的键值对id=-1、msg="hhh"会被记录起来。而当JVM加载AnnotationTest.class文件字节码时,就会将RuntimeVisibleAnnotations属性值保存到AnnotationTest的Class对象中,于是就可以通过AnnotationTest.class.getAnnotation(Test.class)获取到Test注解对象,进而再通过Test注解对象获取到Test里面的属性值。

这里可能会有疑问,Test注解对象是什么?其实注解被编译后的本质就是一个继承Annotation接口的接口,所以@Test其实就是“public interface Test extends Annotation”,当我们通过AnnotationTest.class.getAnnotation(Test.class)调用时,JDK会通过动态代理生成一个实现了Test接口的对象,并把将RuntimeVisibleAnnotations属性值设置进此对象中,此对象即为Test注解对象,通过它的id()方法就可以获取到注解值。

参考:注解机制及其原理

那么通过动态代理生成实现了Test接口的对象具体说怎么样的呢?

动态代理

  • $Proxy0就是通过 Proxy 动态生成的。
  • $Proxy0实现了要代理的接口。
  • $Proxy0通过调用 InvocationHandler来执行任务。

通过Proxy.Proxy.newProxyInstance拿到动态代理,因为动态代理实现了代理接口方法,所以可以调用代理接口拿到最终数据

详情见轻松学,Java 中的代理模式及动态代理

注解生成的动态代理

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。

memberValues是一个hashmap,这个map以key(注解方法名)—value(注解方法对应的值)

分析见Java注解(Annotation)原理详解

编译期注解的解析

APT

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。 简单来说就是在编译期,通过注解生成.java文件。

所以说编译期注解的解析,由Annotation Processor 注解处理器完成

由于篇幅问题,独立开一篇文章,详情见APT详解

扩展

动态代理和静态代理

注解在class字节码里的结构

参考:秒懂,Java 注解 (Annotation)你可以这样学