Java 注解
Java 注解
用注解来保存类相关的信息以供反射调用
提供了一种为程序元素设置元数据的方法
所有注解都隐式地继承自 java.lang.annotation.Annotation
接口
注解的功能
- 作为特定的标记,告诉编译器一些信息
例如 @Override
注解修饰的方法,编译器会检查是否重写
- 编译时动态处理,如动态生成代码
典型的如 @Data
注解
- 运行时动态处理,作为额外信息的载体,如获取注解信息
这类注解在运行时依旧有效,生命周期为RUNTIME
注解的分类
- 标准注解:
@Override
,@Deprecated
,@SuppressWarnings
- 元注解:
@Retention
,@Target
,@Inherited
,@Documented
- 自定义注解
元注解
用于修饰注解的注解,通常用在注解的定义上
元注解 | 功能 |
---|---|
@Target | 定义注解的作用目标 |
@Retention | 定义注解的生命周期 |
@Documented | 定义注解是否应当被包含在JavaDoc文档中 |
@Inherited | 定义是否允许子类继承该注解 |
@Target
Target
接口中定义了一个 ElementType
数组,用来描述注解可以应用的元素类型
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* 返回注释接口可应用于的元素类型的数组。
*/
ElementType[] value();
}
ElementType
枚举类中定义可以被注解修饰的元素类型
public enum ElementType {
/** 类,接口(包括注解接口), 枚举, 或记录
* declaration */
TYPE,
/** 成员变量(包括枚举常量) */
FIELD,
/** 方法 */
METHOD,
/** 方法参数 */
PARAMETER,
/** 构造器 */
CONSTRUCTOR,
/** 本地局部变量 */
LOCAL_VARIABLE,
/** 注解 */
ANNOTATION_TYPE,
/** 包 */
PACKAGE,
/**
* 用于泛型参数
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE,
/**
* Module declaration.
*
* @since 9
*/
MODULE,
/**
* Record component
*
* @jls 8.10.3 Record Members
* @jls 9.7.4 Where Annotations May Appear
*
* @since 16
*/
RECORD_COMPONENT;
}
@Retention
指示带注释接口的注释要保留多长时间。如果注释接口声明中不存在Retention
注释,则保留策略默认为RetentionPolicy.CLASS
。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* 返回保留策略
*/
RetentionPolicy value();
}
RetentionPolicy
定义了所有保留策略
public enum RetentionPolicy {
/**
* 注解在源文件保留,在编译后的class文件中不保留,典型的有 @Override, @Data
*/
SOURCE,
/**
* 注解会在源文件和class文件中保留,但不会在VM运行时保留
*/
CLASS,
/**
* 注释将由编译器记录在类文件中,并在运行时由VM保留,因此可以反射性地读取它们。
* 典型的有 @Autowired,让程序在运行时通过反射的方法将bean实例注入
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
自定义注解
@interface
声明创建了一个真正的Java接口
自定义注解的修饰符支持 public
和 缺省
, 后者定义的注解只能在同一个包下使用
注解不能实现和继承其他接口或类
注解定义格式如下,一般自定义注解要定义 @Target
,@Retention
来指定注解修饰的元素类型和注解的生命周期
因为注解是由编译器计算而来的,因此,所有元素值必须是编译期常量
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
// 修饰符 返回值 属性名() 默认值
public String value() default "default";
}
下面定义一个包含课程信息的注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CourseInfoAnnotation {
//课程名称
public String courseName();
//课程标签
public String courseTag();
//课程简介
public String courseProfile();
//课程序号
public int courseIndex() default 303;
}
用这个注解修饰一个类和类中的方法
@CourseInfoAnnotation(
courseName = "英语",
courseTag = "必修",
courseProfile = "英语模块"
)
public class MyCourse {
@CourseInfoAnnotation(
courseName = "英语听说",
courseTag = "必修",
courseProfile = "",
courseIndex = 1)
public void getCourseInfo() {
}
}
注解的解析
注解自身并不会做任何事情,例如我们常用的@Test
注解,就需要有JUnit工具支持,JUnit可能会调用所有标识为@Test的方法,这个过程中就用到了反射机制来获取注解信息。
我们看一下反射相关的类(Field
、Constructor
、Method
)和注解相关类之间的关系,如下图所示,所有反射对象的基类都实现了 AnnotatedElement
接口。
AnnotatedElement
:表示当前在此VM中运行的程序的注释元素,Method
、Constructor
、Field
、Class
、Package
都实现了这个接口,这个接口定义了判断注解是否存在和多种获取注解的方法AccessibleObject
:AccessibleObject
类是Field
、Method
和Constructor
对象(称为反射对象)的基类。它提供了将反射对象标记为在使用时禁止检查Java语言访问控制的能力。这允许具有足够权限的复杂应用程序(如Java对象序列化或其他持久性机制)以通常被禁止的方式操作对象。
反射解析注解的思路:
- 获取这个类的Class对象
- 通过Class对象获取类上的属性、方法
- 调用
AnnotatedElement
中的方法获取注解类
下面是用反射获取注解信息的例子
public class MyAnnotationParser {
public static void processAnnotations(Object obj) {
// 由实例对象获取Class对象
Class<?> cl = obj.getClass();
// 获取类上修饰的注解
CourseInfoAnnotation courseInfoAnnotationOnClass = cl.getAnnotation(CourseInfoAnnotation.class);
if (courseInfoAnnotationOnClass != null) {
System.out.println("---类注解---");
System.out.println("courseName = " + courseInfoAnnotationOnClass.courseName());
System.out.println("courseProfile = " + courseInfoAnnotationOnClass.courseProfile());
System.out.println("courseTag = " + courseInfoAnnotationOnClass.courseTag());
System.out.println("courseIndex = " + courseInfoAnnotationOnClass.courseIndex());
}
// 获取方法上修饰的注解
for (Method method : cl.getMethods()) {
CourseInfoAnnotation courseInfoAnnotationOnMethod = method.getAnnotation(CourseInfoAnnotation.class);
if (courseInfoAnnotationOnMethod != null) {
System.out.println("---方法注解---");
System.out.println("courseName = " + courseInfoAnnotationOnMethod.courseName());
System.out.println("courseProfile = " + courseInfoAnnotationOnMethod.courseProfile());
System.out.println("courseTag = " + courseInfoAnnotationOnMethod.courseTag());
System.out.println("courseIndex = " + courseInfoAnnotationOnMethod.courseIndex());
}
}
}
public static void main(String[] args) {
MyCourse myCourse = new MyCourse();
processAnnotations(myCourse);
}
}
注解获取属性值的底层实现
- JVM 会为注解生成代理对象
添加 JVM 的启动参数-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true
,保留jdk动态代理生成的类文件
运行我们之前注解解析的main函数,在项目目录下生成了一下文件
其中 $Proxy1
个动态生成的类实现了我们自定义的注解接口,下面主要分析这个类。
public final class $Proxy1 extends Proxy implements CourseInfoAnnotation {
// 列举部分方法
private static Method m1;
private static Method m4;
// ... 省略其他方法声明 ...
// 在静态代码块中给方法赋值
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m4 = Class.forName("demo.annotation.CourseInfoAnnotation").getMethod("courseName");
// ... 省略其他方法赋值 ...
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
public $Proxy1(InvocationHandler var1) throws {
super(var1);
}
// 重写 Object 中的 equals 方法
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 courseName() throws {
try {
return (String)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
// ...省略其他的方法重写...
动态代理
涉及到动态代理,这里简单介绍一下动态代理中常见的类
Proxy
: 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。InvocationHandler
: 由代理实例的调用处理程序实现的接口, 只声明了一个invoke(Object proxy, Method method, Object[] args)
方法来处理代理实例上的方法。
重写方法的执行过程
看一下代理类重写方法后做了什么,以 courseName() 方法为例
public final String courseName() throws {
try {
return (String)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
super
是父类 Proxy
,h
是 InvocationHandler
变量,调用了 invoke
方法来处理代理实例上的方法。
这里调用的InvocationHanler
接口的实现类是 AnnotationInvocationHandler
,实现类的 invoke
方法实现如下:
// 存储注解里的属性名和属性值
private final Map<String, Object> memberValues;
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
int parameterCount = method.getParameterCount();
// Handle Object and Annotation methods
if (parameterCount == 1 && member == "equals" &&
method.getParameterTypes()[0] == Object.class) {
return equalsImpl(proxy, args[0]);
}
if (parameterCount != 0) {
throw new AssertionError("Too many parameters for an annotation method");
}
if (member == "toString") {
return toStringImpl();
} else if (member == "hashCode") {
return hashCodeImpl();
} else if (member == "annotationType") {
return type;
}
// 对于其他方法,从 memberValus 中取值
Object result = memberValues.get(member);
// ... 省略异常处理 ...
return result;
}
注解的工作原理
- 通过键值对的形式为注解属性赋值
- 编译器检查注解的使用范围,并将注解信息写入元素属性表
- 运行时JVM将RUNTIME的所有注解属性取出并最终存入map里
- 创建 AnnotationInvocationHandler 实例并传入前面的map
- JVM 使用 JDK 动态代理为注解生成代理类,并初始化处理器
- 调用 invoke 方法, 通过传入方法名返回注解对应的属性值
1. 通过键值对的形式为注解属性赋值
@CourseInfoAnnotation(
courseName = "英语",
courseTag = "必修",
courseProfile = "英语模块"
)
public class MyCourse {
}
2. 编译器检查注解的使用范围,并将注解信息写入元素属性表
首先编译器检查注解修饰的范围是否正确,否则无法通过编译。
通过编译后,用工具查看编译后的字节码文件,带有注解信息
3. 运行时JVM将RUNTIME的所有注解属性取出并最终存入map里
JVM 会将单个 Class 文件中生命周期为 RUNTIME 的注解取出并放入map中
4. 创建 AnnotationInvocationHandler 实例并传入前面的map
private Annotation generateAnnotation() {
return AnnotationParser.annotationForMap(annoType,
getAllReflectedValues());
}
public static Annotation annotationForMap(final Class<? extends Annotation> type,
final Map<String, Object> memberValues)
{
return AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
public Annotation run() {
return (Annotation) Proxy.newProxyInstance(
type.getClassLoader(), new Class<?>[] { type },
new AnnotationInvocationHandler(type, memberValues));
}});
}
5. JVM 使用 JDK 动态代理为注解生成代理类,并初始化处理器
初始化处理器,调用 $Proxy
的构造函数,执行父类构造函数,将 InvocationHandler 初始化
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
6. 调用 invoke 方法, 通过传入方法名返回注解对应的属性值