几年前带过一个转岗过来的 Java 开发,他指着基础工程里一段 Spring 代码问:这个实现 ApplicationContextAware 的接口到底是干嘛的?我直接 @Autowired 一个 ApplicationContext 不也一样吗?我当时愣了一下——不是因为他问得幼稚,而是这个问题我最早接触 Spring 时也想过,只是后来用多了就习以为常,从没认真把这件事拆开讲清楚。
Spring 里带 Aware 后缀的接口一抓一大把:BeanNameAware、BeanFactoryAware、EnvironmentAware、EmbeddedValueResolverAware……看起来都是"感知某个资源"的意思,但很多人分不清它们什么时候回调、和依赖注入有什么本质区别、Spring 又是怎么把资源塞给 Bean 的。这篇文章就把 Spring 中 Aware 的用法和实现机制一次性讲透:先说明它解决什么问题,再把常用接口逐个盘点,接着从源码看触发链路,然后用完整示例验证执行顺序,最后给出自定义 Aware 的实现方案和我在实际项目里踩过的坑。适合想真正搞懂 Spring 生命周期的人,也适合面试前梳理这种"大家都用过但说不清"的知识点。
1. Aware到底是什么:它解决的痛点和设计意图
1.1 把Bean想象成舞台演员,Aware就是导演递过来的台本
Spring 容器管理着所有 Bean 的完整生命周期:创建、属性填充、初始化、销毁。在这个体系里,Bean 是"被动的",它不知道自己在哪个容器里,也不知道容器给自己起了什么名字,更不知道容器外面还站着多少兄弟 Bean。大多数业务 Bean 根本不需要知道这些——但有那么一小部分代码,确实需要在某个时刻拿到容器的某种资源:比如想知道自己的 Bean 名称、想读环境变量、想发布事件。
这就是 Aware 存在的意义。Aware 在英文里就是"感知",Spring 用它做了一组"回调约定":如果你的类实现了某个 Aware 接口,容器在初始化该 Bean 的特定阶段,就会主动调用接口里定义的 setter 方法,把对应的资源作为参数传进来。
用舞台打比方:演员不需要自己跑去找灯光师、音响师,导演(容器)在上场前会按名单把这些资源依次递给他。演员只需要提前声明"我需要知道舞台布局",导演就保证在合适的时候告诉你。这个"提前声明"就是实现 Aware 接口,"导演递资源"就是容器回调。理解了这个模型,后面再看源码和自定义 Aware 都很顺。
1.2 为什么不能只用@Autowired,非要搞一套回调
这是个很自然的问题。ApplicationContext 本身也是个 Bean,按说 @Autowired 直接注入就行,何必绕一圈实现接口?
关键差别在于两点。
第一,时机不同。@Autowired 属于属性填充(populateBean)阶段的处理,它解决的是"静态依赖"——Bean 的字段需要某个引用。而 Aware 回调发生在初始化流程中,其中一部分(比如 BeanNameAware、BeanClassLoaderAware)甚至在任何 BeanPostProcessor 之前就会触发。如果你的初始化逻辑必须依赖这些资源,在 @Autowired 的场景下很难处理,因为你不一定能保证赋值顺序。
第二,解耦程度不同。实现 BeanNameAware 的类,只知道容器把名字传给自己,代码里不会依赖 ApplicationContext,也不会依赖注解处理器。而 @Autowired 一个 ApplicationContext 字段,你的类就永久地和 Spring 的容器 API 绑在一起。Aware 这种"小接口 + 回调"的模式更克制,更容易被框架复用来做扩展点。
可以理解为:@Autowired 是"自己伸手去拿",Aware 是"容器主动递给你"。对业务代码来说伸手拿更省事,但对框架基础设施来说,被动接收才能保证时机可控、依赖最少。这也是 Spring 内部大量组件宁愿实现一堆 Aware 也不写一堆 @Autowired 的原因。
2. 常用Aware接口速查:回调方法、时机与典型用途
2.1 一份速查表:哪些Aware在你手边
先给一份能直接收藏的速查表。Spring 里常见的 Aware 接口其实就这些,记住它们的回调方法和"能拿到什么",用的时候就不会愣神。
| 接口 | 回调方法 | 获取的资源 | 典型场景 |
|---|---|---|---|
| BeanNameAware | setBeanName(String) | 当前 Bean 在容器中的名字 | 日志前缀、内部标识 |
| BeanClassLoaderAware | setBeanClassLoader(ClassLoader) | 当前 Bean 使用的类加载器 | 动态类加载、SPI 扩展 |
| BeanFactoryAware | setBeanFactory(BeanFactory) | 所在 BeanFactory 实例 | 早期容器操作、嵌套容器 |
| ApplicationContextAware | setApplicationContext(ApplicationContext) | 应用上下文 | getBean、事件、资源、环境 |
| EnvironmentAware | setEnvironment(Environment) | 环境配置访问器 | 读取自定义配置项 |
| EmbeddedValueResolverAware | setEmbeddedValueResolver(StringValueResolver) | 占位符解析器 | 运行时解析 ${...} 表达式 |
| ResourceLoaderAware | setResourceLoader(ResourceLoader) | 资源加载器 | 读取 classpath 资源 |
| ApplicationEventPublisherAware | setApplicationEventPublisher(ApplicationEventPublisher) | 事件发布器 | 发布业务事件 |
| MessageSourceAware | setMessageSource(MessageSource) | 国际化消息源 | 获取本地化消息 |
2.2 最常用的三个:BeanNameAware、ApplicationContextAware、EnvironmentAware
BeanNameAware 门槛最低,只有一个 setBeanName。用在哪?最常见的是日志和标识。比如某个通用组件同时被多个模块以不同 Bean 名注册,你想让日志里带上"我是谁",实现它就行。这类需求用 @Autowired 注入一个字符串反而奇怪,Aware 是更自然的方式。
ApplicationContextAware 是业务代码里见得最多的一个。getBean、publishEvent、getResource、getEnvironment 全都能干。不过能力越大,责任越大,正因为它什么都能拿到,最容易把业务类写得依赖容器。后文会专门讲怎么权衡。
EnvironmentAware 在 Spring Boot 里很实用。读配置不必死磕 @Value,你可以在回调里动态拼 key、做条件判断,比如"根据某个前缀一次性抓取所有配置项"。我曾经做一个多数据源动态切换组件,就是在 setEnvironment 里把各个数据源的前缀配置统一解析出来的,比写几十个 @Value 字段清爽得多。
2.3 相对冷门但关键的两个:占位符解析与事件发布
EmbeddedValueResolverAware 知道的人不多,但能力很实用。它给你一个 StringValueResolver,可以在运行时解析 ${...} 占位符。比如规则引擎里存了一段模板字符串,想动态替换成配置里的真实值,直接用 resolver.resolveStringValue("${timeout}") 就能搞定。这比手动解析 properties 文件优雅得多。
ApplicationEventPublisherAware 则适合那种"我只想发事件,不想把整个容器拖进来"的场景。Spring 的内置事件机制本身就依赖 ApplicationEventPublisher,与其实现 ApplicationContextAware 拿整个容器再去 publishEvent,不如直接实现这个更精准的接口——语义清晰,也方便测试时用一个轻量实现替换。
这两个接口背后正好体现了 Aware 设计的一个原则:能按需拿到"最小资源",就不要抛给你"整个容器"。写框架时如果也能按这个原则设计回调,你的使用者会轻松很多。
3. 触发机制源码拆解:两条回调链其实截然不同
3.1 第一条触发链:invokeAwareMethods的硬编码处理
打开 spring-beans 模块的AbstractAutowireCapableBeanFactory#initializeBean,最先看到的是一个叫invokeAwareMethods的私有方法。
private void invokeAwareMethods(final String beanName, final Object bean, @Nullable final BeanFactory beanFactory) { if (bean instanceof Aware) { if (bean instanceof BeanNameAware) { ((BeanNameAware) bean).setBeanName(beanName); } if (bean instanceof BeanClassLoaderAware) { ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); } if (bean instanceof BeanFactoryAware) { ((BeanFactoryAware) bean).setBeanFactory(beanFactory); } } }注意三点。其一,这个方法只处理 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 这三个接口。其二,它是直接硬编码判断,没有经过任何扩展机制。其三,BeanFactoryAware 传的是当前 Bean 所在的 BeanFactory——如果你跑在 ApplicationContext 场景下,这个 beanFactory 参数通常是容器内部的 DefaultListableBeanFactory,而不是 ApplicationContext 本身。很多人实现完 BeanFactoryAware 之后发现拿到的对象和 ApplicationContext 不是同一个,就是这个原因。
为什么硬编码?因为这三个资源属于 BeanFactory 级别,只要是 Spring 容器就一定具备,没必要再通过 BeanPostProcessor 绕一圈。把它们放在 initializeBean 的最前面处理,也保证了执行顺序:在其它 BeanPostProcessor 还没开始干活的时候,Bean 就已经知道自己的名字了。
3.2 第二条触发链:ApplicationContextAwareProcessor 如何介入
那 ApplicationContextAware、EnvironmentAware、ResourceLoaderAware、MessageSourceAware、ApplicationEventPublisherAware 这些去哪了?它们走的是另一条完全不同的路:BeanPostProcessor。
ApplicationContext 容器在刷新准备阶段会注册一个内置的ApplicationContextAwareProcessor,它的postProcessBeforeInitialization方法会对所有 Bean 逐个做 instanceof 判断,命中了就调用对应的 Aware 回调。源码骨架大致如下:
if (bean instanceof EnvironmentAware) { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof EmbeddedValueResolverAware) { ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); } if (bean instanceof ResourceLoaderAware) { ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); } if (bean instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); } if (bean instanceof MessageSourceAware) { ((MessageSourceAware) bean).setMessageSource(this.applicationContext); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); }注意一个容易踩雷的点:如果你用的是纯粹的 BeanFactory 实现(比如 XmlBeanFactory),ApplicationContextAware 是不会生效的,因为 ApplicationContextAwareProcessor 是 ApplicationContext 容器才注册的处理器。现在虽然基本没人直接操作 BeanFactory,但读异常栈时,这个知识能帮你快速定位为什么回调没触发。
再看一个细节:上面的代码里,EnvironmentAware 的入参是 environment 对象,而 ResourceLoaderAware 和 ApplicationContextAware 的入参直接就是 applicationContext 本身。ApplicationContext 本身就实现了 ResourceLoader 和 ApplicationEventPublisher,所以 Spring 在这把同一个对象以不同身份投递给了不同接口。这种"接口收窄"的写法很值得学习。
3.3 两条链在完整生命周期中的位置
把两条链放进 initializeBean 的完整流程里看:
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { // ... invokeAwareMethods(beanName, bean, factory); // 第一链:三个基础 Aware // ... wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); // └─ ApplicationContextAwareProcessor 在这里执行 (第二链) // └─ CommonAnnotationBeanPostProcessor 在这里执行 @PostConstruct invokeInitMethods(beanName, wrappedBean, mbd); // └─ InitializingBean.afterPropertiesSet // └─ 自定义 init-method wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); return wrappedBean; }结论很清楚:initializeBean 之前还有 createBeanInstance(实例化)、populateBean(属性填充)。Aware 回调整体发生在属性填充之后、初始化方法之前。而第一链的 invokeAwareMethods 又先于第二链的所有 BeanPostProcessor。也就是说 BeanNameAware 永远比 ApplicationContextAware 先回调,这一点下面用示例直接验证。
4. 完整示例:利用Aware观察Spring Bean的初始化顺序
4.1 一个能打印执行顺序的示例Bean
实践出真知。定义这样一个 Bean,把关键阶段全打上日志:
public class AwareOrderDemo implements BeanNameAware, ApplicationContextAware, InitializingBean { private Environment environment; @Autowired public void setEnvironment(Environment environment) { this.environment = environment; System.out.println("0. @Autowired setter 属性注入 -> " + environment.getClass().getSimpleName()); } @Override public void setBeanName(String name) { System.out.println("1. BeanNameAware.setBeanName -> " + name); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("2. ApplicationContextAware.setApplicationContext -> " + applicationContext.getClass().getSimpleName()); } @PostConstruct public void postConstruct() { System.out.println("3. @PostConstruct 执行"); } @Override public void afterPropertiesSet() { System.out.println("4. InitializingBean.afterPropertiesSet 执行"); } public void customInitMethod() { System.out.println("5. 自定义 init-method 执行"); } }再用配置类注册它,指定 initMethod:
@Configuration public class AwareOrderConfig { @Bean(initMethod = "customInitMethod") public AwareOrderDemo awareOrderDemo() { return new AwareOrderDemo(); } }启动一个只包含这个配置的容器:
public class AwareOrderDemoTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AwareOrderConfig.class); context.getBean(AwareOrderDemo.class); context.close(); } }4.2 从输出日志看生命周期的三个结论
控制台依次输出:
0. @Autowired setter 属性注入 -> StandardEnvironment 1. BeanNameAware.setBeanName -> awareOrderDemo 2. ApplicationContextAware.setApplicationContext -> AnnotationConfigApplicationContext 3. @PostConstruct 执行 4. InitializingBean.afterPropertiesSet 执行 5. 自定义 init-method 执行这个输出基本就是 Spring Bean 初始化阶段的一张完整时间线。
第一个结论:属性注入最早,发生在 populateBean 阶段,早于所有 Aware。第二个结论:BeanNameAware 先于 ApplicationContextAware,对应源码里 invokeAwareMethods 在 BeanPostProcessor 之前执行。第三个结论:@PostConstruct 先于 InitializingBean 和 init-method,原因是 @PostConstruct 也是通过 BeanPostProcessor 的 postProcessBeforeInitialization 实现的,而后两个是在 invokeInitMethods 里处理的。
把这个时间线背下来,Spring 生命周期相关的面试题基本就稳了。面试候选人时,只要让他大概说出这几个阶段的先后,再追问一句"@PostConstruct 和 afterPropertiesSet 哪个先执行、为什么",就能看出他是真读过源码还是只会背结论。
4.3 用Aware拿容器后,真正值得做的三件事
实现 ApplicationContextAware 之后,我实际用过且觉得靠谱的是这三件事:
一是按需 getBean 获取原型 Bean。业务里确实有"某段逻辑需要动态获取一个非单例 Bean"的需求,比如状态机需要根据当前状态创建不同的策略对象,用静态注入的方式不太方便。
二是 publishEvent 发布业务事件。模块之间要解耦,又不想引入消息中间件,容器内置事件就是一个轻量方案。通过 ApplicationContext.publishEvent 发布事件,监听端用 @EventListener 接收,整套链路干净利落。
三是 getResource 加载 classpath 资源。比如读取 jar 包里的模板文件、校验规则,用 applicationContext.getResource("classpath:xxx.json") 比手工拼路径稳妥得多。
注意,这些能力原则上只在"绕不开"的时候用。如果某个业务 Bean 只是偶尔需要 getBean,优先考虑用 ObjectProvider 注入,不要实现 ApplicationContextAware。
我平时还会准备一个 SpringContextHolder 静态工具类,把容器存成静态变量,方便在非 Spring 管理的场景(定时任务、工具类、异常处理器里)拿到 Bean:
@Component public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext context) { applicationContext = context; } public static <T> T getBean(Class<T> requiredType) { return applicationContext.getBean(requiredType); } }不过这个类在第六节会重点提醒:它能解决入口问题,也容易制造设计垃圾。
5. 自定义Aware接口:把Spring这套设计搬到自己的框架里
5.1 设计一个业务化的回调接口
Spring 的 Aware 机制不是只能被 Spring 自己用,这套"接口 + 回调 + BeanPostProcessor 装配"的组合,完全可以平移到自己的框架里。场景很常见:你在做一套内部链路追踪组件,希望业务 Bean 如果想生成 TraceId,就实现一个回调接口来获得统一的生成器,而不是让业务方直接 new 一个。
先定义业务接口:
public interface TraceIdGeneratorAware { void setTraceIdGenerator(TraceIdGenerator traceIdGenerator); }这个接口就是你和业务方之间的"约定契约"。业务方不需要知道生成器怎么来的、从哪注册的,他只需要实现 setTraceIdGenerator,容器就会在合适的时机把这个依赖送上门。这正是 Spring 官方 Aware 的原始姿态。
5.2 用BeanPostProcessor完成回调装配
接下来定义一个 BeanPostProcessor,对所有正在初始化的 Bean 做 instanceof 判断,命中就把 TraceIdGenerator 设进去:
@Component public class TraceIdGeneratorAwareProcessor implements BeanPostProcessor { private final TraceIdGenerator traceIdGenerator = new DefaultTraceIdGenerator(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof TraceIdGeneratorAware) { ((TraceIdGeneratorAware) bean).setTraceIdGenerator(traceIdGenerator); } return bean; } }选 postProcessBeforeInitialization 而不是 After,理由很简单:要和 Spring 内置 Aware 保持同一个时机。这样业务方在 @PostConstruct、afterPropertiesSet 里就一定能用上 traceIdGenerator,不会出现"初始化方法执行时还是 null"的尴尬。
有一个容易被忽略的细节:BeanPostProcessor 本身也是一个 Bean,它会在所有普通 Bean 之前被创建并注册。所以不要在这个处理器里写过重的东西,更不要让它依赖其他尚未初始化的业务 Bean,否则容易触发早期的循环依赖问题。
5.3 让自定义Aware能拿到底层容器资源
很多时候,自定义 Aware 回调的对象本身需要依赖 Spring 容器能力。比如你的框架要提供一个"业务上下文",上下文里包含容器里的其他 Bean、环境配置等。这时可以让处理器实现 ApplicationContextAware,先把容器拿到,再在回调时构建业务上下文。
public class BizContext { private final ApplicationContext applicationContext; private final String beanName; public BizContext(ApplicationContext applicationContext, String beanName) { this.applicationContext = applicationContext; this.beanName = beanName; } public ApplicationContext getApplicationContext() { return applicationContext; } public String getBeanName() { return beanName; } }@Component public class BizContextAwareProcessor implements BeanPostProcessor, ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof BizContextAware) { BizContext context = buildBizContext(beanName); ((BizContextAware) bean).setBizContext(context); } return bean; } private BizContext buildBizContext(String beanName) { return new BizContext(applicationContext, beanName); } }注意,处理器本身是 Aware 的接收者,它又是一个 BeanPostProcessor,看起来有点"绕",但 Spring 的初始化顺序能保证:ApplicationContextAwareProcessor 在容器刷新早期就已经把 applicationContext 传给了目标对象,等到业务 Bean 被处理时,处理器里的容器引用早已就位。这种组合用法在 Spring 内部实现里比比皆是。
5.4 用一段测试验证自定义回调生效
自定义机制同样可以用一个极简测试验证。准备一个实现了 BizContextAware 的测试 Bean,通过配置类注册,刷新容器后断言回调已经执行:
class BizContextAwareProcessorTest { @Test void awareShouldInjectContext() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(BizContextAwareProcessor.class, MyBizBean.class); context.refresh(); MyBizBean bean = context.getBean(MyBizBean.class); assertNotNull(bean.getBizContext()); assertEquals("myBizBean", bean.getBizContext().getBeanName()); context.close(); } }测试里注意一点:如果 MyBizBean 没有被任何扫描路径覆盖,就要手动注册进来;如果想让回调生效,BizContextAwareProcessor 也必须在当前容器中存在。注册顺序不重要,但两个 Bean 都不能漏。跑通这个测试后,你的自定义 Aware 机制就完全闭环了——本质上你已经复刻了 Spring 那套"接口 + 后处理器"的扩展模型。
6. 踩坑复盘:Aware使用中容易翻车的几个场景
6.1 构造器里读Aware资源,必踩的NPE
最典型也最容易犯的错误,是在构造器里访问 Aware 回调设置的字段或容器资源。构造器执行时,Bean 实例刚从 new 出来,Spring 还在执行 createBeanInstance,后面的 populateBean、Aware 回调都没开始,此时字段必然为 null。
我见过一个真实事故:某同事写了一个配置校验组件,构造器里调用 setEnvironment 传进来的配置做校验,结果每次启动都报空指针,排查了很久才发现是回调时机问题。正确做法是:构造器只做纯字段初始化,需要外部资源的逻辑一律放到 @PostConstruct 或 InitializingBean 里。如果一定要在构造器阶段做点什么,请改用构造器注入,让 Spring 在创建实例的时候就把依赖传进来。
6.2 原型作用域的Aware回调会反复执行
Singleton Bean 的 Aware 回调在整个容器生命周期只执行一次。但 prototype 作用域的 Bean 不一样,每次 getBean 都会走一遍完整的创建与初始化流程,Aware 回调自然也会执行多次。
这个特性在多数场景下无害,但如果你在回调里做了有状态的操作,麻烦就来了。曾经有个项目,在某个 prototype 的辅助处理类里实现 ApplicationContextAware,回调里给静态列表注册了监听处理器,结果每次 getBean 都往列表里塞一条记录,日志越打越多,内存也跟着涨。排查到根因后,把注册动作移到了 @PostConstruct 或干脆放到 getBean 之后的业务方法里才解决。记住:Aware 回调只适合做"赋值",不适合做"一次性注册"。
6.3 动态代理参与时Aware资源可能出现空值
当 Bean 被 AOP 切面拦截时,Spring 可能创建 JDK 动态代理或 CGLIB 代理。容器创建的代理对象和原始目标对象是两回事。Spring 在处理 Aware 回调时通常作用于原始目标 Bean,但如果你在代码里手动创建代理、或者在切面里试图从代理对象访问目标类的 Aware 字段,很容易拿到 null。
举个例子:某个过滤器实现类实现了 ApplicationContextAware,但它还被一个切面拦截。如果切面逻辑里强制转换目标对象去读容器字段,却发现字段是空的,多半是回调应用在了另一个对象上。对这个坑我的建议是:不要在 Aware 回调里保存需要对外暴露的公开状态,更不要让切面通过字段直取容器资源。如果切面本身需要容器访问,让切面自己实现 Aware,而不是从拦截的目标里取。
6.4 静态持有容器的工具类是把双刃剑
前面推荐的 SpringContextHolder 确实方便,但它的危害同样不小。它让模块之间的依赖从"容器装配时的可见关系"变成了"运行时随时可查的全局单例注册表",管理者根本不知道谁在用谁,单测里也没法方便地 mock。我在老项目里清理过二三十处这种 getBean 调用,基本都是当年图省事埋下的雷。
我的经验是:SpringContextHolder 只留给那些"真的无法通过注入获取 Bean"的场景,比如非 Spring 管理的线程、框架启动器、工具类里的兜底调用。业务类之间的依赖,一律用构造器注入,让依赖关系在方法签名里明明白白。
6.5 强转具体容器类型导致的兼容性事故
setApplicationContext 收到的容器类型在不同运行环境下完全不同:普通 Spring 工程可能是 AnnotationConfigApplicationContext,Spring Boot 里可能是 ConfigurableApplicationContext 的某个子类。如果你为了图方便强转成某个具体类型,换一种启动方式就炸。
正确做法是面向 ApplicationContext 接口编程,只有在确实需要子类能力时才用 instanceof 判断后转换,而且转换失败要有兜底逻辑。这个原则在任何框架代码里都适用,Aware 也不例外。
7. 实战决策:到底该用Aware、注入还是监听器
7.1 一张决策表:遇到需求先选对姿势
平时写代码时遇到"我要拿容器/资源"的念头,可以按这张表快速决策:
| 需求场景 | 推荐方式 | 不建议的方式 |
|---|---|---|
| 业务类想拿容器做 getBean | 构造器注入 / @Autowired 注入 ApplicationContext | 大量实现 ApplicationContextAware |
| 初始化完成后需要容器资源 | @PostConstruct + 依赖注入 | 在构造器里访问容器 |
| 只是想知道自己的 Bean 名 | BeanNameAware | 注入一组奇奇怪怪的元数据 |
| 读取配置项 | @Value / EnvironmentAware | 自己解析 properties,重复造轮子 |
| 启动完成后做一次性逻辑 | ApplicationRunner / SmartInitializingSingleton | ApplicationContextAware + 自定义标记位 |
| 发布业务事件 | 注入 ApplicationEventPublisher | 实现 Aware 拿整个容器再发事件 |
| 非 Spring 管理的对象要拿 Bean | SpringContextHolder,且严格限制使用面 | 到处 static getBean |
这张表背后的原则只有一句话:业务代码尽量少碰容器 API,把 Aware 留给基础设施。你越少依赖容器,代码的可测试性和可替换性就越好。
7.2 为什么Spring没有废弃Aware机制
可能有人会问:都到注解时代了,@Autowired、@Value 这么好用,为什么 Spring 没有把 Aware 废掉?因为它根本不是历史包袱,而是容器基础设施的一部分。
在注解处理器、AOP 基础设施、事务回调这些内部组件的初始化流程里,存在大量"必须是这个时机、必须是这套回调"的传递需求,不能依赖注解扫描和字段注入的复杂度。BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 这三个在 BeanFactory 级别就硬编码处理了,这意味着只要你用 Spring,这套机制就在核心路径上占有一席之地。
翻 Spring 源码时你会发现,很多内部类都是"一身 Aware":又拿 ApplicationContext,又拿 BeanFactory,又拿 ClassLoader。它们这么写不是因为作者喜欢接口,而是因为这是容器早期阶段可用、可靠、低开销的传递方式。业务代码可以少用 Aware,但理解这套机制,能帮你读懂 Spring 内部真正在发生什么。
7.3 把Aware和生命周期串成一条时间线
最后把整篇文章浓缩成一条值得背下来的时间线:实例化(构造器)→ 属性填充(@Autowired、@Value 等注入)→ BeanNameAware / BeanClassLoaderAware / BeanFactoryAware 回调 → BeanPostProcessor 前置处理(ApplicationContextAware、EnvironmentAware 等 Aware 回调,以及 @PostConstruct)→ 初始化(InitializingBean.afterPropertiesSet、init-method)→ BeanPostProcessor 后置处理 → 使用 → 销毁。
我在实际带人的时候发现,能讲清这条时间线的同学,不管在排查循环依赖、自定义注解处理器还是定位初始化顺序问题上,都明显比背结论的人快。Aware 就是理解这条时间线最舒服的一把钥匙,因为它本身就是"容器主动、Bean 被动"这个事实最直接的体现。
以上就是我在项目里接触 Aware 的完整记录。如果你最近在梳理 Spring 生命周期,建议找一段休息时间自己写一遍文中那个打印顺序的 Demo,亲眼看到控制台输出,比看我写十遍都管用。等你自己也实现了两三个自定义 Aware,再回头看 Spring 源码里的那些内部处理器,会觉得亲切很多。