Spring静态工具类中注入 Bean 的原理与实现

一、起因

最近在对一个老旧工具类进行改造。

工具类中原本写死了服务的 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; // ❌ 不行
}

这有两个致命问题:

  1. ApplicationContext 是 Spring 容器本身,它并不是容器中的一个 Bean,因此无法通过 @Autowired@Resource 注入。

  2. 静态字段无法通过实例化的方式注入,@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?

其实原理并不复杂:

  1. Bean 实例存储在 ApplicationContext 的 Map 中,这是一个全局唯一的数据结构。

  2. SpringContextHolder 将 ApplicationContext 的引用保存到了静态变量中

  3. 静态方法访问静态变量,拿到 ApplicationContext 引用,再从它的 Map 里取出 Bean 实例。

本质上,SpringContextHolder 就是一个桥梁,把 Spring 容器暴露给了静态上下文。

为什么需要 @Component 而不是普通类?

有人可能会想:直接写一个普通类,通过某种方式拿到 ApplicationContext 不就行了?

问题在于,如果不加 @Component,Spring 根本不知道这个类的存在,更不会去检查它是否实现了 ApplicationContextAware。只有成为 Spring 容器管理的 Bean,才能享受到生命周期的回调。

关于 @Override 注解

@Override 注解本身不是必须的,去掉也能正常运行。但强烈建议保留,原因有二:

  1. 编译期校验 :如果方法签名写错了(比如方法名拼错),加上 @Override 编译器会直接报错;不加则能编译通过,运行时回调不触发,调试起来很麻烦。

  2. 代码可读性:明确告诉读代码的人,这个方法是重写自接口的,而不是普通方法。

调用频繁需要缓存吗?

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                  // 容器销毁时清理引用

其中 BeanFactoryPostProcessorApplicationContextAware 的区别:

接口 回调时机 用途
BeanFactoryPostProcessor 所有 Bean 定义加载完成后、实例化之前 修改 Bean 定义(如属性占位符替换)
ApplicationContextAware 当前 Bean 初始化时 获取 ApplicationContext 引用

Hutool 实现多个接口,是为了在不同时机都能拿到 ApplicationContext,做到多重保障。日常开发中,自己实现只用 ApplicationContextAware 就足够了。

六、总结

解决"静态方法中获取 Spring Bean"这个问题的核心在于理解 Spring 的生命周期回调机制:

  1. ApplicationContext 不能通过 @Autowired 注入,因为它本身不是容器中的 Bean。

  2. ApplicationContextAware 是 Spring 专门提供的回调接口,用于让 Bean 获取容器引用。

  3. @Component + implements ApplicationContextAware 是最小可行方案。

  4. 静态变量作为桥梁,将 ApplicationContext 暴露给静态上下文,从而在静态方法中获取任意 Bean。

  5. Hutool 的 SpringUtil 已经封装好了这个功能,可以直接使用。

看似简单的几行代码,背后蕴含的是 Spring 的 BeanPostProcessor 机制、Aware 回调体系和对控制反转的深入理解。搞清楚了这些,对 Spring 容器的运作原理也就有了更深刻的认识。


知其然,更知其所以然。