一、注解有什么用
日常开发中我们用到的许多框架中都配置了大量的注解,比如Spring里的: @Component、@Configuration;Mybatis中的:@Mapper、@Select等,要想知道这些框架中的注解是怎么做到相应功能的,以后写一些自定义注解,那么学习注解就是必不可少的一环。
二、什么是注解 Annotation
在Java中注解就是附加在类、方法、字段、参数上的标记,其实就是一种特殊的注释:
- 单行 / 多行注释:仅给人看,编译后丢弃;
- 注解:给程序、框架读取,可保留到编译期、运行期。
它本身不直接执行业务逻辑,但是在代码执行的过程中可以被反射读取到并且进行相应的操作,而如果没有使用反射或者其他检查,那么注解是没有任何真实作用的,也不会影响到程序的正常运行结果。比如@Override注解的作用是标记当前方法是重写父类 / 实现接口的方法,那么编译器会检查方法签名是否和父类完全匹配,如果不匹配的话就是直接标红报错,对于我们来说看到这个注解就知道要重写方法了,不加的话也不会报错,因为编译器就不会进行校验了,但是如果子类重写的方法签名和父类不同的话重写就变成了重载,这是一个很隐蔽的bug。
三、注解的分类
注解一般分为三类:
- 元注解:修饰注解的注解,控制自定义注解的生效范围、生命周期,用来自定义注解。
- 基础注解:java提供的一些基础注解,比如
@Override、@Deprecated。 - 自定义注解:第三方定义的注解,由定义者来进行解析,比如Spring中的
@Controller、@Service。
四、四大元注解
1. @Retention 生命周期(最关键)
控制注解保留到哪个阶段,反射读取必须选 RUNTIME
RetentionPolicy.SOURCE:保留到源码阶段,编译后丢弃。因此就无法使用反射读取(如 @Override)。RetentionPolicy.CLASS:保留到 class 字节码阶段,在JVM 加载时丢弃(默认策略),和SOURCE一样无法用反射读取。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)注册的注解类。
- **
- 工作流程:
- 资源定位:扫描器根据配置的包路径(如
com.example.service)查找所有的.class文件。 - 元数据读取:为了提高效率,Spring会使用 ASM(字节码操作框架) 直接读取
.class文件的字节码,获取类上的注解信息,而不会立即将类加载到JVM中。 - 过滤与匹配:检查类上是否包含
@Component或其派生注解(如@Service,@Controller,@Repository)。因为@Service等注解本身就被@Component标注了,所以能被扫描到。 - **生成并注册
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)从容器中查找匹配的依赖,并通过反射注入。
- 触发时机:在Bean实例化后、属性填充(
- 核心代表2:
CommonAnnotationBeanPostProcessor
用于支持JDK中定义的通用注解,如 JSR-250 规范。- 处理
@Resource:按名称(byName)进行依赖注入。 - 处理
@PostConstruct和@PreDestroy:在Bean初始化后和销毁前调用相应方法。
- 处理
- 其他处理器:Spring还提供了其他
BeanPostProcessor来处理特定注解,例如InitDestroyAnnotationBeanPostProcessor(处理@PostConstruct等)、EventListenerMethodProcessor(处理@EventListener)等
3.总结:
- 阶段一解决的是 “有哪些Bean” 的问题,将注解类转化为
BeanDefinition。 - 阶段二解决的是 “Bean如何创建和装配” 的问题,通过
BeanFactoryPostProcessor调整“设计图纸”,再通过BeanPostProcessor对成品进行“精装修”。