SpringBoot获取Bean的六种方式:原理、选型与踩坑实战 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 采用了 IoCInversion of Control控制反转思想。常规的 Java 代码里对象由使用者自己创建依赖关系由调用方维护控制权在“写代码的人”手里而在 Spring Boot 项目中对象统一交给容器管理容器负责创建、初始化、注入、销毁我们需要某个对象时直接向容器声明“给我一个 XX 类型的 Bean”即可。容器里管理的对象就是所谓的 Bean。它们通常在项目启动时被扫描、注册、实例化随后被放入 Spring 的上下文ApplicationContext中。之后无论哪个组件需要依赖同一个对象容器都会按照你声明的规则把实例交给你。如果你在普通类里new一个 Bean这个对象不会经过 Spring 的生命周期管理因此Autowired、Value注入的属性全是 nullAOP 增强失效事务注解不生效无法获得代理对象和配置属性绑定对象生命周期结束后的销毁方法也不会被调用。所以搞清楚“怎么从容器拿 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 MapString, OrderService map context.getBeansOfType(OrderService.class); } }getBean方法的几个重载形式值得记牢getBean(ClassT requiredType)按类型获取类型不唯一时会报错。getBean(String name)按容器内 Bean 的名称获取返回Object通常需要强转。getBean(String name, ClassT requiredType)按名称 类型获取既避免强转又能让容器做一次类型校验。getBean(ClassT 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(ClassT 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 ObjectProviderSmsService 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注入一个ListSmsServiceSpring 也会把所有实现都注入进来但顺序是按名称排序不能随心所欲控制。而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。如果你真的需要“按全部实现取一遍”改用getBeansOfTypeMapString, 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时也会遇到类似问题你从容器里拿到 AA 的内部依赖还没有完全初始化这时调用 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(ClassT requiredType) { assertContextInjected(); return getApplicationContext().getBean(requiredType); } public static T T getBean(String name, ClassT requiredType) { assertContextInjected(); return getApplicationContext().getBean(name, requiredType); } public static T MapString, T getBeansOfType(ClassT 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 服务还没对外完全开放但又确实能拿到所有 BeanComponent public class CacheWarmerRunner implements ApplicationRunner { private final ApplicationContext applicationContext; public CacheWarmerRunner(ApplicationContext applicationContext) { this.applicationContext applicationContext; } Override public void run(ApplicationArguments args) { MapString, 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”这种低级问题熬夜加班了。