Spring注解解析机制:不是每个注解都要写一个解析类
Spring中每一个注解都需要有一个对应的解析的类吗在我刚开始啃Spring源码的时候这个问题困扰了我很久。翻到AutowiredAnnotationBeanPostProcessor时我下意识觉得Spring是为Autowired单独写了一个解析类那照这么推Service是不是也得有一个ServiceAnnotationPostProcessorTransactional是不是还得有一个事务注解解析器如果真是这样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。它们共用一个AutowiredAnnotationBeanPostProcessorResource、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、ControllerClassPathBeanDefinitionScannerAnnotationTypeFilter(Component.class)包扫描注册BeanDefinitionConfiguration、Import、ComponentScan、Bean、PropertySourceConfigurationClassPostProcessorConfigurationClassParser配置解析阶段生成BeanDefinitionAutowired、Value、InjectAutowiredAnnotationBeanPostProcessor实例化后的属性注入Resource、PostConstruct、PreDestroyCommonAnnotationBeanPostProcessorJSR注解桥接属性注入与生命周期回调Conditional、ProfileConditionEvaluator 具体的Condition实现配置解析阶段判断是否注册BeanTransactionalProxyTransactionManagementConfigurationTransactionInterceptorAOP代理拦截方法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相关类的话。注意这里用的是registerWithGeneratedNameBeanName是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())) { MapString, 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在实例化之前就能拿到类上的注解信息因为你不需要把类加载成ClassSpring通过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.CLASSSpring应用启动时通过反射拿不到这个注解后处理器怎么检查都是空。这个错误连老手都会偶尔犯建议自定义注解时第一行就加上运行时保留。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的注解扩展之路就顺了。