注解

990664862 发布于 3 天前 14 次阅读


一、注解有什么用

日常开发中我们用到的许多框架中都配置了大量的注解,比如Spring里的: @Component@ConfigurationMybatis中的:@Mapper@Select等,要想知道这些框架中的注解是怎么做到相应功能的,以后写一些自定义注解,那么学习注解就是必不可少的一环。

二、什么是注解 Annotation

在Java中注解就是附加在类、方法、字段、参数上的标记,其实就是一种特殊的注释:

  • 单行 / 多行注释:仅给人看,编译后丢弃;
  • 注解:给程序、框架读取,可保留到编译期、运行期。

它本身不直接执行业务逻辑,但是在代码执行的过程中可以被反射读取到并且进行相应的操作,而如果没有使用反射或者其他检查,那么注解是没有任何真实作用的,也不会影响到程序的正常运行结果。比如@Override注解的作用是标记当前方法是重写父类 / 实现接口的方法,那么编译器会检查方法签名是否和父类完全匹配,如果不匹配的话就是直接标红报错,对于我们来说看到这个注解就知道要重写方法了,不加的话也不会报错,因为编译器就不会进行校验了,但是如果子类重写的方法签名和父类不同的话重写就变成了重载,这是一个很隐蔽的bug。

三、注解的分类

注解一般分为三类:

  • 元注解:修饰注解的注解,控制自定义注解的生效范围、生命周期,用来自定义注解。
  • 基础注解:java提供的一些基础注解,比如@Override@Deprecated
  • 自定义注解:第三方定义的注解,由定义者来进行解析,比如Spring中的@Controller@Service

四、四大元注解

1. @Retention 生命周期(最关键)

控制注解保留到哪个阶段,反射读取必须选 RUNTIME

  1. RetentionPolicy.SOURCE:保留到源码阶段,编译后丢弃。因此就无法使用反射读取(如 @Override)。
  2. RetentionPolicy.CLASS:保留到 class 字节码阶段,在JVM 加载时丢弃(默认策略),和SOURCE一样无法用反射读取。
  3. RetentionPolicy.RUNTIME:运行时保留,在实际工作中:99%的自定义注解都会设置为 RUNTIME,因为只有运行时才能通过反射获取属性值进行逻辑处理
//保留到 class 字节码
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {}

2. @Target 作用位置

限制注解能写在哪些元素上,不指定则所有位置都能用

常用取值:

  • TYPE:类、接口、枚举
  • METHOD:方法(AOP 切点最常用)
  • FIELD:成员变量
  • PARAMETER:方法参数
  • CONSTRUCTOR:构造方法
// 仅允许标注在方法上
@Target(ElementType.METHOD)
public @interface LogRecord {}

3. @Documented

生成 JavaDoc 文档时,把注解信息写入文档,业务开发很少用。

4. @Inherited

表示这个注解可被子类继承:父类中加了该注解,其子类不写也能通过反射读到,但是仅作用于 TYPE 类注解,方法 / 字段注解不生效。

五、自定义注解

定义注解的语法和定义接口相似,不过关键字是@interface。

1. 定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
    String value() default "";
}

注意:注解的属性类型只能是基本类型、String、Class、枚举、注解,或以上类型的数组

2.解析逻辑

通常在 AOP 拦截器或启动器中执行

public class RuntimeParser {
    public static void parse(Class<?> clazz) throws Exception {
        for (Method method : clazz.getDeclaredMethods()) {
            MyLog log = method.getAnnotation(MyLog.class);
            if (log != null) {
                System.out.println("方法: " + method.getName() + ", 日志值: " + log.value());
                // 在这里利用反射调用 method.invoke() 执行具体逻辑
            }
        }
    }

六、Spring中解析注解的流程

核心流程大概如下图:

flowchart TD
    A["容器启动"] --> B["阶段一:扫描与注册 <br/> (Bean定义阶段)"]
    B --> C{核心组件}
    C --> D["ClassPathBeanDefinitionScanner <br/> (扫描类路径)"]
    C --> E["AnnotatedBeanDefinitionReader <br/> (读取配置类)"]
    D --> F["生成BeanDefinition <br/> 并注册到容器"]
    E --> F
    F --> G["阶段二:处理与实例化 <br/> (Bean初始化阶段)"]
    G --> H{核心处理器}
    H --> I["BeanFactoryPostProcessor <br/> (处理Bean定义)"]
    H --> J["BeanPostProcessor <br/> (处理Bean实例)"]
    I --> K["ConfigurationClassPostProcessor <br/> (解析Configuration等)"]
    J --> L["AutowiredAnnotationBeanPostProcessor <br/> (处理Autowired等)"]
    J --> M["CommonAnnotationBeanPostProcessor <br/> (处理Resource等)"]
    K --> N["完成Bean定义修改"]
    L --> O["完成依赖注入"]
    M --> O
    N --> P["Bean实例化完成"]
    O --> P

Spring中注解的解析,是一个分阶段、多层次的过程,可以理解为一个 “扫描发现 -> 定义注册 -> 实例化处理” 的流水线。

整个流程的核心思想是:注解本身是“惰性”的,真正起作用的是Spring内部一系列注解处理器(Processor)

阶段一:扫描发现与定义注册 (Bean Definition阶段)

这是解析流程的起点,目标是找出所有被注解标记的类,并将其转化为Spring内部的BeanDefinition对象进行注册。这个阶段主要发生在Spring容器的启动初期。

  • 触发点:通常由 @ComponentScan 注解(或在XML中配置的 <context:component-scan>)触发。
  • 核心组件
    • **ClassPathBeanDefinitionScanner**:一个“扫描器”,负责在指定的包路径下查找类文件。
    • **AnnotatedBeanDefinitionReader**:一个“读取器”,负责解析通过编程方式或配置类(@Configuration)注册的注解类。
  • 工作流程
    1. 资源定位:扫描器根据配置的包路径(如 com.example.service)查找所有的 .class 文件。
    2. 元数据读取:为了提高效率,Spring会使用 ASM(字节码操作框架) 直接读取 .class 文件的字节码,获取类上的注解信息,而不会立即将类加载到JVM中
    3. 过滤与匹配:检查类上是否包含 @Component 或其派生注解(如 @Service, @Controller, @Repository)。因为 @Service 等注解本身就被 @Component 标注了,所以能被扫描到。
    4. **生成并注册 BeanDefinition**:对于匹配的类,Spring会创建一个 BeanDefinition 对象(可以理解为Bean的“设计图纸”),其中包含了Bean的类名、作用域(@Scope)、懒加载(@Lazy)等信息。最后,这个“设计图纸”会被注册到BeanDefinitionRegistry(Bean定义注册表)中。

阶段二:后置处理与实例化 (Bean初始化阶段)

当所有BeanDefinition都注册好后,Spring就开始根据这些“设计图纸”来创建Bean的实例。在这个过程中,两类关键的后置处理器(Post-Processor) 会介入,对Bean的定义和实例进行加工。

1. BeanFactoryPostProcessor:处理Bean定义的“规划师”

它在所有Bean定义被加载后、任何Bean实例被创建前执行。它的职责是修改BeanDefinition中的元数据,例如修改作用域、添加属性等。

  • 核心代表:ConfigurationClassPostProcessor
    这是Spring框架中最重要的BeanFactoryPostProcessor之一,专门负责解析配置类(被@Configuration标注的类):
    • 解析@ComponentScan,触发阶段一的扫描。
    • 解析@Bean,将配置类中方法的返回值注册为Bean。
    • 解析@Import@ImportResource等导入注解。

2. BeanPostProcessor:处理Bean实例的“工匠”

它在Bean实例被创建后、初始化过程中执行。它的职责是对已创建的Bean实例进行加工,比如注入依赖、执行自定义初始化方法等。

  • 核心代表1:AutowiredAnnotationBeanPostProcessor
    专门处理 @Autowired@Value 注解。
    • 触发时机:在Bean实例化后、属性填充(populateBean)阶段被调用。
    • 工作方式:扫描Bean中的字段、构造函数、Setter方法,查找@Autowired注解。然后,根据类型(byType)从容器中查找匹配的依赖,并通过反射注入。
  • 核心代表2:CommonAnnotationBeanPostProcessor
    用于支持JDK中定义的通用注解,如 JSR-250 规范。
    • 处理 @Resource:按名称(byName)进行依赖注入。
    • 处理 @PostConstruct@PreDestroy:在Bean初始化后和销毁前调用相应方法。
  • 其他处理器:Spring还提供了其他BeanPostProcessor来处理特定注解,例如InitDestroyAnnotationBeanPostProcessor(处理@PostConstruct等)、EventListenerMethodProcessor(处理@EventListener)等

3.总结:

  • 阶段一解决的是 “有哪些Bean” 的问题,将注解类转化为BeanDefinition
  • 阶段二解决的是 “Bean如何创建和装配” 的问题,通过BeanFactoryPostProcessor调整“设计图纸”,再通过BeanPostProcessor对成品进行“精装修”。
此作者没有提供个人介绍。
最后更新于 2026-07-26