注解
1. 注解
- 作用:为代码添加说明,可注解包、类、接口、字段、方法参数、局部变量等。
- 主要功能:
- 生成文档:通过元数据生成 Javadoc。
- 编译检查:编译器验证代码。
- 编译时处理:动态生成代码。
- 运行时处理:通过反射动态操作。
- 分类:
- 内置注解:如 @Override、 @Deprecated、 @SuppressWarnings。
- 元注解:定义注解的注解,如 @Target、 @Retention。
- 自定义注解:根据需求自行定义。
2. Java 内置注解
- @Override:
- 作用:标记方法覆盖父类方法,编译器检查签名一致性。
- 定义:@Target(ElementType.METHOD)、@Retention(RetentionPolicy.SOURCE)。
- @Deprecated:
- 作用:标记代码已废弃,调用时编译器警告。
- 定义:@Retention(RetentionPolicy.RUNTIME),可修饰多种元素。
- @SuppressWarnings:
- 作用:抑制指定编译警告,如 "unchecked"、"deprecation"。
- 定义:接受 String[] 参数,源码级别保留。
3. 元注解
- @Target:
- 作用:指定注解的使用范围。
- 取值:ElementType 枚举(如 TYPE、METHOD、FIELD 等)。
- @Retention:
- 作用:指定注解保留阶段。
- 取值:RetentionPolicy 枚举:
- SOURCE:源码保留,编译后丢弃。
- CLASS:编译期保留,默认值。
- RUNTIME:运行时保留,可反射获取。
- @Documented:
- 作用:标记注解是否包含在 Javadoc 中。
- @Inherited:
- 作用:使注解具有继承性,子类自动继承父类的注解。
- @Repeatable(Java 8):
- 作用:允许同一位置重复使用同一注解。
- @Native(Java 8):
- 作用:标记字段可被本地代码引用,常用于代码生成工具。
4. 注解与反射
- 接口:java.lang.reflect.AnnotatedElement。
- 主要方法:
- isAnnotationPresent():检查是否包含某注解。
- getAnnotation():获取指定注解。
- getAnnotations():获取所有注解。
- 注意:只有 @Retention(RUNTIME) 的注解才能通过反射获取。
5. 自定义注解
- 步骤:
- 定义注解接口,使用元注解修饰。
- 指定属性(如 title()、description())。
- 通过反射获取注解信息。
- 示例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
public String title() default "";
public String description() default "";
}
public class TestMethodAnnotation {
@Override
@MyMethodAnnotation(title = "toStringMethod", description = "override toString method")
public String toString() {
return "Override toString method";
}
@Deprecated
@MyMethodAnnotation(title = "old static method", description = "deprecated old static method")
public static void oldMethod() {
System.out.println("old method, don't use it.");
}
@SuppressWarnings({"unchecked", "deprecation"})
@MyMethodAnnotation(title = "test method", description = "suppress warning static method")
public static void genericsTest() throws FileNotFoundException {
List l = new ArrayList();
l.add("abc");
oldMethod();
}
}
6. Java 8 新增注解特性
- @Repeatable:支持重复注解,需配合容器注解使用。
- ElementType.TYPE_USE:扩展注解到类型使用场景(如变量类型声明)。
- ElementType.TYPE_PARAMETER:用于类型参数(如泛型)。
7. 注解是否支持继承?
- 注解本身:不支持通过 extends 继承,但编译后自动实现 Annotation 接口。
- 被注解的类:通过 @Inherited 实现子类继承父类注解。
8. 注解的应用场景
- 配置化到注解化:
- 示例:Spring 从 XML 配置进化到注解(如 @Autowired)。
- 继承到注解实现:
- 示例:Junit3(继承 TestCase)到 Junit4(使用 @Test、@Before)。
- 自定义注解与 AOP:
- 示例:Spring AOP 通过 @Log 注解实现统一日志管理,解耦业务逻辑。
9.注解的本质:接口
Java 注解实际上是一个接口。
- 继承自
java.lang.annotation.Annotation
接口:所有的注解都会自动继承Annotation
接口,可以在 JDK 包中找到Annotation
接口的定义,它是所有注解类型的父接口。 - 声明式接口:注解可以被看作是一种声明式接口,因为它定义了一种规范或约定,用于为代码元素(类、方法、字段等)添加元数据信息。但它本质上仍然是一个接口,而不是像 Spring 等框架中“声明式事务”那种更广义的“声明式”。
@interface
关键字:使用@interface
关键字定义注解时,Java 编译器会将其编译成一个接口。
注解的结构:接口方法
注解中定义的每一个属性,实际上都对应于注解接口中的一个抽象方法。
- 字节码指令
INVOKEINTERFACE
:当在代码中通过注解实例调用属性方法(例如test2.value()
)时,字节码指令是INVOKEINTERFACE
,这进一步证明了注解属性被 Java 编译器视为接口方法。
注解的实例化:JDK 动态代理
如果注解是接口,那么它是如何被实例化,以及何时、如何获取注解实例的呢?当通过反射 API(例如 AnnotionTest.class.getDeclaredAnnotation(Test2.class)
)获取注解实例时,实际上得到的是一个代理对象。
-
Proxy
对象:getDeclaredAnnotation()
方法返回的实例名称是$Proxy1
,这表明这是一个动态代理对象,由 JDK 动态代理生成。 -
JDK 动态代理:注解的实例化使用了 JDK 动态代理机制,这与通常使用的动态代理模式一致,需要以下关键组件:
Proxy
类:JDK 提供的用于生成代理对象的类。InvocationHandler
接口:用于封装代理逻辑的接口。在注解的场景中,实现类是AnnotationInvocationHandler
。AnnotationInvocationHandler
类:AnnotationInvocationHandler
封装了注解的代理逻辑,负责处理注解的解析和属性值的获取。-
Proxy.newProxyInstance()
方法:注解的代理对象是通过Proxy.newProxyInstance()
方法创建的,该方法接收三个参数: -
ClassLoader loader
: 加载代理对象的类加载器。 Class<?>[] interfaces
: 代理类需要实现的接口,对于注解来说,就是注解接口本身。InvocationHandler h
:InvocationHandler
接口的实例,即AnnotationInvocationHandler
实例,用于处理代理逻辑。
注解值的存储与获取
注解的参数信息(即您在代码中为注解属性指定的值)存储在 class 文件的常量池中。在创建注解实例的过程中,会从常量池中读取这些信息。
-
注解解析流程:
java.lang.Class#getDeclaredAnnotation
: 获取注解的入口方法。java.lang.Class#createAnnotationData
: 创建注解数据,用于后续实例化。sun.reflect.annotation.AnnotationParser#parseAnnotations
: 解析注解,关键步骤如下:getRawAnnotations()
(native 方法): 获取原始注解信息。getConstantPool()
(native 方法): 获取常量池信息。sun.reflect.annotation.AnnotationParser#parseAnnotation2
: 对原始注解信息进行加工转换。
sun.reflect.annotation.AnnotationParser#annotationForMap
: 创建注解实例,关键步骤如下:memberDefaults
(HashMap): 在annotationForMap
方法中,注解的值被存储在一个HashMap
类型的memberDefaults
变量中。这个HashMap
实际上是在创建AnnotationInvocationHandler
实例时作为构造函数参数传入的。- JDK 动态代理
newProxyInstance()
: 最终,通过Proxy.newProxyInstance()
方法创建代理对象并返回。
AnnotationInvocationHandler
的invoke()
方法:当调用代理对象的方法(即注解的属性方法)来获取注解值时,实际上会调用AnnotationInvocationHandler
的invoke()
方法。invoke()
方法会从之前存储在memberDefaults
中的注解信息中取出对应的值并返回。