Skip to content

注解

1. 注解

  • 作用:为代码添加说明,可注解包、类、接口、字段、方法参数、局部变量等。
  • 主要功能
    1. 生成文档:通过元数据生成 Javadoc。
    2. 编译检查:编译器验证代码。
    3. 编译时处理:动态生成代码。
    4. 运行时处理:通过反射动态操作。
  • 分类
    • 内置注解:如 @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. 自定义注解

  • 步骤
    1. 定义注解接口,使用元注解修饰。
    2. 指定属性(如 title()、description())。
    3. 通过反射获取注解信息。
  • 示例
@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
    • AnnotationInvocationHandlerAnnotationInvocationHandler 封装了注解的代理逻辑,负责处理注解的解析和属性值的获取。
    • Proxy.newProxyInstance() 方法:注解的代理对象是通过 Proxy.newProxyInstance() 方法创建的,该方法接收三个参数:

    • ClassLoader loader: 加载代理对象的类加载器。

    • Class<?>[] interfaces: 代理类需要实现的接口,对于注解来说,就是注解接口本身。
    • InvocationHandler h: InvocationHandler 接口的实例,即 AnnotationInvocationHandler 实例,用于处理代理逻辑。

注解值的存储与获取

注解的参数信息(即您在代码中为注解属性指定的值)存储在 class 文件的常量池中。在创建注解实例的过程中,会从常量池中读取这些信息。

  • 注解解析流程

    1. java.lang.Class#getDeclaredAnnotation: 获取注解的入口方法。
    2. java.lang.Class#createAnnotationData: 创建注解数据,用于后续实例化。
    3. sun.reflect.annotation.AnnotationParser#parseAnnotations: 解析注解,关键步骤如下:
      • getRawAnnotations() (native 方法): 获取原始注解信息。
      • getConstantPool() (native 方法): 获取常量池信息。
      • sun.reflect.annotation.AnnotationParser#parseAnnotation2: 对原始注解信息进行加工转换。
    4. sun.reflect.annotation.AnnotationParser#annotationForMap: 创建注解实例,关键步骤如下:
      • memberDefaults (HashMap): 在 annotationForMap 方法中,注解的值被存储在一个 HashMap 类型的 memberDefaults 变量中。这个 HashMap 实际上是在创建 AnnotationInvocationHandler 实例时作为构造函数参数传入的。
      • JDK 动态代理 newProxyInstance(): 最终,通过 Proxy.newProxyInstance() 方法创建代理对象并返回。
    5. AnnotationInvocationHandlerinvoke() 方法:当调用代理对象的方法(即注解的属性方法)来获取注解值时,实际上会调用 AnnotationInvocationHandlerinvoke() 方法。invoke() 方法会从之前存储在 memberDefaults 中的注解信息中取出对应的值并返回。