
一、起因最近在对一个老旧工具类进行改造。工具类中原本写死了服务的 IP 和端口大概是这样的public class ServiceUrlUtil { private static final String IP 192.168.1.100; private static final int PORT 8080; public static String getServiceUrl() { return String.format(http://%s:%d, IP, PORT); } }后来 IP 和端口被迁移到了 Nacos 配置中心部门内也有人封装好了一个NacosConfigService类来统一读取 Nacos 配置。问题来了NacosConfigService是一个 Spring Bean通过Component管理。我的ServiceUrlUtil是一个纯静态工具类不在 Spring 容器中无法使用Resource或Autowired注入。业务代码中已经有多处使用了ServiceUrlUtil.getServiceUrl()这种静态调用方式全部改成注入式调用改动太大。于是就需要解决一个问题在不改变调用方式的前提下如何在静态方法中获取 Spring 容器管理的 Bean二、解决思路核心思路很明确手动拿到 Spring 的ApplicationContext然后通过它获取任何已经托管的 Bean。Spring 启动时会创建一个ApplicationContext所有单例 Bean 都存放在它的一个 Map 中。只要能拿到这个ApplicationContext就能随时从中取出任意 Bean。那么问题就变成了如何拿到ApplicationContext为什么不能直接向 static 字段注入 ApplicationContext很多人第一反应是直接注入Component public class SpringContextHolder { Autowired private static ApplicationContext applicationContext; // ❌ 不行 }这段代码不能工作的真正原因是字段被声明成了staticApplicationContext虽然通常不是通过 BeanDefinition 注册的普通 Bean但 Spring 会把它注册为一种可解析的特殊依赖resolvableDependencies因此可以通过Autowired注入到实例字段、构造器或实例方法中未显式指定名称的Resource通常也可以回退到按类型解析。标准依赖注入针对具体 Bean 实例。AutowiredAnnotationBeanPostProcessor会忽略 static 字段和 static 方法因此不能直接给这个静态字段注入容器。所以这里需要通过一个由 Spring 管理的实例接收容器引用再把它保存到静态字段。三、SpringContextHolder 的实现与原理Spring 提供了一系列Aware回调接口允许 Bean 在初始化时感知到容器的某些组件。其中ApplicationContextAware就是专门用于获取ApplicationContext的接口。完整代码实现import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; Component public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext applicationContext; Override public void setApplicationContext(ApplicationContext context) throws BeansException { applicationContext context; } public static T T getBean(ClassT clazz) { if (applicationContext null) { throw new IllegalStateException(ApplicationContext 未初始化); } return applicationContext.getBean(clazz); } public static T T getBean(String name, ClassT clazz) { if (applicationContext null) { throw new IllegalStateException(ApplicationContext 未初始化); } return applicationContext.getBean(name, clazz); } }原理剖析整个流程分为三步第一步让 SpringContextHolder 成为一个 Bean通过Component注解SpringContextHolder被 Spring 扫描并注册为容器中的一个 Bean。只有成为 Bean才能参与 Spring 的生命周期触发后续的回调。第二步实现 ApplicationContextAware 接口public class SpringContextHolder implements ApplicationContextAware { // ... }这个接口只有一个方法void setApplicationContext(ApplicationContext applicationContext) throws BeansException;它的作用就像是一份契约告诉 Spring我需要 ApplicationContext请你在初始化我的时候把它传给我。第三步Spring 的回调时机Spring 在初始化每个 Bean 时会遍历所有已注册的BeanPostProcessor。其中有一个关键的处理器叫做ApplicationContextAwareProcessor它内部会做这样的判断// Spring 源码简化逻辑 private void invokeAwareInterfaces(Object bean) { if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); } }关键点就在于instanceof判断如果你的 Bean 实现了ApplicationContextAware接口Spring 就会调用它的setApplicationContext方法并把容器自身this.applicationContext作为参数传入。如果没有实现这个接口Spring 只会跳过这一套 Aware 回调Bean 仍然可以通过构造器、实例字段或实例 Setter 注入ApplicationContext。因此在本文采用的 Aware 回调方案中implements ApplicationContextAware是触发该回调的必要条件但它不是获取ApplicationContext的唯一方式。第四步保存到静态变量private static ApplicationContext applicationContext; Override public void setApplicationContext(ApplicationContext context) throws BeansException { applicationContext context; }当 Spring 调用setApplicationContext时我们把传入的ApplicationContext赋值给一个静态变量。由于SpringContextHolder是单例的Spring 默认 scope只会初始化一次。静态变量属于类全局唯一。因此此后任何地方都可以通过SpringContextHolder.applicationContext访问到这个容器。第五步对外提供静态获取方法public static T T getBean(ClassT clazz) { return applicationContext.getBean(clazz); }有了ApplicationContext获取 Bean 就简单了。对于已经初始化的单例 Bean按名称获取通常最终会命中 BeanFactory 的单例缓存开销很小但getBean(Class)还涉及按类型查找和唯一性判断不能简单描述为一次 O(1) 的 Map 查询。完整调用链路textSpring 启动 └─ 扫描 Component创建 SpringContextHolder 实例 └─ 初始化 Bean └─ ApplicationContextAwareProcessor 检查 └─ instanceof ApplicationContextAware→ true └─ 调用 setApplicationContext(applicationContext) └─ 保存到静态变量 applicationContext 业务代码调用 ServiceUrlUtil.getServiceUrl() └─ SpringContextHolder.getBean(NacosConfigService.class) └─ applicationContext.getBean(NacosConfigService.class) └─ 从单例池 Map 中返回 NacosConfigService 实例 └─ 调用 getIp() / getPort() 获取配置在工具类中的使用改造后的工具类public class ServiceUrlUtil { public static String getServiceUrl(String serviceName) { NacosConfigService nacosService SpringContextHolder.getBean(NacosConfigService.class); String ip nacosService.getIp(serviceName); int port nacosService.getPort(serviceName); return String.format(http://%s:%d, ip, port); } }所有业务调用方完全不需要改动依然是静态方式调用String url ServiceUrlUtil.getServiceUrl(order-service);四、补充说明为什么静态方法能拿到 Bean很多人会有疑问静态方法属于类不依赖于实例怎么可能拿到 Spring 管理的 Bean其实原理并不复杂Bean 实例存储在 ApplicationContext 的 Map 中这是一个全局唯一的数据结构。SpringContextHolder 将 ApplicationContext 的引用保存到了静态变量中。静态方法访问静态变量拿到 ApplicationContext 引用再从它的 Map 里取出 Bean 实例。本质上SpringContextHolder 就是一个桥梁把 Spring 容器暴露给了静态上下文。为什么需要 Component 而不是普通类有人可能会想直接写一个普通类通过某种方式拿到 ApplicationContext 不就行了问题在于如果不加ComponentSpring 根本不知道这个类的存在更不会去检查它是否实现了ApplicationContextAware。只有成为 Spring 容器管理的 Bean才能享受到生命周期的回调。关于 Override 注解Override注解本身不是必须的去掉也能正常运行。但强烈建议保留原因有二编译期校验如果方法签名写错了比如方法名拼错加上Override编译器会直接报错不加则能编译通过运行时回调不触发调试起来很麻烦。代码可读性明确告诉读代码的人这个方法是重写自接口的而不是普通方法。调用频繁需要缓存吗通常不需要。如果目标 Bean 是单例Spring 容器本身已经完成了缓存再用静态变量和双重检查缓存一次没有实际收益。额外的静态缓存还可能破坏 prototype、request、session 等作用域的语义并在容器重启或存在多个 ApplicationContext 时继续持有旧 Bean。因此这里直接调用SpringContextHolder.getBean(Xxx.class)即可如果这种调用非常频繁更合适的长期方案是把工具类改造成 Spring Bean并通过构造器直接注入依赖。五、Hutool 已经帮你实现好了其实 SpringContextHolder 这种需求非常普遍大名鼎鼎的 Hutool 工具包已经封装好了即SpringUtil类。如果项目中已经引入了 Hutool直接使用即可javaimport cn.hutool.extra.spring.SpringUtil; public class ServiceUrlUtil { public static String getServiceUrl(String serviceName) { NacosConfigService nacosService SpringUtil.getBean(NacosConfigService.class); return String.format(http://%s:%d, nacosService.getIp(serviceName), nacosService.getPort(serviceName)); } }Hutool SpringUtil 的源码要点Hutool 的实现更为健壮它同时实现了四个接口javapublic class SpringUtil implements ApplicationContextAware, // 主要方式拿到 ApplicationContext BeanFactoryPostProcessor, // 备用方式在 Bean 实例化前执行 ApplicationListenerContextRefreshedEvent, // 容器刷新完成后的兜底 DisposableBean // 容器销毁时清理引用其中BeanFactoryPostProcessor和ApplicationContextAware的区别接口回调时机用途BeanFactoryPostProcessor所有 Bean 定义加载完成后、实例化之前修改 Bean 定义如属性占位符替换ApplicationContextAware当前 Bean初始化时获取 ApplicationContext 引用Hutool 实现多个接口是为了在不同时机都能拿到 ApplicationContext做到多重保障。日常开发中自己实现只用ApplicationContextAware就足够了。六、总结解决静态方法中获取 Spring Bean这个问题的核心在于理解 Spring 的生命周期回调机制ApplicationContext 可以通过 Autowired 注入实例成员因为 Spring 把它注册为了可解析的特殊依赖示例失败的原因是目标字段为 static。ApplicationContextAware 是 Spring 专门提供的回调接口用于让 Bean 获取容器引用。Component implements ApplicationContextAware是本文兼容既有静态调用所采用的一种可行方案并不是获取容器的唯一方式。静态变量作为桥梁将 ApplicationContext 暴露给静态上下文从而在静态方法中获取任意 Bean。Hutool 的 SpringUtil已经封装好了这个功能可以直接使用。看似简单的几行代码背后蕴含的是 Spring 的 BeanPostProcessor 机制、Aware 回调体系和对控制反转的深入理解。搞清楚了这些对 Spring 容器的运作原理也就有了更深刻的认识。知其然更知其所以然。