在日常变成过程中,我们经常会用到注解,但是注解又是以何种方式生效的,今天我们来对注解的原理进行一些探索。
以前,XML是各大框架的青睐者,它以松耦合的方式完成了框架中几乎所有的配置,但是随着项目越来越庞大,XML的内容也越来越复杂,维护成本变高。
于是就有人提出来一种标记式高耦合的配置方式,注解。方法上可以进行注解,类上也可以注解,字段属性上也可以注解,反正几乎需要配置的地方都可以进行注解。
关于注解和XML两种不同的配置模式,争论了好多年了,各有各的优劣,注解可以提供更大的便捷性,易于维护修改,但耦合度高,而 XML 相对于注解则是相反的。
追求低耦合就要抛弃高效率,追求效率必然会遇到耦合。本文意不再辨析两者谁优谁劣,而在于以最简单的语言介绍注解相关的基本内容。
什么是注解
注解也叫元数据,例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解
「java.lang.annotation.Annotation」接口中有这么一句话,用来描述『注解』
The common interface extended by all annotation types
所有的注解类型都继承自这个普通的接口(Annotation)
注解的本质就是一个继承了 Annotation 接口的接口。有关这一点,你可以去反编译任意一个注解类,你会得到结果的。
一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。
元注解
元注解是用于修饰注解的注解,通常用在注解的定义上
JAVA 中有以下几个『元注解』:
@Target:注解的作用目标
@Retention:注解的生命周期
@Documented:注解是否应当被包含在 JavaDoc 文档中
@Inherited:是否允许子类继承该注解
其中,@Target 用于指明被修饰的注解最终可以作用的目标是谁。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
通过设置ElementType来定义其作用域
例如@Target(value = {ElementType.FIELD})
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,
}
这边列出了常用的五个定义域
@Retention 用于指明当前注解的生命周期
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
通过设置RetentionPolicy来定义
有以下几个枚举
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* 当前注解编译期可见,不会写入 class 文件
*/
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 文件
*/
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
}
@Documented:注解是否应当被包含在 JavaDoc 文档中
@Inherited:是否允许子类继承该注解
注解与反射
上面提到了,注解的本质就是一个继承了Annotation接口的接口,现在我们从虚拟机的层面看注解的本质
首先我们自定义一个注解
package org.annotation.analysis.annocation;
import java.lang.annotation.*;
@Target(ElementType.FIELD,ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String value() default "123";
}
这里我们指定了 MyAnnotation这个注解只能修饰字段和方法,并且注解永久激活,以便我们反射获取
虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:
- RuntimeVisibleAnnotations:运行时可见的注解
- RuntimeInVisibleAnnotations:运行时不可见的注解
- RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
- RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
- AnnotationDefault:注解类元素的默认值
给大家看虚拟机的这几个注解相关的属性表的目的在于,让大家从整体上构建一个基本的印象,注解在字节码文件中是如何存储的。
所以,对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。
- getAnnotation:返回指定的注解
- isAnnotationPresent:判定当前元素是否被指定注解修饰
- getAnnotations:返回所有的注解
- getDeclaredAnnotation:返回本元素的指定注解
- getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的
下面看一个完整的例子
package org.annotation.analysis.bean;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.annotation.analysis.annocation.MyAnnotation;
@Data
@Accessors(chain=true)
@NoArgsConstructor
public class User {
@MyAnnotation(value = "123")
private String name;
private String age;
@Override
@MyAnnotation(value = "123")
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
package org.annotation.analysis.test;
import org.annotation.analysis.annocation.MyAnnotation;
import org.annotation.analysis.bean.User;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws NoSuchMethodException {
Class clazz = User.class;
Method method = clazz.getMethod("toString",null);
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value());
}
}
注解本质上是继承了 Annotation 接口的接口,而当你通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。
在JVM运行的时候加入一行配置
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
运行后回发现,捕捉到的JDK的动态代理类
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.sun.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import org.annotation.analysis.annocation.MyAnnotation;
public final class $Proxy1 extends Proxy implements MyAnnotation {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m0;
private static Method m3;
public $Proxy1(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final Class annotationType() throws {
try {
return (Class)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String value() throws {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("org.annotation.analysis.annocation.MyAnnotation").getMethod("annotationType");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("org.annotation.analysis.annocation.MyAnnotation").getMethod("value");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
可以看到,代理类实现并重现了MyAnnotation里的所有方法,包括value方法以及从Annotation接口继承而来的方法。
此处非常关键的地方在于InvocationHandler,而这个InvocationHandler又是什么?
AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler。
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
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) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
构造方法里有一个memberValues,存放键值对,键是我们注解属性名称,值就是该属性当初被赋上的值
而这个 invoke 方法就很有意思了,我们的代理类代理了 MyAnnotation 接口中所有的方法,所以对于代理类中任何方法的调用都会被转到这里来。
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;
}
}
}
}
我们的代理类代理了 MyAnnotation 接口中所有的方法,所以对于代理类中任何方法的调用都会被转到这里来。
ar2 指向被调用的方法实例,而这里首先用变量 var4 获取该方法的简明名称,接着 switch 结构判断当前的调用方法是谁,如果是 Annotation 中的四大方法,将 var7 赋上特定的值。
如果当前调用的方法是 toString,equals,hashCode,annotationType 的话,AnnotationInvocationHandler 实例中已经预定义好了这些方法的实现,直接调用即可。
那么假如 var7 没有匹配上这四种方法,说明当前的方法调用的是自定义注解字节声明的方法,例如我们 Hello 注解的 value 方法。这种情况下,将从我们的注解 map 中获取这个注解属性对应的值。
其实,JAVA 中的注解设计个人觉得有点反人类,明明是属性的操作,非要用方法来实现。当然,如果你有不同的见解,欢迎留言探讨。
最后我们再总结一下整个反射注解的工作原理:
首先,我们通过键值对的形式可以为注解属性赋值,像这样:@MyAnnotation(value = “hello”)。
接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。
最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类,你应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。一句话概括就是,通过方法名返回注解属性值。