拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring注解解析机制:不是每个注解都要写一个解析类

Spring注解解析机制:不是每个注解都要写一个解析类

Spring中每一个注解都需要有一个对应的解析的类吗?在我刚开始啃Spring源码的时候,这个问题困扰了我很久。翻到AutowiredAnnotationBeanPostProcessor时,我下意识觉得Spring是为@Autowired单独写了一个解析类;那照这么推,@Service是不是也得有一个ServiceAnnotationPostProcessor?@Transactional是不是还得有一个事务注解解析器?如果真是这样,Spring的源码怕是要膨胀到没法维护。

后来把AnnotationConfigUtils、ConfigurationClassPostProcessor、ClassPathBeanDefinitionScanner这一串源码看完,才彻底想明白:Spring根本没有为每一个注解配置一个解析类,它用的是一套“少量处理器 + 统一处理策略 + 元注解驱动”的架构。这篇文章就把这个机制从原理到代码拆开讲清楚,适合正在准备Spring面试、想读Spring源码但找不到突破口、或者准备做自定义注解和框架封装的人。看完全文你不仅能回答开头那个问题,还能自己写一个“一套处理机制搞定多个注解”的迷你框架。

1. 先给结论:不是每个注解都要有对应解析类

1.1 从@Service说起,大半个Spring都没有“专用解析类”

先看一个最简单的例子:@Service是被谁解析的?很多人的第一反应是Spring为它写了一个独立类。实际上@Service、@Repository、@Controller、@Configuration这一车组件注解,Spring根本没有给它们分别写解析器,而是共用了一个统一扫描器ClassPathBeanDefinitionScanner。

这个扫描器在扫描包时,内部维护了一组includeFilter,默认包含一个AnnotationTypeFilter(Component.class)。它的判断逻辑是:只要某个类上标了@Component,或者标了“被@Component元注解标注的注解”,就算候选Bean。也就是说@Service之所以能被扫描到,是因为@Service自己标了@Component。Spring判断的是“你有没有带@Component元注解”,而不是“你是不是叫@Service”。

这就解释了一个初学者很容易忽略的现象:你在一个类上自定义一个注解@MyComponent,然后在@MyComponent上标@Component,这个类不需要你写任何处理器,Spring扫描器就会自动把它当成Bean。你的自定义注解根本没有对应的解析类,一样工作得很好。

同样的道理也适用于@Value和@Autowired。它们共用一个AutowiredAnnotationBeanPostProcessor;@Resource、@PostConstruct、@PreDestroy这些JSR标准注解,又共用一个CommonAnnotationBeanPostProcessor。一个处理器管一批注解,而不是一个注解配一个处理器。

1.2 大量注解是被“批量处理器”扫进去的

所以更准确的说法是:Spring把处理机制分成了有限的几个角色,每个角色可以同时处理多种注解。下面这些是Spring容器里真正在干活的核心处理器:

  • AutowiredAnnotationBeanPostProcessor:处理@Autowired、@Value,如果classpath里有javax.inject包,还会处理@Inject。
  • CommonAnnotationBeanPostProcessor:处理@Resource、@PostConstruct、@PreDestroy。
  • ConfigurationClassPostProcessor:处理@Configuration、@Import、@ComponentScan、@Bean、@PropertySource等一系列配置类注解。
  • ScheduledAnnotationBeanPostProcessor:处理@Scheduled。
  • AsyncAnnotationBeanPostProcessor:处理@Async。
  • EventListenerMethodProcessor:处理@EventListener。

注意看这些命名,几乎都是XxxAnnotationBeanPostProcessor,而不是XxxAnnotationResolver。Spring把一个注解的处理时机绑定到了某个“Bean生命周期阶段”,再用一个后处理器接住这个阶段里出现的所有注解。好处是显而易见的:注册一次处理器,覆盖一批注解;新增注解不需要新增处理器,除非你要处理一种全新的语义。

还有一个佐证:Spring 5.1之前有个RequiredAnnotationBeanPostProcessor专门处理@Required,后来这个注解被移除了,原因是官方认为它容易和构造器注入混淆,误用成本高。它从头到尾就只有一个注解一个处理器,但这是极少数。如果你去翻Spring仓库源码,注解接口几十个,定义在org.springframework.context里的处理器类远远少于注解数量,这本身就说明不是一对一的。

1.3 注解加在哪里,就决定了它由哪个阶段处理

要彻底理解这个问题,得先建立“注解位置决定处理时机”的心智模型。一个注解可以加在类上、字段上、方法上、参数上,Spring对它们的处理阶段完全不同:

  • 加在类上:影响的是BeanDefinition的“注册”。典型如@Component,要么靠扫描器在包扫描阶段把类变成BeanDefinition,要么靠ConfigurationClassPostProcessor解析@Configuration类。
  • 加在字段上:影响的是依赖注入。典型如@Autowired和@Value,它们在Bean实例化之后、初始化之前被后处理器通过反射写字段。
  • 加在方法上:可能是初始化回调,也可能是AOP拦截。@PostConstruct是在初始化阶段被调用的;@Transactional则要交给AOP代理,在方法执行前后做事务增强。
  • 加在方法参数上:常见就是@RequestParam这类Web层的注解,由专门的HandlerMethod参数解析器处理,这又是另一套机制。

理解了这条链路,就会发现“一个注解一个解析类”这种想法本身就不合理。Spring需要的是:扫描器管注册、后处理器管注入和生命周期回调、AOP代理管拦截。一种注解甚至可能被多个机制在不同阶段各处理一次。比如@Configuration,它既会被扫描器当作普通Bean注册,又会被ConfigurationClassPostProcessor专门解析配置语义。

2. 内置注解与处理器:一张对照表看清全貌

2.1 常见注解与处理入口对照表

我把Spring中常见的注解按处理机制整理成了一张表,看这张表能快速建立全局观:

注解处理入口处理阶段
@Component、@Service、@Repository、@ControllerClassPathBeanDefinitionScanner+AnnotationTypeFilter(Component.class)包扫描注册BeanDefinition
@Configuration、@Import、@ComponentScan、@Bean、@PropertySourceConfigurationClassPostProcessor+ConfigurationClassParser配置解析阶段生成BeanDefinition
@Autowired、@Value、@InjectAutowiredAnnotationBeanPostProcessor实例化后的属性注入
@Resource、@PostConstruct、@PreDestroyCommonAnnotationBeanPostProcessorJSR注解桥接,属性注入与生命周期回调
@Conditional、@ProfileConditionEvaluator+ 具体的Condition实现配置解析阶段判断是否注册Bean
@TransactionalProxyTransactionManagementConfiguration+TransactionInterceptorAOP代理拦截方法
@AsyncAsyncAnnotationBeanPostProcessor+ AOP Advisor实例化后创建代理
@ScheduledScheduledAnnotationBeanPostProcessor容器启动后注册计划任务
@EventListenerEventListenerMethodProcessor容器启动后注册事件监听器
自定义注解(类级)BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessorBeanDefinition处理阶段
自定义注解(字段级)InstantiationAwareBeanPostProcessor实例化后属性注入
自定义注解(方法级)MethodInterceptor、AOP切面代理调用阶段

这张表里真正叫“解析类”的东西其实很少,更多的是“后处理器”和“解析器”。哪怕是ConfigurationClassPostProcessor这种名字里带“PostProcessor”的,它的核心工作也是解析配置类,而不是逐注解解析。

2.2 元注解驱动:为什么@Component能带出一整个“注解家族”

@Service这类注解没有自己的解析器,还能正常工作,核心靠的是Spring的元注解机制。

Spring在判断一个类是不是候选组件时,走的逻辑大致是这样的:读取目标类的AnnotationMetadata,然后把注解属性转成MultiValueMap,例如@AliasFor声明的属性覆盖关系都会被解析出来。接着递归判断这个注解上有没有@Component元注解。AnnotationTypeFilter的match方法里,如果元注解链上出现了Component.class.getName(),就会返回匹配成功。

所以@Service能被识别,完全是因为@Service头上顶着@Component。我可以在自己的注解上写一个@Component,或者让自定义注解组合@Service,这个类就能被扫描到。记住这个套路,后面你自定义注解时,如果只是想让某个类被Spring当成Bean管理,根本不需要写处理机制。

2.3 处理器不是“解析类”,而是“生命周期参与者”

“解析类”这个说法容易误导人。在Spring源码里,你看到的角色名不是Resolver,而是BeanPostProcessor、BeanFactoryPostProcessor、Advisor、Condition、ImportSelector。这些角色是按“处理能力”和“处理时机”划分的,不是按“注解”划分。

BeanPostProcessor是Bean实例化前后参与的;BeanFactoryPostProcessor是BeanDefinition注册完成后参与的;Advisor是AOP代理创建时参与的;Condition是配置解析时参与的。一个角色可以处理多个注解,比如CommonAnnotationBeanPostProcessor在postProcessMergedBeanDefinition里会把@Resource和@PostConstruct一起解析好记下来。

反过来,一种注解也可能被多个角色处理两次。比如@EnableScheduling这个组合注解,它通过@Import(SchedulingConfiguration.class)把ScheduledAnnotationBeanPostProcessor引入容器,而@Import本身又是ConfigurationClassParser在处理。你甚至可以理解为@EnableXXX注解系列是“注解触发了配置类的加载”,而不是“被某个解析类解析”。

3. 底层运转方式:注册入口与处理阶段剖析

3.1 一切从AnnotationConfigUtils开始

要搞清楚这些处理器是怎么进容器的,直接看AnnotationConfigUtils.registerAnnotationConfigProcessors方法。这是整个注解驱动机制真正的“总入口”。

AnnotatedBeanDefinitionReader在构造时就会调用这个方法,把一批内置的后处理器注册到容器里。使用AnnotationConfigApplicationContext时,这个构造动作在容器启动早期就会发生。如果你用ClassPathXmlApplicationContext配合<context:annotation-config/>,底层也会走到同一个方法。

这个方法做的事非常直接:先判断当前注册表里是否已经有ConfigurationClassPostProcessor,没有就注册一个;再判断有没有AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、EventListenerMethodProcessor等,没有就依次注册。它还顺带注册了PersistenceAnnotationBeanPostProcessor,如果classpath里有JPA相关类的话。

注意这里用的是registerWithGeneratedName,BeanName是Spring自动生成的。我当初翻源码时第一反应是:为什么注册BeanPostProcessor要放在AnnotationConfigUtils里,而不是让Spring容器自检?答案很简单:这些处理器是“处理Bean的Bean”,必须让它们自己先于普通业务Bean完成注册,才能去处理别的Bean。如果靠后续扫描再注册,扫描器自己都还没跑起来,整个机制就没法工作。

3.2 BeanFactoryPostProcessor与BeanPostProcessor的分工

这两组后处理器是理解Spring注解机制的枢纽,但很多人会把它们搞混。我用一个比喻解释:BeanFactoryPostProcessor处理的是“图纸”,在房子还没开工之前,你可以改图纸上写的面积、格局、装修方式;BeanPostProcessor处理的是“盖好的房子”,在交付前后你可以给房子装门锁、贴瓷砖、加防火墙,甚至把它包成智能家居。

ConfigurationClassPostProcessor就是最典型的BeanDefinitionRegistryPostProcessor,它在invokeBeanFactoryPostProcessors阶段最先执行,把@Configuration类里的@Bean方法变成BeanDefinition,把@Import引入的类也变成BeanDefinition。这个动作发生以后,普通@Component扫描出来的BeanDefinition才陆续注册。所以@Bean方法产生的Bean和@Component扫描出来的Bean在时序上是有先后关系的。

而AutowiredAnnotationBeanPostProcessor属于InstantiationAwareBeanPostProcessor,它在Bean实例化之后、初始化前后介入,通过postProcessProperties方法找到字段上的@Autowired和@Value,完成反射注入。@PostConstruct也是这个处理器在初始化回调阶段找到的。

3.3 AOP注解和条件注解又在哪里介入

有人会问:@Transactional连BeanPostProcessor都不是,它是怎么生效的?答案是通过AOP。

Spring在处理@EnableTransactionManagement时,会向容器注册一个BeanFactoryTransactionAttributeSourceAdvisor。这个Advisor匹配“类或方法上是否存在@Transactional注解”,匹配成功就给Bean创建代理。代理对象在方法调用时进入TransactionInterceptor,由它来开启、提交、回滚事务。这里没有“事务注解解析类”,而是由切面负责“识别注解 + 增强方法”。

条件注解@Conditional的介入点又不一样。ConfigurationClassParser在解析配置类时,会调用ConditionEvaluator.shouldSkip来判断当前配置类或者配置类中的@Bean方法是否满足条件。如果@Conditional判断不通过,对应的BeanDefinition根本不会注册。@Profile其实就是@Conditional(ProfileCondition.class)的一个组合注解。

所以你看,Spring对不同注解的介入点是刻意分散的:类注册阶段、配置解析阶段、Bean实例化阶段、AOP代理阶段、事件监听阶段,都各有一套机制。一套机制支持多种注解,这是它的核心设计原则。

4. 手写示例:一个处理机制同时搞定多个自定义注解

4.1 场景设定:类级注解和字段级注解一起处理

光讲原理不够,我写一个可以本地跑通的小例子,证明“一套处理机制可以服务多个注解”。

假设我们要做一个内部小框架,需要两类自定义注解:

  • @MyMetrics:加在类上,标记这个类需要被改造,比如修改它的Bean作用域。
  • @MyValue:加在字段上,用来从环境配置里读取值,类似@Value。
  • @MyInject:加在字段上,用来按类型注入依赖,类似简化版@Autowired。

我们不打算为每个注解写一个解析类,而是用一个BeanFactoryPostProcessor处理类级注解,再用一个InstantiationAwareBeanPostProcessor处理字段级注解。

4.2 类级注解:BeanFactoryPostProcessor统一扫描BeanDefinition

先定义注解:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyMetrics { String name() default "default"; }

然后定义处理器:

public class MyMetricsBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { String[] beanNames = beanFactory.getBeanDefinitionNames(); for (String beanName : beanNames) { BeanDefinition bd = beanFactory.getBeanDefinition(beanName); if (bd instanceof AnnotatedBeanDefinition annotatedBd) { AnnotatedTypeMetadata metadata = annotatedBd.getMetadata(); if (metadata.isAnnotated(MyMetrics.class.getName())) { Map<String, Object> attrs = metadata.getAnnotationAttributes(MyMetrics.class.getName()); String name = attrs == null ? "default" : (String) attrs.get("name"); System.out.println("[MyMetrics] beanName=" + beanName + ", name=" + name); // 统一修改Bean作用域 bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); } } } } }

这段代码的用法很值得说一下。AnnotatedBeanDefinition里的Metadata在实例化之前就能拿到类上的注解信息,因为你不需要把类加载成Class,Spring通过ASM读取字节码元数据就能判断注解是否存在。这样可以避免早期触发类加载,启动效率更高。遍历所有BeanDefinition,发现带@MyMetrics的就统一改作用域,这就是“一种机制处理一类注解”的典型样例。

4.3 字段级注解:一个后处理器处理两个注解

再定义两个字段注解:

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MyValue { String value(); } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MyInject { }

核心处理器:

public class AutoInjectBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter { private final ConfigurableListableBeanFactory beanFactory; public AutoInjectBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { Class<?> clazz = bean.getClass(); for (Field field : clazz.getDeclaredFields()) { MyValue myValue = field.getAnnotation(MyValue.class); if (myValue != null) { String resolved = beanFactory.resolveEmbeddedValue(myValue.value()); field.setAccessible(true); try { field.set(bean, convertIfNecessary(resolved, field.getType())); } catch (IllegalAccessException e) { throw new RuntimeException(e); } continue; } MyInject myInject = field.getAnnotation(MyInject.class); if (myInject != null) { Object dependency = beanFactory.getBean(field.getType()); field.setAccessible(true); try { field.set(bean, dependency); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } return pvs; } private Object convertIfNecessary(String value, Class<?> targetType) { if (targetType == int.class || targetType == Integer.class) { return Integer.parseInt(value); } return value; } }

这里继承InstantiationAwareBeanPostProcessorAdapter而不是直接实现InstantiationAwareBeanPostProcessor,是为了少写无用方法,适配器类把所有方法都空实现了,我们只需要覆盖postProcessProperties。这个方法在属性注入阶段被调用,Spring原本的@Autowired处理器也是在这种地方干活的。关键点是:一个循环里同时检查@MyValue和@MyInject,证明一个处理器可以批量处理多个注解,而不需要每个注解一个类。

4.4 注册处理器时的三个注意点

这个例子虽然简单,但真要在Spring Boot工程里跑起来,有几个细节不能踩坑:

第一,BeanPostProcessor如果通过@Bean注册,要尽量用static方法。原因是普通@Bean方法所在的配置类本身需要被实例化,而BeanPostProcessor必须在其他普通Bean实例化之前完成注册。用静态方法可以避免配置类实例化时机影响处理器注册时机。

@Configuration public class DemoConfiguration { @Bean public static MyMetricsBeanFactoryPostProcessor myMetricsPostProcessor() { return new MyMetricsBeanFactoryPostProcessor(); } @Bean public AutoInjectBeanPostProcessor autoInjectPostProcessor(ConfigurableListableBeanFactory beanFactory) { return new AutoInjectBeanPostProcessor(beanFactory); } }

第二,不要在BeanPostProcessor的回调方法里对同一个类调用getBean,非常容易触发循环依赖。比如你正处理的bean是DemoService,回调里又beanFactory.getBean(DemoService.class),可能直接把容器卡死。

第三,处理字段时要跳过节流和静态字段。实际框架里应该用ReflectionUtils的doWithLocalFields统一遍历,并判断Modifier.isStatic,避免错误注入。我在公司写组件时就因为没跳过静态字段,把一个静态缓存字段覆盖成了null,排查了半天。

5. 踩坑记录与排查思路

5.1 自定义注解“没生效”的最常见原因

自己定义了一个注解,启动一看根本没反应,这种事我碰到不下十次。大部分情况下不是Spring没能力处理,而是处理机制没有触发。我把常见原因整理成一张速查表:

现象可能原因解决方向
类上注解没被扫描到类没有被任何组件扫描机制发现给自定义注解补@Component元注解,或确认扫描包路径包含目标类
字段注解没赋值处理器没有注册进容器检查@Bean注册方法是否生效,确认处理器确实在BeanFactory里
方法注解没拦截缺少代理机制确认是否引入AOP相关配置,方法是不是final
注解一点反应都没有RetentionPolicy.RUNTIME缺失Spring必须用运行时注解,SOURCE或CLASS级别是拿不到的
注解在工具类里失效实例不是Spring托管的通过ApplicationContext.getBean获取,或者手动传入依赖

其中最容易忽略的是@Retention(RetentionPolicy.RUNTIME)。如果你自定义注解时习惯性写成RetentionPolicy.CLASS,Spring应用启动时通过反射拿不到这个注解,后处理器怎么检查都是空。这个错误连老手都会偶尔犯,建议自定义注解时第一行就加上运行时保留。

5.2 一个很经典的注入失败现场

问得最多的问题是:为什么@Autowired在new出来的工具类里会报空指针?

原因很简单:@Autowired是由AutowiredAnnotationBeanPostProcessor在Bean生命周期里完成的,容器创建Bean实例时才会走到注入逻辑。你用new违反了这个流程,Spring根本不知道这个对象存在,自然不可能给它注入依赖。

正确做法是让工具类也成为Spring Bean,或者通过构造器把依赖传进去。实在要写静态工具类,就用一个ApplicationContextHolder在容器启动时把ApplicationContext存起来,再在静态方法里取Bean。但这只是兜底方案,最干净的还是把工具类也交给容器管理。

5.3 怎么快速定位“某个注解到底怎么处理的”

看注解源码是一个很高效的习惯。@Autowired的Javadoc里就写了“处理它的类是AutowiredAnnotationBeanPostProcessor”;@EventListener的Javadoc会指向EventListenerMethodProcessor。Spring官方在注解注释里会明确标注对应的处理类,这比你去搜博客要可靠得多。

如果是一个组合注解,比如@EnableCaching,点进去看就发现它标了@Import(CachingConfigurationSelector.class),那它的处理机制就在CachingConfigurationSelector里。通过@Import引入配置类或者ImportSelector,是Spring提供给框架开发者最常用的扩展口子。遇到不熟悉的@EnableXXX,一律先看有没有@Import,这招对读Spring Boot自动配置源码也很管用。

如果注解本身没有@Import也没有@Component元注解,那就要看它被哪个BeanPostProcessor的硬编码判断捕获了。你可以全局搜索注解名,看哪些类引用了它的class对象,基本就能锁定处理机制。

5.4 排查时的小技巧:直接把处理器列表打出来

在排查BeanPostProcessor注册问题时,我习惯在启动早期打印一下当前容器已经注册了哪些BeanPostProcessor。方法是在BeanFactoryPostProcessor里遍历:

@Component public class ProcessorDebugPrint implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { System.out.println("--- BeanPostProcessor list ---"); beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); } }

输出里你能看到自己定义的处理器是否已经注册,以及它和其他内置处理器之间的顺序。实际项目中我还会在自定义后处理器的回调开头打beanName日志,确认它到底处理了哪些类。经常有同事自定义处理器写了但没加@Component,容器里根本没有这个处理器,离线看代码看不出来,打印一次列表立刻现形。

我做公司内部基础组件时,也沿用了Spring这套思路:把注解分成“类标记型”“配置型”“注入型”“增强型”四类,每一类对应一个处理机制,而不是逐注解地写处理器。这几年下来,这个思路一直很稳。如果你也想做自定义注解相关的封装,记住一个原则:先确定注解作用于哪个生命周期阶段,再决定用扫描器、BeanFactoryPostProcessor还是BeanPostProcessor,不要一上来就想着写“某个注解的解析类”。想清楚这一点,Spring的注解扩展之路就顺了。

返回列表