java注解研究

简介

Spring框架经常用到两种注入方式:一种是注解,比如@Controller,@Service,@Repository,另一种是XML。采用注解可以提供更大的便捷性,易于维护修改,但耦合度高,而 XML 相对于注解则是相反的。两者各有优劣,本文主要讲注解。

例子

/**
 * 注解接口
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    int set() default 0;
}
@MyAnnotation(set=12)
public class TestAnnotation {
    public static void main(String[] args) {
        // 检查TestAnnotation类是否有注解
        if (TestAnnotation.class.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation = (MyAnnotation) TestAnnotation.class.getAnnotation(MyAnnotation.class);
            System.out.println(annotation.set());
        }
    }
}
输出:
12
注解本质

java.lang.annotation.Annotation接口

//The common interface extended by all annotation types
public interface Annotation

注释说明了:所有的注解类型都继承自这个普通的接口(Annotation)
比如上面的MyAnnotation,它本质上就是

public interface MyAnnotation extends Annotation{   
}
注解语法

通过上面的例子:
注解通过 @interface 关键字进行定义。

public @interface MyAnnotation{
}

它的形式跟接口很类似,不过前面多了一个 @ 符号。上面的代码就创建了一个名字为 MyAnnotation的注解。
不过这样注解还不能工作,还需要一些标识告诉编译器这个注解用在什么地方,以及它的生命周期等。这些标识就是元注解。

元注解

元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
元标签有@Retention、@Documented、@Target、@Inherited、@Repeatable5 种。
(1)@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
RetentionPolicy.SOURCE:只在本编译单元的编译过程中保留,并不写入Class文件中。这种注解主要用于在本编译单元(这里一个Java源码文件可以看作一个编译单元)内触发注解处理器(annotation processor)的相关处理,例如说可以让注解处理器相应地生成一些代码,或者是让注解处理器做一些额外的类型检查,等等。例如:@Override@SuppressWarnings
RetentionPolicy.CLASS:在编译的过程中保留并且会写入Class文件中,但是JVM在加载类的时候不需要将其加载为运行时可见的(反射可见)的注解。这里很重要的一点是编译多个Java文件时的情况:假如要编译A.java源码文件和B.class文件,其中A类依赖B类,并且B类上有些注解希望让A.java编译时能看到,那么B.class里就必须要持有这些注解信息才行。同时我们可能不需要让它在运行时对反射可见(例如说为了减少运行时元数据的大小之类),所以会选择CLASS而不是RUNTIME。
RetentionPolicy.RUNTIME:在编译过程中保留,会写入Class文件,并且JVM加载类的时候也会将其加载为反射可见的注解。这就不用多说了,例如说Spring的依赖注入就会在运行时通过扫描类上的注解来决定注入啥。

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

上面的代码中,我们指定 MyAnnotation可以在程序运行周期被获取到,因此它的生命周期非常的长。
(2)@Documented
当我们执行 JavaDoc 文档打包时会被保存进 doc 文档。
(3)@Target
Target 是目标的意思,@Target指定了注解运用的地方。
@Target 有下面的取值

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

(4)@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 这个注解。
(5)@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{

}

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

Class 类中提供了以下一些方法用于反射注解

getAnnotation:返回指定的注解
isAnnotationPresent:判定当前元素是否被指定注解修饰
getAnnotations:返回所有的注解
getDeclaredAnnotation:返回本元素的指定注解
getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的

注解原理

Debug上面的代码,


可以看出MyAnnotation本质上通过由JDK生成动态代理类来进行调用的
main方法加入

public static void main(String[] args) {
    //把生成的代理字节码写到com/sun/proxy包下
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 检查TestAnnotation类是否有注解
        if (TestAnnotation.class.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation = (MyAnnotation) TestAnnotation.class.getAnnotation(MyAnnotation.class);
            System.out.println(annotation.set());
        }
    }

com\sun\proxy目录下生成$Proxy1.class
反编译以后

public final class $Proxy1
  extends Proxy
  implements MyAnnotation
{
  private static Method m1;
  private static Method m2;
  private static Method m3;
  private static Method m4;
  private static Method m0;
  
  public $Proxy1(InvocationHandler paramInvocationHandler)
    throws 
  {
    super(paramInvocationHandler);
  }
  
  public final boolean equals(Object paramObject)
    throws 
  { 
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); 
  }
  
  public final String toString()
    throws 
  {
          return (String)this.h.invoke(this, m2, null); 
  }
  
  public final int set()
    throws 
  { 
      return ((Integer)this.h.invoke(this, m3, null)).intValue();  
  }
  
  public final Class annotationType()
    throws 
  {    
      return (Class)this.h.invoke(this, m4, null);  
  }
  
  public final int hashCode()
    throws 
  {  
      return ((Integer)this.h.invoke(this, m0, null)).intValue();   
  }
  
  static
  {  
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m3 = Class.forName("com.huige.MyAnnotation").getMethod("set", new Class[0]);
      m4 = Class.forName("com.huige.MyAnnotation").getMethod("annotationType", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;  
  }
}

从上面可以看出我们自定义的注解HelloAnnotation是一个接口,而$Proxy1这个Java生成的动态代理类就是它的实现类

InvocationHandler

既然MyAnnotation具体是通过Proxy1来调用的,那Proxy1调用最终会传递给绑定的InvocationHandler实例的invoke方法处理,那InvocationHandler是谁呢?


从上图可以看出java提供了AnnotationInvocationHandler来处理注解
跟踪进去
最终调用sun.reflect.annotation.AnnotationInvocationHandler#invoke方法

 public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }

看看invoke方法是如何处理我们annotation.set()方法的调用的
debug进去


上图可以看出,在default环节进行获取值的,明显memberValues是一个LinkedHashMap,set方法的返回值是从一个LinkedHashMap中获取到的。这个LinkedHashMap以key(注解方法名)—value(注解方法对应的值)存储

那memberValues这个Map对象是怎么生成的,继续调试
通过方法调用栈找到memberValues的本源
调用栈如下:


可以看出在执行TestAnnotation.class.isAnnotationPresent(MyAnnotation.class)时就生成memberValues
具体创建LinkedHashMap是在sun.reflect.annotation.AnnotationParser#parseAnnotation2方法中

private static Annotation parseAnnotation2(ByteBuffer var0, ConstantPool var1, Class<?> var2, boolean var3, Class<? extends Annotation>[] var4) {
       //。。省略
            LinkedHashMap var10 = new LinkedHashMap(var8.memberDefaults());
          //。。。省略:var10就是LinkedHashMap 
            return annotationForMap(var6, var10);
        }
    }

具体获取值在sun.reflect.annotation.AnnotationParser#parseConst方法中


即从运行时常量池地址为30中取出12,
最后调用AnnotationInvocationHandler构造器创建AnnotationInvocationHandler对象,把已经生成的map赋值给memberValues

番外篇

上面提到MyAnnotation继承Annotation接口,翻了源码没发现在哪里继承了,到底是什么回事呢?
我们查看下MyAnnotation的字节码

D:\project\src\main\java\com\huige>javap -verbose MyAnnotat
ion.class
Classfile /D:/project/src/main/java/com/huige/MyAnnotation.
class
  Last modified 2019-1-16; size 439 bytes
  MD5 checksum c1dd0a0bf103c12955a919a57ce48806
  Compiled from "MyAnnotation.java"
public interface com.huige.MyAnnotation extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #18            // com/huige/MyAnnotation
   #2 = Class              #19            // java/lang/Object
   #3 = Class              #20            // java/lang/annotation/Annotation
   #4 = Utf8               set
   #5 = Utf8               ()I
   #6 = Utf8               AnnotationDefault
   #7 = Integer            0
   #8 = Utf8               SourceFile
   #9 = Utf8               MyAnnotation.java
  #10 = Utf8               RuntimeVisibleAnnotations
  #11 = Utf8               Ljava/lang/annotation/Target;
  #12 = Utf8               value
  #13 = Utf8               Ljava/lang/annotation/ElementType;
  #14 = Utf8               TYPE
  #15 = Utf8               Ljava/lang/annotation/Retention;
  #16 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #17 = Utf8               RUNTIME
  #18 = Utf8               com/huige/MyAnnotation
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/annotation/Annotation
{
  public abstract int set();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#7}
SourceFile: "MyAnnotation.java"
RuntimeVisibleAnnotations:
  0: #11(#12=[e#13.#14])
  1: #15(#12=e#16.#17)

从上面的字节码可以看出
(1)MyAnnotation继承了Annotation的接口
(2)虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:

RuntimeVisibleAnnotations:记录所有运行时可见的Annotation
RuntimeInVisibleAnnotations:记录所有运行时不可见的注解RuntimeVisibleParameterAnnotations:记录所有运行时可见的方法参数注解
RuntimeInVisibleParameterAnnotations:记录所有运行时不可见的方法参数注解
AnnotationDefault:注解类元素的默认值

所以字节码倒数第三行采用RuntimeVisibleAnnotations来保存Target跟Retention注解,这两个都是运行时驻留的

RuntimeVisibleAnnotations:
  0: #11(#12=[e#13.#14])  //Ljava/lang/annotation/Target;
  1: #15(#12=e#16.#17)   //Ljava/lang/annotation/Retention;

顺便看看RuntimeVisibleAnnotations属性表都有哪些字段

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266