ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot中获取Bean的几种方式,从依赖注入到ApplicationContext实战

SpringBoot中获取Bean的几种方式,从依赖注入到ApplicationContext实战 很多人在写SpringBoot项目的时候依赖注入用得飞起Autowired往字段上一贴就完事了。但真到了某些场景比如需要在一个工具类的静态方法里拿bean、在过滤器里做鉴权、或者写策略模式做动态分发的时候你会发现Autowired突然就不好使了要么null要么直接启动报错。这个问题的本质是你对容器和bean的获取方式理解得还不够透。这篇文章不打算讲太深奥的IoC源码我就站在一个天天写业务、也偶尔需要搞搞框架封装的老开发者的角度把SpringBoot里获取bean的几种方式给你捋一遍。从最常用的注入方式到通过ApplicationContext主动获取再到一些能让你代码更优雅的进阶写法全部配上代码和场景让你看完能直接抄到项目里用。无论你是刚入门的小白还是准备面试的中级开发或者正在封装通用模块的老手这篇文章都能给你点实在的东西。1. 为什么非要主动获取beanAutowired不香吗先搞清楚一个概念SpringBoot项目启动的时候容器会帮我们创建和管理bean平时我们用Autowired或者构造器注入都是让Spring把bean“喂”到我们手里。这种方式确实省事但有一个前提——你必须也是一个被Spring管理的bean。这就带来一个问题当你写一个普通工具类或者一个通过new关键字创建的对象又或者是在静态方法里Spring根本不知道你的存在自然也没办法给你注入任何东西。这时候你就得主动去容器里“拿”bean。另外一个常见场景是运行时动态决策。比如你有一个接口PayService下面有WechatPayServiceImpl和AlipayPayServiceImpl两个实现你要根据前端传的参数决定调用哪个。虽然你也可以把两个实现都注入进来再用if判断但如果实现有十几个每次新增实现都要改代码那就很蠢了。这时候通过ApplicationContext按类型拿到所有实现再动态筛选就优雅得多。还有像ApplicationListener、Filter、Interceptor这类组件虽然它们本身可以交给Spring管理但在某些自定义的初始化流程里你拿不到注入点只能主动去容器里捞一把。再比如你写单元测试的时候想单独测某个bean直接context.getBean()是最快的。所以结论很简单注入是首选主动获取是必须掌握的备用手段。这两者不是替代关系而是互补关系。理解了这一点我们就能往下看了。2. 日常开发最常用的获取方式把bean直接注入到字段里2.1 属性注入、构造器注入和setter注入的取舍这是一种“让Spring把bean送上门”的方式。根据注入位置的不同分为三种。属性注入字段注入也就是最常见的那种写法Service public class OrderService { Autowired private UserService userService; }优点就是代码量少清爽。但缺点很明显依赖关系藏在字段里不直观而且如果你用new OrderService()去手动创建对象这个字段一定是null很难做单元测试。另外它也会破坏类的封装性依赖关系不透明。构造器注入现在Spring官方推荐的方式Service public class OrderService { private final UserService userService; public OrderService(UserService userService) { this.userService userService; } }用final修饰的字段保证了依赖不可变而且只要构造参数里写了Spring启动时缺了依赖就会直接报错不会拖到运行期才炸。最关键的是单元测试的时候直接new OrderService(mockUserService)就能搞定不需要任何容器。setter注入现在已经很少用了一般只用于可选依赖的场景Service public class OrderService { private UserService userService; Autowired public void setUserService(UserService userService) { this.userService userService; } }三者的优先级我的建议是构造器注入为主属性注入为辅setter注入能不用就不用。但需要说明的是在SpringBoot的日常业务开发里字段注入因为太方便依然是很多团队的主流写法。这个看团队规范没有绝对的对错不过如果你在准备面试建议把构造器注入的优势说清楚。2.2 Autowired和Resource到底有什么区别除了Autowired还有一个高频注解Resource面试也经常被追问。Autowired是Spring提供的默认按类型byType查找bean。如果类型有多个实现会抛异常除非你配合Qualifier指定名称或者用Primary标记主选bean。Resource是JDK标准JSR-250提供的默认按名称byName查找bean。比如字段叫userService它就会先去容器里找名为userService的bean找不到再退化为按类型查找。看个例子就清楚了Autowired private UserService userService; // 按UserService类型去找 Resource private UserService userService; // 先按userService这个名字去找如果UserService接口有两个实现类一个叫UserServiceImpl一个叫VipUserServiceImpl字段名恰好叫userServiceImpl那Resource和Autowired的行为就会不一样。实操中我见过不少同事因为混用这两个注解踩到类型冲突的坑所以记住一句话明确只有一个实现的时候随便用有多个实现时建议AutowiredQualifier显式指定名称别依赖默认行为。3. 最硬核的方式通过ApplicationContext.getBean()直接拿如果说注入方式是让Spring把bean送上门那ApplicationContext.getBean()就是你主动打开容器仓库点名要拿哪个bean。这种方式最直接、最底层几乎能解决所有“拿不到bean”的痛点。但要注意现在的SpringBoot讲究“依赖注入优先”手动getBean属于兜底方案别到处乱用。3.1 方式一注入ApplicationContext对象这是最简单粗暴的做法把自己变成一个Spring管理的bean再把容器对象拿进来Component public class SpringContextHolder { Autowired private ApplicationContext applicationContext; public Object getBean(String beanName) { return applicationContext.getBean(beanName); } }但这样有个问题如果调用方也是一个需要注入的bean那直接注入SpringContextHolder就好了绕了一圈没解决问题。这个方案真正有意义的地方是你可以在任何被Spring管理的类里通过注入ApplicationContext来按需获取其它bean非常灵活。3.2 方式二实现ApplicationContextAware接口把容器存到静态变量这个方案是我在项目里最常用的特别适合做工具类。核心思路是让一个类感知到容器并把容器保存到一个静态变量里这样即使不在Spring管理范围内的类也能通过这个静态变量获取bean。Component public class SpringContextUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; Override public void setApplicationContext(ApplicationContext applicationContext) { SpringContextUtils.applicationContext applicationContext; } public static T T getBean(ClassT clazz) { return applicationContext.getBean(clazz); } public static Object getBean(String beanName) { return applicationContext.getBean(beanName); } public static T T getBean(String beanName, ClassT clazz) { return applicationContext.getBean(beanName, clazz); } }这里有个关键点要注意setApplicationContext方法是容器回调的也就是说这个工具类必须被Spring扫描到比如放在主启动类能扫到的包路径下否则这个静态变量永远是null。我之前帮同事排查过一个线上问题他把这个工具类放在了主启动类扫描路径之外的包里结果所有通过工具类拿bean的接口都报NPE排查了半天才发现是扫描路径问题。3.3 方式三在SpringBoot启动的时候直接把容器存下来这种方式比ApplicationContextAware更直白但是有个管理成本。一般就是新建一个配置类Configuration public class AppContextConfig { Bean public ApplicationContext applicationContext(ApplicationContext applicationContext) { return applicationContext; } }这种写法我实际用得不多因为它本质上还是把容器交给Spring管理绕了一圈还是要注入。除非你在非Spring管理的类里需要这个容器否则不如直接实现ApplicationContextAware来得干净。不过这个思路可以了解一下面试偶尔会问。3.4 getBean的几种重载方法以及底层是如何匹配的ApplicationContext.getBean()有以下几个常用重载// 按bean名称获取返回Object需要强转 Object bean applicationContext.getBean(userService); // 按类型获取类型唯一时直接拿 UserService userService applicationContext.getBean(UserService.class); // 按名称类型获取最推荐避免强转 UserService userService applicationContext.getBean(userServiceImpl, UserService.class); // 带构造参数的获取一般不用SpringBoot下基本废弃 UserService userService applicationContext.getBean(UserService.class, arg1, arg2);底层匹配逻辑其实很简单容器启动时BeanFactory会维护一个beanDefinitionMap里面存着每个bean的名称和定义信息。当你调用getBean()时AbstractBeanFactory会根据你传入的名称或类型去这个Map里做匹配找到对应的BeanDefinition然后决定是新建一个实例还是返回已有的单例对象。要注意的是如果你按类型获取而这个类型在容器中有多个实现比如接口有多个实现类Spring会抛出NoUniqueBeanDefinitionException。解决方式有两种一种是按名称获取另一种是用Primary标记主选bean。后面我会专门讲这个问题。4. 让代码更优雅的进阶技巧前面几种方式算是基础操作下面这几个技巧在实际项目中非常有用能让代码更简洁、更好维护。它们本质上还是依赖注入的思想只不过用了一些Spring的高级特性。4.1 按类型批量获取Map注入真的很好用这是处理策略模式的一把利器。假设你有一个Handler接口有多个实现类每个实现处理一种业务类型public interface Handler { String getType(); void handle(String data); } Component public class OrderHandler implements Handler { Override public String getType() { return ORDER; } Override public void handle(String data) { System.out.println(处理订单 data); } } Component public class UserHandler implements Handler { Override public String getType() { return USER; } Override public void handle(String data) { System.out.println(处理用户 data); } }然后你在一个调度类里用Map把所有Handler实现都注入进来Component public class HandlerDispatcher { // Spring会把所有Handler类型的bean放到这个Map里key是bean名称 private final MapString, Handler handlerMap; public HandlerDispatcher(MapString, Handler handlerMap) { this.handlerMap handlerMap; } public void dispatch(String type, String data) { handlerMap.values().stream() .filter(handler - handler.getType().equals(type)) .findFirst() .orElseThrow(() - new RuntimeException(No handler for type: type)) .handle(data); } }以后新增一个Handler实现只需要加一个Component类调度逻辑一行都不用改。这就是依赖注入的优雅之处。你也可以用ListHandler注入顺序可以用Order注解控制灵活性更高。4.2 ObjectProvider延迟获取bean解决循环依赖和可选依赖ObjectProvider是Spring 4.3引入的很多老开发者不太熟。它的核心作用是让你在需要的时候再去容器里“找”bean而不是在注入的时候就强行绑定。有两个典型场景第一个是循环依赖。假设A依赖BB又依赖A如果用字段注入Spring在某些情况下能自动解决循环依赖但如果用构造器注入就可能直接启动报错。这时候用ObjectProvider可以打破僵局Component public class A { private final ObjectProviderB bProvider; public A(ObjectProviderB bProvider) { this.bProvider bProvider; } public void doSomething() { B b bProvider.getIfAvailable(); if (b ! null) { b.help(); } } }第二个是可选依赖。有些bean存在就用不存在也不能报错Autowired private ObjectProviderCacheService cacheServiceProvider; public void useCacheIfPresent() { CacheService cacheService cacheServiceProvider.getIfAvailable(() - new DefaultCacheService()); cacheService.put(key, value); }这里getIfAvailable还可以传一个Supplier提供默认实现非常灵活。4.3 用Lazy实现懒加载解决启动时的初始化冲突Lazy注解可能大家见得不那么多但它确实能解决一类很头疼的问题有时一个bean的初始化依赖另一个bean而后者又没有被及时初始化导致启动时各种Error creating bean with name xxx报错。用Lazy可以推迟依赖bean的创建Component public class OrderService { private final InventoryService inventoryService; public OrderService(Lazy InventoryService inventoryService) { this.inventoryService inventoryService; } }加了Lazy之后InventoryService只有在第一次被实际用到的时候才会初始化。这在处理一些复杂的启动依赖关系时非常管用。不过我要提醒一句Lazy别滥用。如果你的代码逻辑本身没问题依赖关系清晰就不需要靠这个来规避问题。它本质是一个缓解手段不是治本方案。我见到过有人在排查启动慢的问题时到处加Lazy结果启动速度是快了但第一笔请求因为触发了大量初始化而变得极慢这种移花接木的做法其实很不值得。4.4 从BeanFactory原始容器获取除了ApplicationContextSpring的最底层容器是BeanFactory。ApplicationContext在功能上是BeanFactory的增强版增加了国际化、事件发布、资源加载等能力。你可以通过ApplicationContext获取到底层的BeanFactoryAutowired private ApplicationContext applicationContext; public void useBeanFactory() { ConfigurableListableBeanFactory beanFactory (ConfigurableListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); UserService userService beanFactory.getBean(UserService.class); }实际业务中基本用不到BeanFactory直接操作但在做一些框架级封装的时候可能会碰到。知道有这么个东西就行不用深究。如果你在面试中被问到BeanFactory和ApplicationContext的区别能答出“一个是基础容器一个是增强容器”这个层面的东西基本就能过关。5. 高频踩坑现场获取bean时的报错排查清单不管用哪种方式获取bean报错总是难免的。我把这几年遇到的高频问题整理一下基本上你照着对号入座就能解决大部分难题。5.1 NoSuchBeanDefinitionException找不到bean这是最经典的报错字面意思就是容器里没有你要的这个bean。常见原因有四个第一类没有被Spring扫描到。检查一下主启动类所在的包路径如果类放在扫描路径之外Spring根本不知道它的存在。解法就是调整包结构或者在启动类上用ComponentScan显式指定扫描路径。第二bean名称写错了。用getBean(userServiceImpl)的时候如果实际bean名称是userService那自然会找不到。Spring默认的bean名称是类名首字母小写但也有例外比如某些类名首字母是大写字母连续的情况命名规则会有些特殊。不确定时用applicationContext.getBeanDefinitionNames()打印出所有bean名称看看。第三类型不匹配。你要找UserService.class但容器里只有VipUserService implements UserService这种通过Bean返回的具体类型且类型推断不到接口上。这种情况换按名称获取UserService userService (UserService) applicationContext.getBean(vipUserService);第四作用域导致的问题。如果你拿的是原型作用域Scope(prototype)的bean每次获取都会创建新实例这本身没问题。但如果你把一个原型bean注入到一个单例bean里那注入的实际上只是第一次创建的那个实例后续每次获取的都是同一个对象根本没有原型的效果。这种情况要对注入点也加上Scope处理或者使用ObjectProvider。5.2 NoUniqueBeanDefinitionException类型有多个实现无法决定用哪个这个报错正好和找不到对应是类型太多了。比如用户服务有两个实现你直接getBean(UserService.class)Spring直接懵了。解法有三种。第一种按名称获取最直接UserService userService (UserService) applicationContext.getBean(vipUserServiceImpl);第二种在其中一个实现类上加上Primary注解标记为主选Service Primary public class VipUserServiceImpl implements UserService { }第三种是配合Qualifier注入Autowired Qualifier(vipUserServiceImpl) private UserService userService;这三种各有适用场景。按名称获取最灵活适合在代码中动态指定的场景Primary适合定义了默认实现但不想在调用方写太多代码的场景Qualifier适合一个类需要同时注入同类型多个bean的场景比如业务代码里同时注入了两个数据源。实际操作中我遇到的最常见情况是某个依赖升级后多出一个实现类导致原本正常的代码突然报这个错。排查方式就是用上面的方法先打印出所有实现类的bean名称看一下确认是哪个新增的类闯的祸。5.3 Error creating bean with name xxx初始化阶段就挂了这个报错前缀大家一定不陌生它说明问题不在于你“获取”bean的姿势而在于这个bean在创建阶段就失败了。官网控制台经常会跟着输出一串Caused by那才是真正的原因所在。常见的Caused by有数据库连接失败比如金仓、MySQL等数据库URL配置错误或服务没启动导致DataSource初始化失败。这种情况排查思路是先确认数据库连接串是否可访问再检查连接池配置是否合理最后看驱动的版本和数据库版本是否匹配。依赖的bean不存在。比如A依赖B但B没有被扫描到导致A创建不了。解决方案是让B能被扫描到或者用Lazy延迟加载。对象初始化时执行了外部调用但超时了。比如某个bean的PostConstruct里调用了远程接口而远程服务挂掉了。这种问题线上偶尔会遇到关键是要做超时控制和降级。看报错一定要从下往上看从Caused by开始排查。这一点我强调过很多次但每次帮人看问题都发现还有人在读第一行浪费时间。5.4 getBean拿到的对象不是想要的有时候报错没有但拿到的bean在类型转换的时候报ClassCastException。这通常是因为同一个类型在容器中有多个实例而你的代码按名称拿错了。解决办法也简单用带类型的重载方法PayService payService applicationContext.getBean(wechatPayServiceImpl, PayService.class);这样Spring在返回之前就会做类型检查如果类型不匹配合适的bean会立刻报错而不是等到你强转才炸。另外要留个心眼静态方法里不能用Autowired注入非静态字段。我见过很多新手写工具类字段上加个Autowired然后想在静态方法里直接用结果永远是null。正确做法就是章节3.2里那样用ApplicationContextAware把容器存到静态变量里再在静态方法中调用。5.5 另外一个高频坑工具类没有被Spring扫描到这一点值得单独拎出来说。很多人把SpringContextUtils这个类写好了但在某个普通类里调用它时发现applicationContext是null。检查了好久代码逻辑没问题最后发现是工具类所在的位置不在主启动类的扫描路径下。遇到这种情况处理方式有三在启动类上加上组件扫描SpringBootApplication ComponentScan(basePackages {com.example.common, com.example.business}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }或者用Import手动引入SpringBootApplication Import({SpringContextUtils.class}) public class Application { }再或者把工具类放到主启动类同级的包或者子包下这样默认就能被扫描到。第三种是最省事而且最符合习惯的推荐优先使用。5.6 各种获取方式的适用场景对比最后给一张对比表方便大家在实际开发中快速选择。获取方式代码简洁度灵活度适用场景典型代码Autowired字段注入高低常规业务代码、同类型唯一beanAutowired private UserService userService;构造器注入中低需要强依赖、单元测试友好的场景public OrderService(UserService userService) { ... }Autowired Qualifier中中同类型有多个实现调用方明确指定Autowired Qualifier(vipUserServiceImpl)注入ApplicationContext中高需要在代码中动态获取各种beancontext.getBean(xxx, Xxx.class)ApplicationContextAware工具类中高工具类、非Spring管理的类中获取beanSpringContextUtils.getBean(Xxx.class)ObjectProvider低高延迟加载、可选依赖、循环依赖缓解provider.getIfAvailable()Map批量注入中中策略模式、处理器分发MapString, Handler handlerMap从业务项目的角度来说我会遵循这样一个基本顺序常规依赖注入能解决的就不要用getBeangetBean能解决的就不要把容器存静态变量只有当你写的不是业务代码而是基础能力组件、或者你想要动态解耦的时候才建议上ApplicationContextAware或Map批量注入这类高级写法。我的一些实操体会踩过几次坑之后我的体会是SpringBoot获取bean的方式虽然多但本质上就两条路可走一条是让Spring把bean送过来依赖注入另一条是你自己去容器里拿getBean。第一条路优雅但受限于“你必须是Spring管理的类”这个前提第二条路万能但写多了会让代码变得难维护二者要按场景灵活切换。最后分享一个小技巧如果你经常需要调试容器的内容可以在启动类里临时打印所有bean的名称SpringBootApplication public class Application { public static void main(String[] args) { ApplicationContext context SpringApplication.run(Application.class, args); Arrays.stream(context.getBeanDefinitionNames()) .filter(name - name.toLowerCase().contains(service)) .forEach(System.out::println); } }这个方法在排查bean是否注册、名称是否正确的时候特别好用。等到项目稳定了再删掉就行它能帮你省下不少凭空猜测的时间。
返回列表