注解也被称为元数据。
这个名字也体现了注解的价值:在某处提供额外的信息,便于之后使用这些信息。
注解有多重要
以前的框架流行的是xml配置,而现在更多的是用注解。主流的Spring开发都是全注解开发。
自定义注解最常见的应用场景就是:Spring AOP,用来做日志切面打印处理
因此,学会元注解,自定义注解,了解注解实现原理是Java程序员的必修课
内置注解
java.lang提供的基础注解:
@Deprecated
:表示代码被弃用@SuppressWarnings
:表示关闭编译器警告信息 有参数,直接用(all)吧@Override
:表示方法被覆写
JAVA8新增:
@FunctionalInterface
:表示一个函数式接口
元注解
元注解: 是针对 public @interface Annotation {}
自己实现注解时用到的基础注解
@targert
表示可以修饰什么内容@Retention & @RetentionTarget
表示注解在它所修饰的类中可以被保留到何时,注解的生命周期@Inherited
表示被该注解修饰的类 的子类 会一起继承该注解@Documented
:注解是否应当被包含在 JavaDoc 文档中
@target
取值:
注:可以用{}
多选
- ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
- ElementType.FIELD:允许作用在属性字段上
- ElementType.METHOD:允许作用在方法上
- ElementType.PARAMETER:允许作用在方法参数上
- ElementType.CONSTRUCTOR:允许作用在构造器上
- ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
- ElementType.ANNOTATION_TYPE:允许作用在注解上
- ElementType.PACKAGE:允许作用在包上
@Retention
取值:
- RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
- RetentionPolicy.CLASS:类加载阶段丢弃,在class文件的属性表中用 RuntimeInvisibleAnnotations表示
- RetentionPolicy.RUNTIME:永久保存,可以反射获取,一般自定义注解都是RUNTIME,在class文件的属性表中用RuntimeVisibleAnnotations 表示
Java8新增的元注解
@Repeatable :使用这个注解时,可以多次修饰
@Native :注解修饰成员变量:表示这个变量可以被本地代码引用,不常用
注解与继承
定义注解时无法继承注解。毕竟编写注解不会花费你太多时间,更多的是元注解的定义和一张哈希表。与继承强调的代码复用只能说没什么关系。
我们的@Inherited
元注解,是指:一个父类的被@Inherited
修饰的注解,子类也会有。这两点需要区别开来
自定义注解
- 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
public String title() default "";
public String description() default "";
}
- 使用注解
@MyMethodAnnotation(title = "xxx", description = "xxx")
public String xxx() {
return "";
}
- 获取注解
public static void main(String[] args) {
try {
Method[] methods = TestMethodAnnotation.class.getClassLoader()
.loadClass(("com.pdai.java.annotation.TestMethodAnnotation"))
.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
MyMethodAnnotation methodAnno = method
.getAnnotation(MyMethodAnnotation.class);
System.out.println(methodAnno.title());
}
}
注解如何生效
- 编译器扫描处理
- 运行期反射处理
编译器扫描处理一般只有Java内置注解会用到,比如@Override修饰的方法,编译器会检查父类是否有相同的方法
而大部分自定义的注解,都是在运行期通过反射拿到并处理。
运行时注解存放在哪里
在class文件中的attributes属性表中。
运行期如何获取注解🚩
反射获取注解的核心在: java.lang.reflect下的 AnnotatedElement接口,而AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口。
对于一个类或者接口来说,Class 类中提供了以下一些方法用于注解操作。
判断
判断是否包含指定类型的注解
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
获取
1、获取指定类型的注解
<T extends Annotation> T getAnnotation(Class<T> annotationClass)
如果该注解可重复,即同type的注解有多个:
<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
忽略继承的注解的版本 + Declared
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
2、获取所有注解
Annotation[] getAnnotations()
同样有个Declared的版本忽略继承的注解
Annotation[] getDeclaredAnnotations()
注解实现原理/本质
此处是面向运行期注解的实现原理,在此关于编译期注解简单说一嘴
JDK5首次提出注解仅仅面向运行期注解,在JDK6才提出了编译期注解,提供了「插入式注解处理器」的API,这会影响前端编译器的工作。比如:Lombok,这个够有名吧,就是利用了「插入式注解处理器」实现的功能。
在Javac源码中,插入式注解处理器的初始化过程是在initPorcessAnnotations()方法中完成的,而它的执行过程则是在processAnnotations()方法中完成。这个方法会判断是否还有新的注解处理器需要执行,如果有的话,通过com.sun.tools.javac.processing.JavacProcessing-Environment类的doProcessing()方法来生成一个新的JavaCompiler对象,对编译的后续步骤进行处理。
推荐阅读:周志明《深入理解Java虚拟机》P510:插入式注解处理器实战
Java注解处理器[1]这篇文章详细分析了编译期注解的原理,很推荐
注解的本质就是一个继承了 Annotation 接口的接口,因此也会被编译成class文件
public interface Override extends Annotation{
}
没错,注解本身就是一个接口。
public String test() default "";
并且注解内部的“数据”,本质是一个接口方法。
但我们是可以通过反射拿到Annotation实例的,那么:
- Annotation明明是个接口,怎么实例化的?
- 方法也是抽象方法,它的执行逻辑去哪里了
实际上,我们在运行期获取到的注解,都是代理类。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
return (A) annotationData().annotations.get(annotationClass);
}
最终调用到
public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
public Annotation run() {
return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(),
new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
}
});
}
Proxy.newProxyInstance,这个东西眼熟吧。
JDK动态代理核心:AnnotationInvocationHandler
核心属性:
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;
注意上面调用的构造函数:
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}
重点看invoke方法
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
if (var4.equals("annotationType") return this.type;
Object var6 = this.memberValues.get(var4);
if(异常){抛出}
else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
注解原理小结
注解本质是个接口,无法实例化,所以我们在运行期反射拿到的注解,其实是Proxy代理对象,本质是JDK动态代理。核心的代理类是AnnotationInvocationHandler。这个类内部用一张Map存储注解的k - v,用一个Class描述注解的类型。对注解内数据的获取,因为注解是个接口,方法都是抽象方法,实际仅仅是对内部的map的get调用。当然此处会屏蔽一些比如toString,equals等不需要代理的方法。
再往JVM底层说,注解也被存储在class文件的属性表,包括了注解的全类名,以及若干pair键值对,保存注解的参数和具体值。
参考文献
Java 基础 - 注解机制详解[2]
Java 注解机制[3]
java注解的本质以及注解的底层实现原理[4]