关于java注解最完整的知识点

1,357 阅读22分钟

引言

我们经常在代码中看到各种注解,如下例子

@Override

 @Override
public void onCreate(@Deprecated  Bundle savedInstanceState)
{
    。。。
}

@Override 用来标识该方法继承自父类,在子类重写,而不是子类自己定义的方法,加上该标签,会在编译的时候帮你检查该方法是否在父类有定义,如果没有定义会报错;去掉的话编译没问题,但一旦方法签名改变(方法名或参数类型),则编译器会认为这是子类自己定义的方法。所以,对于重写自父类的方法,我们一般都会加上这个标签(编译器会自动帮我们加),阅读起来也直观。

@Deprecated

如下android sdk中被标注为@Deprecated的方法

/**
     * @deprecated use {@link #setBackground(Drawable)} instead
     */
    @Deprecated
    public void setBackgroundDrawable(Drawable background) {
        ...
    }
}

当我们调用该方法时:mChild.setBackgroundDrawable(getResources().getDrawable(resid));被标注为@Deprecated的代码会被划掉。

@Deprecated 用来标识表示此方法(或类、变量)不再建议使用,调用时会出现删除线,但并不代表不能用,只是不推荐使用,因为还有更好的方法可以调用(如上面代码的注释中会建议用setBackground(Drawable)代替)。 那为什么会出现加这个注解呢,直接在写方法的时候定义一个新的不就好了吗? 因为在一个项目中,工程比较大,代码比较多,而在后续开发过程中,可能之前的某个方法实现的并不是很合理,这个时候就要新加一个方法,而之前的方法又不能随便删除,因为可能在别的地方有调用它,所以加上这个注解,就方便以后开发人员的方法调用了。

@SuppressWarnings

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

    @SuppressWarnings("deprecation")//忽略被@Deprecated标注的警告
    private void setDrawable(int resid) {
        mChild.setBackgroundDrawable(getResources().getDrawable(resid));
    }

注解的定义与解释

初学者可以这样理解注解:想像代码具有生命,注解就是对于代码中某些鲜活个体的贴上去的一张标签。简化来讲,注解如同标签。正如我们可以给某男贴上各种标签:暖男、高富帅、直男癌、妈宝...,同样的我们也可以给 类、方法、变量贴上一张或者多张标签,也就是加一个或者多个注解。

官方解释

注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。

注解是java5引入的特性,在代码中插入一种注释化的信息,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。注解也叫元数据(meta data)。这些注解信息可以在编译期使用预编译工具进行处理(pre-compiler tools),也可以在运行期使用 Java 反射机制进行处理。

注解的作用

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

  • 生成文档,通过代码里标识的元数据生成javadoc文档。
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
  • 注解也可以单纯作为代码上的特定标识,便于开发者阅读

注解的分类

从注解的来源划分,可以分为三类:

  • JDK内置的注解,包括@Override、@Deprecated和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。
  • 元注解,元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented,@Retention,各个元注解的含义下面会详解
  • 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。

那么我们如何去定义一个注解?

注解通过 @interface关键字进行定义。

public @interface TestAnnotation {
}

它的形式跟接口很类似,不过前面多了一个@符号。上面的代码就创建了一个名字为 TestAnnotaion 的注解。

你可以简单理解为创建了一张名字为 TestAnnotation 的标签。

注解的字节码

如上定义的TestAnnotation的字节码如下:

Classfile /G:/demo/reflexDemo/out/production/reflexDemo/eft/jvm/anotation/TestAnnotation.class
  Last modified 2019-8-13; size 166 bytes
  MD5 checksum 1a9b357374ab309c2647a99ab2fe62ec
  Compiled from "TestAnnotation.java"
public interface eft.jvm.anotation.TestAnnotation extends java.lang.annotation.Annotation
  SourceFile: "TestAnnotation.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
  #1 = Class              #6              //  eft/jvm/anotation/TestAnnotation
  #2 = Class              #7              //  java/lang/Object
  #3 = Class              #8              //  java/lang/annotation/Annotation
  #4 = Utf8               SourceFile
  #5 = Utf8               TestAnnotation.java
  #6 = Utf8               eft/jvm/anotation/TestAnnotation
  #7 = Utf8               java/lang/Object
  #8 = Utf8               java/lang/annotation/Annotation
{
}

从注解的字节码可以看出:

  • 注解是一个默认继承java.lang.annotation.Annotation基类特殊接口类型,而Annotation默认继承Ojbect类
  • 注解默认带有ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION修饰符
    • 不能使用private修饰,否则编译器报错
    • 比普通接口多了ACC_ANNOTATION修饰符

注意:

  • 注解不支持继承 即使Java的接口可以实现多继承,但定义注解时依然无法使用extends关键字继承@interface
  • 且虽然其父类Annotation默认继承Object,我们依然不能重写Object里的所有方法,我们只能在注解里面定义其属性,不能做其他操作,至于如何定义注解的属性, 接下来会讲到。

注解的简单使用

上面定义的注解,没有任何限制,可以“贴”在任何类、接口、方法、全局变量、静态字段、局部变量,甚至注解的前面,就这样给它们贴上了TestAnnotation的标签

@TestAnnotation
public class AnotationTest{

    @TestAnnotation
    private String mName;
    
    @TestAnnotation
    private static final int AGE=18;
    
    @TestAnnotation
    public void test(@TestAnnotation String arg){

    }
}

元注解

什么是元注解?

元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。 元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的

比如JDK内置的注解@Override源码如下:

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

可以看到这个"标签"还贴上了@Target、@Retention的"标签",这两个就是元注解

元注解有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种

@Retention

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

它的取值如下:

  • SOURCE:注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里)

  • CLASS:注解在class文件中可用,但会被VM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中),请注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等

  • RUNTIME:注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息(源码、class文件和执行的时候都有注解的信息),如SpringMvc中的@Controller、@Autowired、@RequestMapping等。

我们可以这样的方式来加深理解,@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。

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

上面的代码中,我们指定 TestAnnotation 可以在程序运行周期被获取到,因此它的生命周期非常的长。

@Target

@Target 用来约束注解可以应用的地方(如方法、类或字段),其中ElementType是枚举类型,其定义如下,也代表可能的取值范围

public enum ElementType {
    /**标明该注解可以用于类、接口(包括注解类型)或enum声明*/
    TYPE,

    /** 标明该注解可以用于字段(域)声明,包括enum实例 */
    FIELD,

    /** 标明该注解可以用于方法声明 */
    METHOD,

    /** 标明该注解可以用于参数声明 */
    PARAMETER,

    /** 标明注解可以用于构造函数声明 */
    CONSTRUCTOR,

    /** 标明注解可以用于局部变量声明 */
    LOCAL_VARIABLE,

    /** 标明注解可以用于注解声明(应用于另一个注解上)*/
    ANNOTATION_TYPE,

    /** 标明注解可以用于包声明 */
    PACKAGE,

    /**
     * 标明注解可以用于类型参数声明(1.8新加入)//todo 
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * 类型使用声明(1.8新加入)//todo 
     * @since 1.8
     */
    TYPE_USE
}

请注意,当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,如下:

@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
@Documented

被修饰的注解会生成到javadoc中 定义如下注解


@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}

//没有使用@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentB {
}

//使用注解
@DocumentA
@DocumentB
public class DocumentDemo {
    public void A(){
    }
}

使用javadoc命令生成文档:

D:\demo\MyJvm\app\src\main\java\cn\eft\anotation>javadoc DocumentDemo.java DocumentA.java DocumentB.java

可以发现使用@Documented元注解定义的注解(@DocumentA)将会生成到javadoc中,而@DocumentB则没有在doc文档中出现,这就是元注解@Documented的作用。

@Inherited

@Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解,如下:

在注解@DocumentA加上@Inherited,@DocumentB不加:

@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}
@DocumentA
class A{ }

class B extends A{ }

@DocumentB
class C{ }

class D extends C{ }

public class DocumentDemo {
    public void A(){
    }
    
    public static void main(String... args){
        A instanceA=new B();
        System.out.println("已使用的@Inherited注解:"+Arrays.toString(instanceA.getClass().getAnnotations()));
        
        C instanceC = new D();
        
        System.out.println("没有使用的@Inherited注解:"+Arrays.toString(instanceC.getClass().getAnnotations()));
    }
}

运行结果:

已使用的@Inherited注解:[@cn.eft.anotation.DocumentA()]
没有使用的@Inherited注解:[]
@Repeatable

元注解 @Repeatable是JDK1.8新加入的,它表示在同一个位置重复相同的注解。在没有该注解前,一般是无法在同一个类型上使用相同的注解的

//Java8前无法这样使用
@FilterPath("/web/update")
@FilterPath("/web/add")
public class A {}

Java8前如果是想实现类似的功能,我们需要在定义@FilterPath注解时定义一个数组元素接收多个值如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterPath {
    String [] value();
}

//使用
@FilterPath({"/update","/add"})
public class A { }

但在Java8新增了@Repeatable注解后就可以采用如下的方式定义并使用了 什么样的注解会多次应用呢?通常是注解的值可以同时取多个。

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

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

@Repeatable(Persons.class)//@Repeatable 注解了 Person。而 @Repeatable 后面括号中的类相当于一个容器注解。
@interface Person{
	String role default "";
}

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

注意上面的代码,@Repeatable 注解了 Person。而 @Repeatable 后面括号中的类相当于一个容器注解。

什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。 我们再看看代码中的相关容器注解。

@interface Persons {
	Person[]  value();//这里规定名字就叫value()
}

按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组,注意它是数组

如果不好理解的话,可以这样理解。Persons 是一张总的标签,上面贴满了 Person 这种同类型但内容不一样的标签。把 Persons 给一个 SuperMan 贴上,相当于同时给他贴了程序员、产品经理、画家的标签。

我们可能对于 @Person(role=“PM”) 括号里面的内容感兴趣,它其实就是给 Person 这个注解的 role 属性赋值为 PM ,大家不明白正常,马上就讲到注解的属性这一块。

注解的属性

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

如下自定义的注解,带有nameage属性:

@Documented
@Inherited
@Target(ElementType.TYPE) //标明该注解可以用于类、接口(包括注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME)//运行期也保留该注解
public @interface MyAnnotation
{
    String name() default "";
    int age() default 0;
}

使用该注解,并为其属性赋值:

@MyAnnotation(name = "酸辣汤",age = 18)
public class TestAnnotation
{
    ...
}

注意:

  • 赋值的方式是在注解的括号内以 value="" 形式,多个属性之前用 ,隔开。
  • 在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组
  • 注解中属性可以有默认值,默认值需要用 default 关键值指定
  • 一个注解可以没有任何属性,使用时直接@MyAnnotation,括号省略

如果一个注解内仅仅只有一个名字为 value 的属性时,应用这个注解时可以直接接属性值填写到括号内:如

public @interface Check {
	String value();
}

使用时省略value=:

@Check("hi")
int a;

JDK内置的注解

除了引言提到的三种,再举几个例子

@SafeVarargs

参数安全类型注解。此注解告诉编译器:在可变长参数中的泛型是类型安全的。它的目的是提醒开发者不要用参数做一些不安全的操作。它是在 Java 1.7 的版本中加入的。

@SafeVarargs // Not actually safe!
static void m(List<String>... stringLists) {
    Object[] array = stringLists;
    List<Integer> tmpList = Arrays.asList(42);
    array[0] = tmpList; // Semantically invalid, but compiles without warnings
    String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime!
}

上面的代码中,编译阶段不会报错,但是运行时会抛出 ClassCastException 这个异常,所以它虽然告诉开发者要妥善处理,但是开发者自己还是搞砸了。

Java 官方文档说,未来的版本会授权编译器对这种不安全的操作产生错误警告。

@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();
}

我们进行线程开发中常用的 Runnable 就是一个典型的函数式接口,上面源码可以看到它就被 @FunctionalInterface 注解。 该注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

自定义注解

注解什么时候用?我只能告诉你,这取决于你想利用它干什么用。接下来看一个例子,这个例子是利用注解来实现对方法的测试。

自定义@Jiecha注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Jiecha {

}

被测试的类以及所有的方法:


public class NoBug {
	
	@Jiecha
	public void suanShu(){
		System.out.println("1234567890");
	}
	@Jiecha
	public void jiafa(){
		System.out.println("1+1="+1+1);
	}
	@Jiecha
	public void jiefa(){
		System.out.println("1-1="+(1-1));
	}
	@Jiecha
	public void chengfa(){
		System.out.println("3 x 5="+ 3*5);
	}
	@Jiecha
	public void chufa(){
		System.out.println("6 / 0="+ 6 / 0);
	}
	
	public void ziwojieshao(){
		System.out.println("我写的程序没有 bug!");
	}

}

测试NoBug是否有bug:


public class TestTool {

	public static void main(String[] args) {
        // TODO Auto-generated method stub
		
		NoBug testobj = new NoBug();
		
		Class clazz = testobj.getClass();
		
		Method[] method = clazz.getDeclaredMethods();
		//用来记录测试产生的 log 信息
		StringBuilder log = new StringBuilder();
		// 记录异常的次数
		int errornum = 0;
		
		for ( Method m: method ) {
			// 只有被 @Jiecha 标注过的方法才进行测试
			if ( m.isAnnotationPresent( Jiecha.class )) {
				try {
					m.setAccessible(true);
					m.invoke(testobj, null);
				
				} catch (Exception e) {
					// TODO Auto-generated catch block
					//e.printStackTrace();
					errornum++;
					log.append(m.getName());
					log.append(" ");
					log.append("has error:");
					log.append("\n\r  caused by ");
					//记录测试过程中,发生的异常的名称
					log.append(e.getCause().getClass().getSimpleName());
					log.append("\n\r");
					//记录测试过程中,发生的异常的具体信息
					log.append(e.getCause().getMessage());
					log.append("\n\r");
				} 
			}
		}
		
		
		log.append(clazz.getSimpleName());
		log.append(" has  ");
		log.append(errornum);
		log.append(" error.");
		
		// 生成测试报告
		System.out.println(log.toString());

	}

}

输出结果:

1234567890
1+1=11
3 x 5=15
1-1=0
chufa has error:
  caused by ArithmeticException
/ by zero
NoBug has  1 error.

提示 NoBug 类中的 chufa() 这个方法有异常,这个异常名称叫做 ArithmeticException,原因是运算过程中进行了除 0 的操作。

所以,NoBug 这个类有 Bug。

这样,通过注解我完成了我自己的目的,那就是对别人的代码进行测试。

注解使用场景

  • 类属性自动赋值。
  • 验证对象属性完整性。
  • 代替配置文件功能,像spring基于注解的配置。
  • 可以生成文档,像java代码注释中的@see,@param等

注解的原理

前面我们定义了一个注解:

@Documented
@Inherited
@Target(ElementType.TYPE) //标明该注解可以用于类、接口(包括注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME)//运行期也保留该注解
public @interface MyAnnotation
{
    String name() default "";
    int age() default 0;
}

查看MyAnnotation对应的字节码如下:

Classfile /D:/demo/MyJvm/app/build/intermediates/javac/debug/compileDebugJavaWit
hJavac/classes/cn/eft/anotation/MyAnnotation.class
  Last modified 2019-8-14; size 575 bytes
  MD5 checksum 8cd245eac38b9546b9c944b18350a5de
  Compiled from "MyAnnotation.java"
public interface cn.eft.anotation.MyAnnotation extends java.lang.annotation.Anno
tation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #23            // cn/eft/anotation/MyAnnotation
   #2 = Class              #24            // java/lang/Object
   #3 = Class              #25            // java/lang/annotation/Annotation
   #4 = Utf8               name
   #5 = Utf8               ()Ljava/lang/String;
   #6 = Utf8               AnnotationDefault
   #7 = Utf8
   #8 = Utf8               age
   #9 = Utf8               ()I
  #10 = Integer            0
  #11 = Utf8               SourceFile
  #12 = Utf8               MyAnnotation.java
  #13 = Utf8               RuntimeVisibleAnnotations
  #14 = Utf8               Ljava/lang/annotation/Documented;
  #15 = Utf8               Ljava/lang/annotation/Inherited;
  #16 = Utf8               Ljava/lang/annotation/Target;
  #17 = Utf8               value
  #18 = Utf8               Ljava/lang/annotation/ElementType;
  #19 = Utf8               TYPE
  #20 = Utf8               Ljava/lang/annotation/Retention;
  #21 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #22 = Utf8               RUNTIME
  #23 = Utf8               cn/eft/anotation/MyAnnotation
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/annotation/Annotation
{
  public abstract java.lang.String name();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: s#7
  public abstract int age();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#10}
SourceFile: "MyAnnotation.java"
RuntimeVisibleAnnotations:
  0: #14()
  1: #15()
  2: #16(#17=[e#18.#19])
  3: #20(#17=e#21.#22)

从该字节码文件可以看出编译器根据我们定义的属性为我们生成了无参的nameage抽象方法,方法带有AnnotationDefault属性,里面default_value就是我们设置的默认属性。至于我们声明的元注解,会被保存在RuntimeVisibleAnnotations属性中,元注解的属性会以键值对的形式保存。

为什么在类、方法上加一个注解,就可以通过getAnnotation()获取到申明的注解的值? 比如:

@MyAnnotation(name = "酸辣汤",age = 18)
public class AnnotationTest {
    public static void main(String[] args) {
        System.out.println(AnnotationTest.class.getAnnotation(MyAnnotation.class));
        MyAnnotation annotation=AnnotationTest.class.getAnnotation(MyAnnotation.class);
        System.out.println("注解属性name="+annotation.name());
        System.out.println("注解属性age="+annotation.age());
        System.out.println("注解的类型"+annotation.getClass());
    }
}

输出结果:

@eft.annotation.MyAnnotation(name=酸辣汤, age=18)
注解属性name=酸辣汤
注解属性age=18
注解的类型class com.sun.proxy.$Proxy2   //这个后面说

对于注解MyAnnotation就可以在运行时通过AnnotationTest.class.getAnnotation(MyAnnotation.class)获取注解声明的值。

  • 注解信息保存在哪儿?
  • 注解信息如何获取?

如下为AnnotationTest的字节码文件部分内容:

在AnnotationTest类被编译后,在对应的AnnotationTest.class文件中会包含一个RuntimeVisibleAnnotations属性,由于这个注解是作用在类上,所以此属性被添加到类的属性集上。即MyAnnotation注解的键值对(name=酸辣汤,age=18)会被记录起来。而当JVM加载AnnotationTest.class文件字节码时,就会将RuntimeVisibleAnnotations属性值保存到AnnotationTest的Class对象中,于是就可以通过AnnotationTest.class.getAnnotation(MyAnnotation.class)获取到MyAnnotation注解对象,进而再通过MyAnnotation注解对象获取到MyAnnotation里面的属性值。 通过字节码,我们可以看出其实注解被编译后的本质就是一个继承Annotation接口的接口,所以@MyAnnotation其实就是public interface MyAnnotation extends Annotation,当我们通过AnnotationTest.class.getAnnotation(MyAnnotation.class)调用时,JDK会通过动态代理生成一个实现了MyAnnotation接口的对象,并把将RuntimeVisibleAnnotations属性值设置进此对象中,此对象即为MyAnnotation注解对象,通过它的age()和name方法就可以获取到注解对应属性的值。

更深入的原理

从上面打印出的注解的类型,我们可以看到注解的类型是 com.sun.proxy.$Proxy2,从名字可以看出是动态生成的代理类,如何查看到该类呢?可在VM options配置-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

这样,运行的时候会将动态生成的代理类保存到项目下的这个目录

其中$Proxy2.class就是MyAnnotation所属的类 看下$Proxy2.class的结构

可见$Proxy2 实现了MyAnnotation接口,而我们知道MyAnnotation是继承Annotation的接口,Annotation类的结构如下

public interface Annotation {
    boolean equals(Object var1);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}

可见只有name()和age()是MyAnnotation自己的, 其他方法是继承而来的。$Proxy2实现了这些接口方法, 主要看看name()方法

    public final String name() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);//调用代理该对象的的m3方法,传入参数为null
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
其中
 m3 = Class.forName("cn.eft.llj.annotation.MyAnnotation").getMethod("name");
protected InvocationHandler h;    

好了,上面只是简单介绍下代理类$Proxy2.class的结构,那么这个代理类是在什么时机下生成的呢?我们从AnnotationTest.class.getAnnotation(MyAnnotation.class)进去查看,如下:

根据方法调用栈,我们可以推测,在执行getAnnotation时,会根据前面提到的常量池里的RuntimeVisibleAnnotations属性将注解的属性保存在map里,然后

综上,当我们执行

MyAnnotation annotation=AnnotationTest.class.getAnnotation(MyAnnotation.class);

这里会通过动态代理,生成$Proxy2.class(实现MyAnnotation接口)的代理对象,而当我们调用 annotation.name()其实是通过InvocationHandler的invoke方法调用了代理对象的name方法

常用注解框架

  • JUnit

JUnit 这个是一个测试框架,典型使用方法如下:

public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}

@Test 标记了要进行测试的方法 addition_isCorrect().

  • ButterKnife

ButterKnife 是 Android 开发中大名鼎鼎的 IOC 框架,它减少了大量重复的代码。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_test)
    TextView mTv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);
    }
}
  • Dagger2

也是一个很有名的依赖注入框架。

  • Retrofit

很牛逼的 Http 网络访问框架

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

当然,还有许多注解应用的地方,这里不一一列举。

注解的高级使用——APT

APT(Annotation Processing Tool),是一种在代码编译时处理注解,按照一定的规则,生成相应的java文件,多用于对自定义注解的处理。

编译期解析注解基本原理: 在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是ButterKnife等开源库的基本原理。

编译注解的核心原理依赖APT(Annotation Processing Tools)实现 (ButterKnife、Dagger、Retrofit等开源库都是基于APT)

什么是注解处理器?

注解处理器是(Annotation Processor)是javac的一个工具,用来在编译时扫描和编译和处理注解。你可以自己定义注解和注解处理器去搞一些事情。一个注解处理器它以Java代码或者(编译过的字节码)作为输入,生成文件(通常是java文件)。这些生成的java文件不能修改,并且会同其手动编写的java代码一样会被javac编译。看到这里加上之前理解,应该明白大概的过程了,就是把标记了注解的类,变量等作为输入内容,经过注解处理器处理,生成想要生成的java代码。

如果还是不是很懂没关系,先直接看下面自定义注解处理器步骤

自定义注解处理器

  1. 由于 Android 项目并不支持创建自定义的注解处理器,因此,需要引用一个外部的拥有自定义注解以及自定义注解处理器的 Jar 包 或者 把 Java 项目作为库引入,所以需要在外部创建一个纯 Java 项目,用来创建自定义注解,利用 Android Studio新建一个类型为“java library”的module

  2. 定义注解注解类Hello

package apt.eft.cn.myadt.ButterKnife;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Hello {
}
  1. 定义自己的注解处理器HelloProcessor继承自AbstractProcessor
//把自定义注解处理器 HelloProcessor 的路径提供给 JavaCompile,这里需要配置google的auto-service库
@AutoService(Processor.class) 
@SupportedAnnotationTypes("apt.eft.cn.myadt.ButterKnife.Hello")//指定该注解处理器可以解决的类型,需要完整的包名+类命
@SupportedSourceVersion(SourceVersion.RELEASE_8)//指定编译的JDK版本
public class HelloProcessor extends AbstractProcessor {
    private Messager mMessager;
    private Elements mElementUtils;
    private Filer mFiler;
    
    //提供了一个参数 processingEnvironment,利用这个参数可以获取编译时候的信息
    //processingEnvironment提供了一些工具
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        // 用来输出 Log 信息
        mMessager = processingEnvironment.getMessager();
        // 解析 Element 的工具
        mElementUtils = processingEnvironment.getElementUtils();
        // 用于生成文件
        mFiler = processingEnvironment.getFiler();
    }


    /**
    这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件
    set:请求处理得注解烈性
    roundEnvironment:有关当前和以前的信息环境
    **/
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 获取所有使用 Hello 注解的 Element
        Set<? extends Element> helloElements = roundEnvironment.getElementsAnnotatedWith(Hello.class);
        for (Element element : helloElements) {
            // 如果 Hello 注解不是作用于类,打印报错信息
            if (element.getKind() != ElementKind.CLASS) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "Hello Annotation only support class");
                return true;
            }
            // 强制转换为类元素 TypeElement
            TypeElement elementType = (TypeElement) element;
            // 利用 ElementUtils 获取包信息
            PackageElement packageName = mElementUtils.getPackageOf(elementType);

            // 用 StringBuilder 生成样板代码
            StringBuilder sb = new StringBuilder();
            sb.append("package ").append(packageName).append(";\n\n");
            sb.append("import android.content.Context;\n");
            sb.append("import android.widget.Toast;\n\n");
            sb.append("public class HelloWorld {\n\n");
            sb.append(" public static void sayHello(Context context){\n");
            sb.append(" Toast.makeText(context, \"Hello World!\", Toast.LENGTH_SHORT).show();\n");
            sb.append(" }\n");
            sb.append("}\n");

            try {
                // 用 Filer 生成 HelloWorld 文件
                JavaFileObject helloWorld = mFiler.createSourceFile(packageName + ".HelloWorld");
                Writer writer = helloWorld.openWriter();
                // 向 HelloWorld 文件写入样板代码
                writer.write(sb.toString());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    //指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6 `
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    //这里必须指定,这个注解处理器是注册给哪个注解的。
    //从这里进去的源码可以看出,会去获取@SupportedAnnotationTypes的属性作为要处理的注解
    //@return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    //getSupportedOptions() 用来提供给命令行的参数,这个可以不用复写
    @Override
    public Set<String> getSupportedOptions() {
        return super.getSupportedOptions();
    }

配置google的auto-service库依赖:

dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
}

到此,自定义注解处理器就完成了

  1. MainActivity类中添加注解hello
package cn.eft.android;

import android.app.Activity;
import android.os.Bundle;

import apt.eft.cn.myadt.ButterKnife.Hello;
@Hello
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HelloWorld.sayHello(this);//这个HelloWorld是编译器根据我们的注解处理器自动为我们生成的类,所以可以直接使用
    }
}
  1. 无需运行,直接“Rebuild Project”,可以看到编译器自动为我们在build/source/apt/debug/cn.eft.android路径下生成了HelloWord
package cn.eft.android;

import android.content.Context;
import android.widget.Toast;

public class HelloWorld {

 public static void sayHello(Context context){
 Toast.makeText(context, "Hello World!", Toast.LENGTH_SHORT).show();
 }
}
  • ButterKnife原理

ButterKnife是一个轻量级的注解框架,作用于Android视图的字段和方法、资源的绑定。

ButterKnife用的就是APT编译时解析技术,动态生成绑定事件或者控件的java代码,然后在运行的时候,直接调用bind方法完成绑定,因此你不必担心注解的性能问题。

大致原理:该框架的注解处理器类ButterKnifeProcessor继承AbstractProcessor,所以编译阶段,会通过process方法来解析所有有注解的类,保存在类似map,然后通过javapoet处理生成样板代码XXXXXX_ViewBinding.java(JavaPoet 是一个用来生成 .java源文件的Java API)。当执行ButterKnife.bind(this);方法时,会通过反射获取样板代码的实例,该样板代码的构造函数里将view与注解的关系对应起来,其实相当于该样板代码的示例帮我们写了findViewById(R.id.xxxxx)之类的代码。

总结

主要掌握以下知识点

  • 注解含义与作用
  • 元注解
  • 自定义注解
  • 注解的原理
  • APT的概念
  • 自定义注解处理器
  • 常用注解框架如Butterknife的原理

参考资源

blog.csdn.net/briblue/art…

blog.csdn.net/javazejian/…

www.cnblogs.com/aheizi/p/70…

linbinghe.com/2017/ac8515…

www.voidcn.com/article/p-y…