一、起因
最近在对一个老旧工具类进行改造。
工具类中原本写死了服务的 IP 和端口,大概是这样的:
java
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?
误区:不能通过 @Autowired 注入 ApplicationContext
很多人第一反应是直接注入:
java
@Component
public class SpringContextHolder {
@Autowired
private static ApplicationContext applicationContext; // ❌ 不行
}
这有两个致命问题:
-
ApplicationContext是 Spring 容器本身,它并不是容器中的一个 Bean,因此无法通过@Autowired或@Resource注入。 -
静态字段无法通过实例化的方式注入,
@Autowired作用于实例字段,Spring 无法对静态字段赋值。
所以必须另寻他路。
三、SpringContextHolder 的实现与原理
Spring 提供了一系列 Aware 回调接口,允许 Bean 在初始化时感知到容器的某些组件。其中 ApplicationContextAware 就是专门用于获取 ApplicationContext 的接口。
完整代码实现
java
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(Class<T> clazz) {
if (applicationContext == null) {
throw new IllegalStateException("ApplicationContext 未初始化");
}
return applicationContext.getBean(clazz);
}
public static <T> T getBean(String name, Class<T> 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,它内部会做这样的判断:
java
// Spring 源码简化逻辑
private void invokeAwareInterfaces(Object bean) {
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
}
关键点就在于 instanceof 判断:
-
如果你的 Bean 实现了
ApplicationContextAware接口,Spring 就会调用它的setApplicationContext方法,并把容器自身(this.applicationContext)作为参数传入。 -
如果没有实现这个接口,Spring 直接跳过,你的 Bean 永远也拿不到
ApplicationContext。
这就解释了为什么 implements ApplicationContextAware 是必须的------它不是魔法,只是 Java 多态机制下的一个简单回调。
第四步:保存到静态变量
java
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
}
当 Spring 调用 setApplicationContext 时,我们把传入的 ApplicationContext 赋值给一个静态变量。由于:
-
SpringContextHolder是单例的(Spring 默认 scope),只会初始化一次。 -
静态变量属于类,全局唯一。
因此此后任何地方都可以通过 SpringContextHolder.applicationContext 访问到这个容器。
第五步:对外提供静态获取方法
java
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
有了 ApplicationContext,获取 Bean 就简单了。getBean() 方法底层就是从容器内部的一个 ConcurrentHashMap 中根据类型或名称查找对应的 Bean 实例,时间复杂度 O(1),非常快。
完整调用链路
text
Spring 启动
└─ 扫描 @Component,创建 SpringContextHolder 实例
└─ 初始化 Bean
└─ ApplicationContextAwareProcessor 检查
└─ instanceof ApplicationContextAware?→ true
└─ 调用 setApplicationContext(applicationContext)
└─ 保存到静态变量 applicationContext
业务代码调用:
ServiceUrlUtil.getServiceUrl()
└─ SpringContextHolder.getBean(NacosConfigService.class)
└─ applicationContext.getBean(NacosConfigService.class)
└─ 从单例池 Map 中返回 NacosConfigService 实例
└─ 调用 getIp() / getPort() 获取配置
在工具类中的使用
改造后的工具类:
java
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);
}
}
所有业务调用方完全不需要改动,依然是静态方式调用:
java
String url = ServiceUrlUtil.getServiceUrl("order-service");
四、补充说明
为什么静态方法能拿到 Bean?
很多人会有疑问:静态方法属于类,不依赖于实例,怎么可能拿到 Spring 管理的 Bean?
其实原理并不复杂:
-
Bean 实例存储在 ApplicationContext 的 Map 中,这是一个全局唯一的数据结构。
-
SpringContextHolder 将 ApplicationContext 的引用保存到了静态变量中。
-
静态方法访问静态变量,拿到 ApplicationContext 引用,再从它的 Map 里取出 Bean 实例。
本质上,SpringContextHolder 就是一个桥梁,把 Spring 容器暴露给了静态上下文。
为什么需要 @Component 而不是普通类?
有人可能会想:直接写一个普通类,通过某种方式拿到 ApplicationContext 不就行了?
问题在于,如果不加 @Component,Spring 根本不知道这个类的存在,更不会去检查它是否实现了 ApplicationContextAware。只有成为 Spring 容器管理的 Bean,才能享受到生命周期的回调。
关于 @Override 注解
@Override 注解本身不是必须的,去掉也能正常运行。但强烈建议保留,原因有二:
-
编译期校验 :如果方法签名写错了(比如方法名拼错),加上
@Override编译器会直接报错;不加则能编译通过,运行时回调不触发,调试起来很麻烦。 -
代码可读性:明确告诉读代码的人,这个方法是重写自接口的,而不是普通方法。
调用频繁需要缓存吗?
applicationContext.getBean() 本身是从内存中的 Map 取出对象,速度很快,不做缓存也完全没有问题。
但如果在多个静态方法中频繁书写 SpringContextHolder.getBean(Xxx.class),代码会显得冗余。可以这样优化:
java
public class ServiceUrlUtil {
private static volatile NacosConfigService nacosService;
private static NacosConfigService getNacosService() {
if (nacosService == null) {
synchronized (ServiceUrlUtil.class) {
if (nacosService == null) {
nacosService = SpringContextHolder.getBean(NacosConfigService.class);
}
}
}
return nacosService;
}
public static String getServiceUrl(String serviceName) {
// 直接使用 getNacosService(),代码更简洁
return String.format("http://%s:%d",
getNacosService().getIp(serviceName),
getNacosService().getPort(serviceName));
}
}
这只是代码组织上的优化,与性能无关。
五、Hutool 已经帮你实现好了
其实 SpringContextHolder 这种需求非常普遍,大名鼎鼎的 Hutool 工具包已经封装好了,即 SpringUtil 类。
如果项目中已经引入了 Hutool,直接使用即可:
java
import 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 的实现更为健壮,它同时实现了四个接口:
java
public class SpringUtil implements
ApplicationContextAware, // 主要方式,拿到 ApplicationContext
BeanFactoryPostProcessor, // 备用方式,在 Bean 实例化前执行
ApplicationListener<ContextRefreshedEvent>, // 容器刷新完成后的兜底
DisposableBean // 容器销毁时清理引用
其中 BeanFactoryPostProcessor 和 ApplicationContextAware 的区别:
| 接口 | 回调时机 | 用途 |
|---|---|---|
BeanFactoryPostProcessor |
所有 Bean 定义加载完成后、实例化之前 | 修改 Bean 定义(如属性占位符替换) |
ApplicationContextAware |
当前 Bean 初始化时 | 获取 ApplicationContext 引用 |
Hutool 实现多个接口,是为了在不同时机都能拿到 ApplicationContext,做到多重保障。日常开发中,自己实现只用 ApplicationContextAware 就足够了。
六、总结
解决"静态方法中获取 Spring Bean"这个问题的核心在于理解 Spring 的生命周期回调机制:
-
ApplicationContext 不能通过 @Autowired 注入,因为它本身不是容器中的 Bean。
-
ApplicationContextAware 是 Spring 专门提供的回调接口,用于让 Bean 获取容器引用。
-
@Component + implements ApplicationContextAware 是最小可行方案。
-
静态变量作为桥梁,将 ApplicationContext 暴露给静态上下文,从而在静态方法中获取任意 Bean。
-
Hutool 的 SpringUtil 已经封装好了这个功能,可以直接使用。
看似简单的几行代码,背后蕴含的是 Spring 的 BeanPostProcessor 机制、Aware 回调体系和对控制反转的深入理解。搞清楚了这些,对 Spring 容器的运作原理也就有了更深刻的认识。
知其然,更知其所以然。