跳至主要內容

Java注解探究

PPLong大约 7 分钟学习

Java注解探究

为什么学习?以前只是初步了解注解的概念,但实际应用场景寥寥无几。因为最近才开始深入的学习Spring,希望从底层弄懂其原理,当然作为Spring一定会用到的关键的注解,例如@Autowired之类的,要弄清其原理则肯定首先要弄懂Java的注解机制,因此才有了本文

关于本文你可能需要提前了解:📖 Java的反射open in new window、📖 JDK动态代理机制open in new window
关于本文包含的内容:注解的概念、注解的结构、自定义注解、注解字节码层面和Java层面探究

介绍

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。

Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

注解主要被反射所读取并进行配置
❔ 为什么?因为反射是JVM所提供的特性,能读取内存中字节码的信息

内置的注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

作用在代码的注解是

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。

作用在其他注解的注解(或者说 元注解)是:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次

基本结构

规定

注解可涵盖的数据类型

  • 八种基本数据类型
  • String
  • 枚举
  • Class
  • 注解类型
  • 以上类型的一维数组,数组的赋值用{}声明(只有一个元素时{}可省略)

如果使用其他类型的对象,则会报错
image-20220616105156419

若注解中只有一个方法value(),则注解时可以直接不加方法名称说明,@Hello("myname")即可

结构

注解的本质是一个继承了Annotation接口的接口

基础回顾: Interface中的方法默认都是public abstract的,且属性默认是static的

Annotation

public interface Annotation {
  
    boolean equals(Object obj);
    
    int hashCode();

    String toString();
	
    /**
     * Returns the annotation type of this annotation.
     * @return the annotation type of this annotation
     */
    Class<? extends Annotation> annotationType();
}

ElementType

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

RetentionPolicy

  • Source: 仅在源码中使用,不会生成在Class文件中,JVM也检测不到
  • Class: 在Class文件中生成(能在Class文件中看到该注解的标注),但JVM中检测不到(在加载字节码文件进入内存中时便会筛去)
  • Runtime:JVM检测的出 => 能够通过getAnnotation(..)来检测该注解
/**
 * Annotation retention policy.  The constants of this enumerated type
 * describe the various policies for retaining annotations.  They are used
 * in conjunction with the {@link Retention} meta-annotation type to specify
 * how long annotations are to be retained.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

一个Annotation与一个RetentionPolicy关联,并且与1-n个ElementType关联;ElementType来指定Annotation的类型,指明Annotation的用途;RetentionPolicy指定Annotation的作用域,三种作用域的作用在上述注解中均已说明。

实验

使用

getAnnotation指定的注解来获取指定的注解类型,可直接调用该注解对象下的方法,进而获取其中的数据

@Target(value = {ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Hello {
    String value() default "123";
    String name() default "zyl";
}
@Hello(name = "hhh")
public class Obj {
    // .....
}
Class<Obj> clazz = Obj.class;
Hello annotations = clazz.getAnnotation(Hello.class);
System.out.println(annotations.name());

探究

字节码层

定义一个简单的注解,然后通过javap -c -v 反编译其class文件

@Target(value = {ElementType.FIELD, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Hello {
    String value() default "123";
}
Classfile /opt/JavaProjects/AnnotationStudy/target/classes/Hello.class
  Last modified 2022-6-14; size 442 bytes
  MD5 checksum 4db04dd07931cec8b63bb2890dd0ad0d
  Compiled from "Hello.java
public interface Hello 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            // Hello
   #2 = Class              #19            // java/lang/Object
   #3 = Class              #20            // java/lang/annotation/Annotation
   #4 = Utf8               value
   #5 = Utf8               ()Ljava/lang/String;
   #6 = Utf8               AnnotationDefault
   #7 = Utf8               123
   #8 = Utf8               SourceFile
   #9 = Utf8               Hello.java
  #10 = Utf8               RuntimeVisibleAnnotations
  #11 = Utf8               Ljava/lang/annotation/Target;
  #12 = Utf8               Ljava/lang/annotation/ElementType;
  #13 = Utf8               FIELD
  #14 = Utf8               METHOD
  #15 = Utf8               Ljava/lang/annotation/Retention;
  #16 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #17 = Utf8               RUNTIME
  #18 = Utf8               Hello
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/annotation/Annotation
{
  public abstract java.lang.String value();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: s#7
 }
SourceFile: "Hello.java"
RuntimeVisibleAnnotations:
  0: #11(#4=[e#12.#13,e#12.#14])
  1: #15(#4=e#16.#17)

可以看到,本质上注解是继承于Annotation接口的,也就是注释本质上是一种接口,自定义的方法变成了抽象的方法,并带有特殊的AnnotationDefault说明。

那提到了接口,而注解又涉及到反射,那自然想到了JDK的动态加载,于是在VMOptions里开一下-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true显示一下Proxy加载的class文件,果然看到其被JDK动态加载

image-20220616111005419

预先加载了Retention的注解,因为Hello的注解中有使用到Retention注解(那为啥没有Target?)

源码层面

JDK 版本: 1.8.0_312

经过调试发现,途经一系列调用,最终进行Proxy.newProxyInstance,并且传入的是一个AnnotationInvocationHandler,type表示一个该注解Class对象(interface),memberValues表明使用该注解的对象下注解的方法与对应的值,本质是一个LinkedHashMap,存储方法名与对应的Object值

return AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
            public Annotation run() {
                return (Annotation) Proxy.newProxyInstance(
                    type.getClassLoader(), new Class<?>[] { type },
                    new AnnotationInvocationHandler(type, memberValues));
            }});

定位到AnnotationInvocationHandler的invoke方法,查看生成的这个代理类是如何进行方法调用的。

public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member);

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

即代理类在调用方法时对自定义的方法,会通过Map memberValues去取之前存有的注解中的值。而注解中设置的值并不直接在生成的$Proxy类中,而是通过构造该类时传入的AnnotationInvocationHandler中的Map<String, Object> memberValues来管理,在每次invoke时去get对应的值

总结

注解是一种帮助快速开发的手段,通过主要Java反射来检测和执行,依赖JDK动态代理的机制。

参考文章: 📖 知乎-注解(上)open in new window 📖 Java注解(Annotation) open in new window 📖 RUNOOB-Java 注解open in new window