Long time no see。我印象最深的一次翻车,不是复杂的并发问题,反而是“想在一个工具类的静态方法里调用 Service”这种最基本的场景。同事图省事直接 new 了一个 Service,结果调接口时 Mapper 全是 null 报空指针。原因很简单:Spring Boot 里的 Bean 是容器创建、管理、注入的,你手动 new 出来的对象,Spring 根本不知道它的存在,依赖注入自然不会生效。也就是从那天起,我把“SpringBoot 获取 Bean 的几种方式”彻彻底底整理了一遍,今天把完整思路、代码和踩过的坑都写出来。
这个内容适合谁?刚接触 Spring Boot 的初学者,写框架代码、工具类时需要拿 Bean 的人,以及准备面试、想搞懂 IoC 和 Bean 实例化原理的开发者。我会从最基础的“为什么要取 Bean”开始,讲到 6 种常见获取方式、对比选型、常见坑,最后给你一份可以直接抄进项目的工具类。
1. 先从“为什么要取 Bean”说起:IoC 容器下的高频需求
1.1 不 new 对象,那 Bean 到底归谁管
很多刚接触 Spring Boot 的人会有一个疑问:以前写 Java 都是new一个对象直接用,为什么到了 Spring Boot 里反而不会写了?原因在于 Spring Boot 采用了 IoC(Inversion of Control,控制反转)思想。常规的 Java 代码里,对象由使用者自己创建,依赖关系由调用方维护,控制权在“写代码的人”手里;而在 Spring Boot 项目中,对象统一交给容器管理,容器负责创建、初始化、注入、销毁,我们需要某个对象时,直接向容器声明“给我一个 XX 类型的 Bean”即可。
容器里管理的对象,就是所谓的 Bean。它们通常在项目启动时被扫描、注册、实例化,随后被放入 Spring 的上下文(ApplicationContext)中。之后无论哪个组件需要依赖同一个对象,容器都会按照你声明的规则把实例交给你。如果你在普通类里new一个 Bean,这个对象不会经过 Spring 的生命周期管理,因此:
@Autowired、@Value注入的属性全是 null;- AOP 增强失效,事务注解不生效;
- 无法获得代理对象和配置属性绑定;
- 对象生命周期结束后的销毁方法也不会被调用。
所以搞清楚“怎么从容器拿 Bean”,本质上就是搞清楚怎么让 Spring 管理好的对象进入我们自己的代码里。
1.2 哪些场景真正需要主动获取 Bean
依赖注入通常能解决 90% 的业务需求,在 Controller、Service、Mapper 之间,我们只要写好@Autowired或构造器注入就行。但总有几类场景必须“主动获取”:
- 工具类静态方法:像
DateUtils、FileUtils这类静态工具类,本身不是 Spring Bean,但内部可能需要调用 Service 层的业务方法,此时只能通过静态持有 ApplicationContext 的方式获取。 - 定时任务与监听器:某些框架自带的回调接口、第三方库的扩展点,对象被外部创建,不在 Spring 容器扫描范围内,但回调方法里又要用容器里的 Bean。
- 策略模式与运行时动态选择:多个实现类通过一个工厂或 Map 保存,编写代码时需要按名称或条件动态取对应 Bean。
- 框架封装与自定义 Starter:开发自己的通用组件时,无法预知调用方要注入什么类型的 Bean,只能在运行时通过参数动态获取。
理解这些场景后,再看后面几种获取方式,就会明白每一种方式存在的理由了。
2. 从容器取 Bean 的六种常见方式:代码、原理与取舍
2.1 注入式获取:@Autowired 与 @Resource
最常用、最直观的方式,就是依赖注入。@Autowired是 Spring 提供的注解,默认按类型匹配;@Resource是 JSR-250 标准注解,默认按名称匹配,名称找不到了再按类型匹配。
@Service public class OrderService { public void createOrder() { System.out.println("create order"); } } @Component public class OrderController { @Autowired private OrderService orderService; // 按类型注入 @Resource(name = "orderService") private OrderService orderService2; // 按名称注入 }这里我要说一个很多人忽略的本质:注入也好,主动getBean也好,最终都是从同一个容器里取对象,区别只是入口不同。正因为 Spring 容器在启动时把单例对象统一放在一个存储结构里,@Autowired才能如此轻巧地完成“按需下发”。
用@Autowired时有几个注意点:
- 如果同类型有多个 Bean,默认会报
NoUniqueBeanDefinitionException。解决办法是配合@Qualifier("具体名称"),或是在某个实现类上加@Primary。 - 字段注入写起来方便,但容易导致类难以进行单元测试,也无法实现不可变对象。后面我会专门说构造器注入为什么更推荐。
@Resource是 JDK 扩展包里的注解,Spring 支持但并非 Spring 原生注解,如果你重度依赖 Spring 特性,建议优先用@Autowired。
2.2 ApplicationContext.getBean():最直接的手动获取
ApplicationContext是 Spring 容器的核心接口。在 Spring Boot 的启动入口,SpringApplication.run(...)返回的就是一个ConfigurableApplicationContext。
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(DemoApplication.class, args); // 按类型获取 OrderService orderService = context.getBean(OrderService.class); // 按名称获取 OrderService orderService2 = context.getBean("orderService", OrderService.class); // 获取多个同类型 Bean Map<String, OrderService> map = context.getBeansOfType(OrderService.class); } }getBean方法的几个重载形式值得记牢:
getBean(Class<T> requiredType):按类型获取,类型不唯一时会报错。getBean(String name):按容器内 Bean 的名称获取,返回Object,通常需要强转。getBean(String name, Class<T> requiredType):按名称 + 类型获取,既避免强转,又能让容器做一次类型校验。getBean(Class<T> requiredType, Object... args):如果目标 Bean 是原型作用域,可以通过 args 传构造参数。
这个方式最直接,它的前提是你能拿到一个ApplicationContext对象。如果你在某个类里拿不到容器,就得借助第 2.3 节的ApplicationContextAware。
2.3 ApplicationContextAware:让普通类也能感知容器
当你需要在一个非 Spring 管理的类里获取 Bean 时,就必须让某个 Spring 管理的对象先拿到ApplicationContext,再通过静态变量暴露出来。Spring 提供了ApplicationContextAware接口,会被容器回调setApplicationContext方法。
@Component public class SpringContextUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtils.applicationContext = applicationContext; } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } public static Object getBean(String name) { return applicationContext.getBean(name); } }这套方案的原理并不复杂:容器在初始化这个工具类时,会把自身作为参数传进来,类内部用一个静态字段保存引用。之后不管在哪个普通类、静态方法、过滤器中,只要调用SpringContextUtils.getBean(...),都能拿到容器里的 Bean。
注意几个细节:
- 这个工具类本身必须是 Spring 管理的组件,否则
setApplicationContext不会被调用。 - 静态字段的初始化发生在启动阶段,如果过早调用(比如某些第三方框架在容器刷新前就触发),可能拿到 null。
- 因为持有的是静态引用,要注意多环境下的上下文切换问题。常规单体应用中影响不大,但如果你写的是通用 SDK,建议增加一次判空校验。
2.4 ObjectProvider:优雅处理“可能没有、可能有多个”的情况
从 Spring 4.3 开始,ObjectProvider被引入,它就是为“延迟获取”和“可选依赖”设计的。字段注入会在容器启动时就强行解析依赖,而ObjectProvider是惰性的:真正调用方法时才去容器取。
@Service public class MessageService { @Autowired private ObjectProvider<SmsService> smsServiceProvider; public void sendSms(String mobile, String content) { // 如果没有 SmsService 的 Bean,返回 null SmsService smsService = smsServiceProvider.getIfAvailable(); if (smsService != null) { smsService.send(mobile, content); } // 如果存在多个实现,逐个处理 smsServiceProvider.stream().forEach(s -> s.send(mobile, content)); // 针对原型 Bean,每次获取都能拿到新实例 SmsService newInstance = smsServiceProvider.getObject(); } }ObjectProvider的价值在于把“取不到”“取多个”这些边界情况从我们手里接管了。常规@Autowired在容器里找不到依赖时,启动直接失败;用ObjectProvider,可以做到“有就用,没有就算了”。这在开发通用组件、插件式扩展点时特别好用。
如果你用@Autowired注入一个List<SmsService>,Spring 也会把所有实现都注入进来,但顺序是按名称排序,不能随心所欲控制。而ObjectProvider.stream()可以配合@Order实现更明确的优先级。
2.5 构造器注入:最值得养成的习惯
严格来说,构造器注入不属于“主动获取 bean”,但它比字段注入更像“获取依赖”,而且从 Spring 4.3 起,单构造器类甚至可以省略@Autowired。这一点现代 Spring 官方也是推荐的。
@Service public class OrderServiceImpl implements OrderService { private final UserService userService; private final ProductService productService; public OrderServiceImpl(UserService userService, ProductService productService) { this.userService = userService; this.productService = productService; } }构造器注入的好处:
final关键字保证依赖不可变,对象一旦创建成功,依赖关系就不会被篡改。- 更容易做单元测试:直接 new 对象时传入 mock,不依赖 Spring 容器。
- 能明显暴露依赖过多的问题,逼着开发者拆分职责。
Spring 容器在创建这个 Bean 时,会自动识别构造函数并逐一解析参数。如果构造函数有多个,或参数上使用了@Qualifier,容器仍然能精确匹配。实际项目里,我建议所有业务 Bean 全部采用构造器注入,除非是工具类这种特殊情况。
2.6 静态工具类封装:项目里最常看到的“取 Bean 入口”
实际开发中,大家更常用的是对 2.3 封装一层,做成一个像SpringUtils的静态工具类。这样在任意位置都能写:
OrderService orderService = SpringUtils.getBean(OrderService.class);这个封装本质就是ApplicationContextAware + 静态方法。我通常在项目里起名SpringContextHolder,放一个getBean、getBeansOfType、getProperty等静态方法。在第 6 部分,我会给一个相对完善的版本。这里先说明一点:静态工具类适合“不得不在静态上下文里使用 Spring 对象”的场景,不要把它当成日常业务代码的首选,否则会在项目里到处出现SpringContextHolder.getBean(xxx),代码可读性和可测试性都会下降。
3. 选型对比:直接注入、容器获取与上下文感知到底用哪个
3.1 一张表看完六种方式
| 获取方式 | 核心实现 | 推荐场景 | 注意事项 |
|---|---|---|---|
| @Autowired / @Resource | 声明字段、方法、构造器参数 | 常规业务依赖注入 | 类型冲突时需 @Qualifier;字段注入不利于测试 |
| ApplicationContext.getBean | 直接调用上下文方法 | 启动入口、框架代码、运行时参数传入 | 必须先拿到容器对象;类型不唯一会异常 |
| ApplicationContextAware | 实现接口,静态保存上下文 | 工具类、非 Spring 管理的类 | 必须在 Spring 容器初始化后使用 |
| ObjectProvider | 注入 ObjectProvider,延迟获取 | 可选依赖、多实现、原型 Bean | 适合边界场景,不等于普通注入的替代品 |
| 构造器注入 | 构造函数参数自动装配 | 日常业务组件 | 推荐为主流方式 |
| 静态工具类 | ApplicationContextAware + 静态方法 | 全局工具类、框架封装 | 注意上下文持有与生命周期管理 |
3.2 我的选型顺序和建议
在真实项目中,我会遵循这样的选型逻辑:
第一优先级是构造器注入。所有能静态写死的依赖关系,都用final+ 构造器。这样的代码最容易测试,也最能体现依赖关系。
第二优先级是@Autowired字段注入。通常在 Controller 层看代码简洁度,或者快速原型开发时才用。字段注入最大的问题是隐藏依赖,IDE 上看一个类有哪些依赖,必须把字段都看一遍。新项目不建议大量使用。
第三优先级是ObjectProvider。当依赖可能不存在,或者运行时需要获取新的原型实例时使用。比如网关里判断“有没有某个过滤器 Bean”,有就执行,没有就跳过,这种场景用ObjectProvider比@Autowired靠谱得多。
第四优先级是ApplicationContextAware或静态工具类。普通业务代码不优先推荐,但在工具类、监听器、自定义 Starter 里绕不开。这一层是整个 Spring 容器的“兜底入口”,用得好确实能解决很多问题,但用多了也容易让代码味道变差。
如果是在面试中被问到,我建议给出这样的思考过程:优先依赖注入,让容器管理依赖关系;确实需要在容器外拿 Bean,再考虑 ApplicationContextAware 封装工具类;当依赖不确定时,用 ObjectProvider 做延迟和容错处理。
4. 取 Bean 路上常见的五个坑:排查思路与规避办法
4.1 容器还没初始化就去取 Bean
很多同学把ApplicationContextAware工具类写好,结果调用时静态字段一直是 null。问题往往不是代码逻辑问题,而是执行时机太早。
举个例子:某个第三方框架在SpringApplication.run()之前的某个阶段就回调了我们写的扩展方法,而那个工具类 Bean 还没被容器实例化,setApplicationContext自然没有被调用。
排查思路:
- 在
setApplicationContext方法里加日志,确认是否被调用。 - 确认调用取 Bean 方法的位置是否在容器刷新完成之后。
- 如果需要在启动阶段执行逻辑,优先使用
ApplicationRunner或CommandLineRunner,不要依赖工具类的静态字段。
@Component public class StartupRunner implements ApplicationRunner { private final ApplicationContext context; public StartupRunner(ApplicationContext context) { this.context = context; } @Override public void run(ApplicationArguments args) { // 此时容器已就绪,可以安全取 Bean OrderService orderService = context.getBean(OrderService.class); } }这类坑有个通用特征:报错不是 ClassNotFoundException,而是空指针或者BeanNotYetReadyException。
4.2 同类型多个实现,getBean(Class) 必然报错
如果一个接口有多个实现类,比如SmsService下有两个实现AliyunSmsService和TencentSmsService,直接用context.getBean(SmsService.class)会抛出NoUniqueBeanDefinitionException。
如果你真的需要“按全部实现取一遍”,改用getBeansOfType:
Map<String, SmsService> serviceMap = context.getBeansOfType(SmsService.class); serviceMap.forEach((name, service) -> service.send("13900000000", "hello"));如果你只需要其中一个,并且通过@Service("aliyunSmsService")明确了名称,就用名称 + 类型:
SmsService service = context.getBean("aliyunSmsService", SmsService.class);还有一种思路是给某个实现加@Primary,这样容器会优先返回那个 Bean,适合“找不到默认实现”的产品场景。
4.3 代理对象导致类型转换异常
Spring Boot 中 AOP、事务注解、@Configuration默认都会生成 CGLIB 代理对象。代理并不是原来的目标类,而是目标类的子类。所以一个 UserService 的代理对象可以强转成UserService,但不能强转成最终实现类UserServiceImpl以外的类型。
我遇到过类似代码:
UserServiceImpl impl = (UserServiceImpl) applicationContext.getBean(UserService.class);启动时报ClassCastException,就是因为getBean返回的实际是 CGLIB 生成的代理对象。很多人会觉得“代码没问题啊”,实际上问题是类型转换期望过于具体。容器里注册的 Bean 可能是接口类型,返回的对象是代理增强后的实现子类,但直接转成某个实现类会失败。
解决办法是面向接口编程,用接口类型接收:
UserService userService = applicationContext.getBean(UserService.class);如果一定要拿实现类的特性,就在实现类内部通过@Autowired注入自身,或者把需要的方法定义在接口里。
4.4 循环依赖:Autowired 能过,手动 getBean 却可能失败
Spring Boot 对循环依赖的控制比较严格。两个 Bean 互相依赖时,容器无法确定先创建谁。经典解法是在其中一方加@Lazy,生成一个延迟代理,等真正调用时再去解析目标 Bean。
@Component public class AService { private final BService bService; @Autowired public AService(@Lazy BService bService) { this.bService = bService; } }手动getBean时也会遇到类似问题:你从容器里拿到 A,A 的内部依赖还没有完全初始化,这时调用 A 的方法可能触发循环依赖导致启动失败。所以在写框架初始化代码时,尽量只取当前真正需要的 Bean,不要一口气把所有 Bean 都取出来然后搞连带关系。
4.5 缓存了 Bean 引用,导致单例变“多例”或状态不一致
默认情况下 Spring 的 Bean 是单例的,每次getBean返回同一实例。但如果某个 Bean 是 prototype 作用域,每getBean一次都会新创建一个实例。
常见错误是:写了一个静态 Map 缓存 Bean 引用,第一次取到 prototype Bean 就放进 Map,后面每次都用缓存里的旧对象,打印出来全是同一个引用,完全违背了“原型作用域每次新建”的初衷。
@Component public class PrototypeService { private int count = 0; public void incr() { count++; } } @Configuration public class BeanConfig { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public PrototypeService prototypeService() { return new PrototypeService(); } }如果你的业务确实需要每次取新实例,就老老实实用ObjectProvider.getObject(),或者直接context.getBean(PrototypeService.class),不要做自定义缓存。
5. 自动装配、CGLIB 代理与 Bean 实例化:几个被忽略的底层细节
5.1 @SpringBootApplication 与主动装配的关系
Spring Boot 之所以能“自动配置”,核心在@SpringBootApplication里组合了@EnableAutoConfiguration。这个注解会去加载META-INF/spring.factories或AutoConfiguration.imports里声明的配置类,从而把数据源、Redis、消息队列等常见组件自动注册成 Bean。也就是说,你取到的很多 Bean,根本不是业务代码里写的@Service,而是自动配置类通过@Bean方法注册进来的。
这给我们一个启发:当getBean取不到某个组件时,不要只怀疑自己的类没扫描到,还要检查自动配置是否生效。比如你只引入了数据源依赖,却取不到DataSource,很可能是因为某个自动配置条件不满足。
5.2 CGLIB 代理和 getBean 返回对象的关系
Spring Boot 默认使用 CGLIB 作为 AOP 的代理方式。@Configuration加载的配置类本身也是 CGLIB 代理的对象,这保证了同一个@Bean方法在容器内只会被调用一次。当你在配置类里同时调用多个相同类型的@Bean方法时,得到的是同一个单例对象。
这直接影响了“取 Bean 的类型认知”:
- 你
getBean(UserService.class)拿到的未必是原始 new 出来的对象,而是代理增强后的子类。 - 代理对象的方法调用会额外进入 AOP 拦截链,事务、权限等逻辑才会生效。
- 如果只是单纯想拿“原始对象”做原型数据初始化,取到的代理对象反而可能让人困惑,但实际上代理才是容器管理的真正成品。
所以面对这类对象时,不要用getClass()判断期望类,也不要在代理对象上做基于字段的反射操作。反射时拿到的是 CGLIB 的TargetObject等额外字段,容易出幺蛾子。
5.3 简化理解 Bean 的实例化过程
从底层看,一次完整的 Bean 获取链路大致是:
- 扫描候选类或读取配置类,注册
BeanDefinition。 - 根据定义,由
BeanFactory创建实例。 - 填充属性,执行
@Autowired字段/方法/构造器的依赖注入。 - 执行 BeanPostProcessor 回调,创建 AOP 代理。
- 执行初始化方法。
- 放入单例池,等待对外提供。
所以当你写getBean时,得到的“完成品”可能已经经历了代理、增强、属性填充等一整套流程。这也是为什么容器创建的 Bean 和手动 new 出来的差异巨大——容器里的一切都有生命周期管理,手动 new 的对象就只是一块普通内存。
6. 我常用的 SpringContextHolder 工具类与进阶扩展
6.1 一个可以直接复制到项目的工具类
这里给出我项目里一直在用的简化版。它基于ApplicationContextAware,代码量不大,但覆盖了常见需求:
@Component public class SpringContextHolder implements ApplicationContextAware, DisposableBean { private static ApplicationContext applicationContext = null; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextHolder.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { assertContextInjected(); return applicationContext; } public static <T> T getBean(Class<T> requiredType) { assertContextInjected(); return getApplicationContext().getBean(requiredType); } public static <T> T getBean(String name, Class<T> requiredType) { assertContextInjected(); return getApplicationContext().getBean(name, requiredType); } public static <T> Map<String, T> getBeansOfType(Class<T> type) { assertContextInjected(); return getApplicationContext().getBeansOfType(type); } public static Object getBean(String name) { assertContextInjected(); return getApplicationContext().getBean(name); } private static void assertContextInjected() { if (applicationContext == null) { throw new IllegalStateException("applicationContext 未注入,请确认 SpringContextHolder 已被 Spring 扫描"); } } @Override public void destroy() { applicationContext = null; } }几点设计说明:
- 实现了
DisposableBean,容器关闭时把静态上下文置空,避免内存泄漏和误用。 - 所有获取方法统一先断言上下文存在,保证报错信息清晰。
- 方法都加了泛型,调用方不需要手写强转。
6.2 配合 ApplicationRunner 的进阶用法
有时我们需要在项目启动完成后,对某些 Bean 做一次预热或校验。用ApplicationRunner是在容器完全刷新后执行的,此时 Web 服务还没对外完全开放,但又确实能拿到所有 Bean:
@Component public class CacheWarmerRunner implements ApplicationRunner { private final ApplicationContext applicationContext; public CacheWarmerRunner(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public void run(ApplicationArguments args) { Map<String, CacheLoader> loaders = applicationContext.getBeansOfType(CacheLoader.class); for (CacheLoader loader : loaders.values()) { loader.preload(); } } }这种场景下,为什么不直接用@Autowired?因为实现类数量是动态的,后续新增一个CacheLoader实现,代码不需要改动,容器会自动多注入一个进来。如果你用静态工具类去拿,也需要注意 runner 的执行顺序,别在一个 runner 里取另一个还没初始化的组件。
6.3 使用静态工具类的红线
我见过太多项目把SpringContextHolder.getBean()当作万能药,到处都是静态调用,最终代码里依赖关系完全黑盒化,测试基本没法写。这里给三条红线:
第一,业务依赖能注入绝不静态获取。Controller 里写SpringContextHolder.getBean(UserService.class)不仅多此一举,还破坏了依赖可视化。
第二,静态字段保存的上下文不要被随意覆盖。如果一个类实现了多个ApplicationContextAware子接口,或者上下文被多次 set,要保证最终赋值逻辑是幂等的。
第三,不要在 Bean 初始化阶段用静态工具类去取尚未创建的 Bean。如果确实需要按顺序初始化,就用@DependsOn显式声明依赖顺序,而不是靠“运气”去取。
最后再分享一个小技巧:如果你在写自定义注解或框架代码,不确定目标 Bean 是否存在,优先用ObjectProvider,它天生就是为你这种“边界情况”设计的;如果你只是想在普通工具类里快速拿对象,SpringContextHolder配合DisposableBean清理静态引用,就是很稳妥的兜底方案。这套组合我用了几年,没怎么出过幺蛾子。实际上,弄懂 SpringBoot 获取 Bean 这几种方式,核心不是背代码,而是真正理解容器什么时候创建对象、代理对象是什么形态、调用时机的边界在哪里。把这些关键点记住了,无论项目怎么换,你都不会再因为“手动 new 一个 Service”这种低级问题熬夜加班了。