Java注解探究
Java注解探究
为什么学习?以前只是初步了解注解的概念,但实际应用场景寥寥无几。因为最近才开始深入的学习Spring,希望从底层弄懂其原理,当然作为Spring一定会用到的关键的注解,例如@Autowired之类的,要弄清其原理则肯定首先要弄懂Java的注解机制,因此才有了本文
关于本文你可能需要提前了解:📖 Java的反射、📖 JDK动态代理机制
关于本文包含的内容:注解的概念、注解的结构、自定义注解、注解字节码层面和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
- 注解类型
- 以上类型的一维数组,数组的赋值用{}声明(只有一个元素时{}可省略)
如果使用其他类型的对象,则会报错
若注解中只有一个方法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动态加载
而
预先加载了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动态代理的机制。
参考文章: 📖 知乎-注解(上) 📖 Java注解(Annotation) 📖 RUNOOB-Java 注解