优雅终结启动顺序噩梦:ObjectProvider —— Spring 4.3 开始引入

优雅终结启动顺序噩梦:ObjectProvider —— Spring 4.3 开始引入
🧑 博主简介ZEEKLOG博客专家「历代文学网」(PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程高并发设计分布式系统架构设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图


在这里插入图片描述

优雅终结启动顺序噩梦:ObjectProvider —— Spring 4.3 开始引入

从“饥渴式依赖”到“按需获取”,一次依赖注入的思想跃迁

缘起:一个再普通不过的配置类,为何启动就报错?

就在上周,我在维护一个 Spring Boot 4.0 项目时遇到了一个棘手的问题:

@ConfigurationpublicclassWebmvcEncryptionConfiguration{@AutowiredprivateHttpMessageConverter<Object> httpMessageConverter;// 注入总为 null@BeanFilterRegistrationBean<EncryptedFilter>encryptedFilterRegistrationBean(){// 启动时抛出 NullPointerException,httpMessageConverter 未初始化!returnnewFilterRegistrationBean<>(newEncryptedFilter(httpMessageConverter));}}

无论我怎么调整注入方式——字段注入、构造器注入、方法参数注入——HttpMessageConverter 始终是 null。更诡异的是,只要注释掉这个配置类,整个应用就能正常启动,HttpMessageConverter 也能被完美初始化

直觉告诉我:这不是注入“写错了”,而是 FilterRegistrationBean 的初始化时机早于 HttpMessageConverter 的自动配置

Spring 容器尚未完成 WebMvc 基础设施的装配,我的过滤器却已经急不可耐地索要依赖——这就像在清晨 5 点去咖啡馆要一杯现磨手冲,咖啡师还在通勤路上。

如何让依赖“等等再给”?

答案就藏在 Spring 4.3 引入的一个低调接口中:ObjectProvider


一、版本之问:ObjectProvider 究竟出生在哪一年?

关于 ObjectProvider 的引入版本,网上存在不少混淆信息。我需要在这里正本清源:

Spring Framework 4.3 首次引入 ObjectProvider
Spring 5.0 说、Spring Boot 2.0 说 均为误传

权威证据链

  1. Spring 官方博客(2016年3月)明确写道:“Spring Framework 4.3 引入了 ObjectProvider,它是现有 ObjectFactory 接口的扩展,提供 getIfAvailablegetIfUnique 等便捷签名”
  2. 版本时间线:Spring 4.3.0.RC1 发布于 2016年3月,4.3.0.GA 发布于 2016年5月;而 Spring 5.0 在 2017年9月才正式发布
  3. 历史实证:有开发者反馈在 Spring 4.2.4 中遇到 NoClassDefFoundError: org/springframework/beans/factory/ObjectProvider,升级到 4.3 后解决

为什么会有 5.0 的说法?因为 Spring 5.0 和 Spring Boot 2.0 增强了ObjectProvider(如添加 orderedStream() 方法),但它真正的诞生时刻是 2016 年 5 月,Spring Framework 4.3 GA


二、原理深潜:ObjectProvider 为什么能解决顺序问题?

2.1 两种依赖获取哲学的较量

要理解 ObjectProvider 的优雅,首先要看清 @Autowired 的本质:

维度@AutowiredObjectProvider<T>
获取时机Bean 实例化立即解析调用 getObject()延迟解析
依赖强度默认强依赖(required=true可选依赖,允许不存在
多实例处理必须配合 @Qualifier/@Primary支持运行时动态筛选、流式处理
原型 Bean注入固定实例(违背原型语义)每次调用 getObject() 获取新实例
异常处理启动即失败将异常推迟到业务运行时

形象的比喻

  • @Autowired“咖啡必须在我进办公室前就放在桌上”
  • ObjectProvider“给我一张咖啡券,我想喝的时候自己去打”

2.2 Spring 源码级的特殊对待

为什么 ObjectProvider 能“躲过”启动时的依赖解析?秘密藏在 DefaultListableBeanFactory.resolveDependency() 中 :

@OverridepublicObjectresolveDependency(DependencyDescriptor descriptor,...){// 1. Optional<T>if(Optional.class== descriptor.getDependencyType()){returncreateOptionalDependency(descriptor, requestingBeanName);}// 2. ObjectFactory<T>、ObjectProvider<T> —— 重点在这里!elseif(ObjectFactory.class== descriptor.getDependencyType()||ObjectProvider.class== descriptor.getDependencyType()){// 不触发真正的依赖解析,直接返回一个“懒加载代理”returnnewDependencyObjectProvider(descriptor, requestingBeanName);}// ... 其他情况else{returndoResolveDependency(descriptor,...);// 立即解析}}

关键结论:当 Spring 发现你注入的是 ObjectProvider<T> 时,它不会去容器中查找 T 类型的 Bean,而是直接给你一个 DependencyObjectProvider 对象。真正的 Bean 查找被推迟到你第一次调用 getObject()getIfAvailable() 的时刻。

这正是解决 FilterRegistrationBean 初始化顺序问题的终极武器——我的 Filter 可以提前注册,但 HttpMessageConverter 可以“按需再取”


三、实战改造:从“饥渴注入”到“按需获取”

3.1 问题代码的完整解决方案

publicclassEncryptedFilterimplementsFilter{privatefinalWebClientConfigProperties properties;privatefinalRequestMappingHandlerMapping handlerMapping;privatefinalObjectProvider<HttpMessageConverter<?>> converterProvider;// 注入提供者publicEncryptedFilter(WebClientConfigProperties properties,RequestMappingHandlerMapping handlerMapping,ObjectProvider<HttpMessageConverter<?>> converterProvider){this.properties = properties;this.handlerMapping = handlerMapping;this.converterProvider = converterProvider;}@OverridepublicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{// 真正需要转换器时才去获取——此时容器已完成自动配置HttpMessageConverter<?> converter = converterProvider.getIfAvailable();if(converter !=null){// 使用转换器处理加解密逻辑} chain.doFilter(request, response);}}

配置类(彻底抛弃字段注入):

@ConfigurationpublicclassWebmvcEncryptionConfiguration{@BeanFilterRegistrationBean<EncryptedFilter>encryptedFilterRegistrationBean(WebClientConfigProperties webClientConfigProperties,RequestMappingHandlerMapping requestMappingHandlerMapping,ObjectProvider<HttpMessageConverter<?>> httpMessageConverterProvider){// 参数注入EncryptedFilter filter =newEncryptedFilter( webClientConfigProperties, requestMappingHandlerMapping, httpMessageConverterProvider);// 传递的是 Provider,不是具体的 ConverterFilterRegistrationBean<EncryptedFilter> registrationBean =newFilterRegistrationBean<>(); registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); registrationBean.setFilter(filter); registrationBean.setName("encryptedFilter"); registrationBean.addUrlPatterns("/*");return registrationBean;}}

改动极小,但彻底解决了启动顺序问题


四、ObjectProvider 的四大黄金场景

如果说解决启动顺序只是 ObjectProvider 的“意外之喜”,那么下面四个场景才是它真正的设计目标,也是每一位 Spring 开发者都应该掌握的进阶技巧。

场景一:优雅处理可选依赖(替代 @Autowired(required=false)

传统写法——丑陋的空值检查:

@ServicepublicclassNotificationService{@Autowired(required =false)privateSmsService smsService;// 可能为 nullpublicvoidsendAlert(String message){if(smsService !=null){// 每次都要判空 smsService.send(message);}}}

ObjectProvider 写法——函数式、零判空:

@ServicepublicclassNotificationService{@AutowiredprivateObjectProvider<SmsService> smsServiceProvider;publicvoidsendAlert(String message){// 存在才执行,不存在什么都不发生 smsServiceProvider.ifAvailable(sms -> sms.send(message));// 或者提供默认实现SmsService sms = smsServiceProvider.getIfAvailable(()->newMockSmsService());}}

场景二:在单例 Bean 中正确获取原型 Bean(最核心用途)

错误示范——90% 开发者踩过的坑 :

@Component@Scope("prototype")publicclassTaskProcessor{/* ... */}@Service// 默认单例publicclassTaskService{@AutowiredprivateTaskProcessor taskProcessor;// ❌ 仅在启动时注入一次,之后永远是同一个实例!publicvoidexecute(){ taskProcessor.process();// 每次用的都是同一个对象}}

ObjectProvider 拯救

@ServicepublicclassTaskService{@AutowiredprivateObjectProvider<TaskProcessor> taskProcessorProvider;publicvoidexecute(){// 每次调用都从容器获取全新的原型实例TaskProcessor processor = taskProcessorProvider.getObject(); processor.process();}}

与 @Lookup 对比

方式优点缺点
@Lookup注解简洁,无需显式注入仅能用于方法,调试困难
ObjectProvider灵活、可编程、支持条件判断需注入 Provider 对象
ApplicationContext功能最全耦合容器,性能稍差

结论原型 Bean 获取,首选 ObjectProvider

场景三:动态筛选多个同类型 Bean

当一个接口有多个实现时,传统方式必须指定 @Qualifier@Primary编译时就决定了用哪一个

ObjectProvider 实现运行时动态选择

publicinterfacePaymentService{booleansupports(String type);voidpay(Order order);}@ServicepublicclassPaymentProcessor{@AutowiredprivateObjectProvider<PaymentService> paymentServiceProvider;publicvoidprocessPayment(Order order,String paymentType){PaymentService service = paymentServiceProvider.stream().filter(ps -> ps.supports(paymentType)).findFirst().orElseThrow(()->newUnsupportedOperationException("不支持的支付方式")); service.pay(order);}}

加上排序支持(Spring 5.1+):

// 按照 @Order 或 Ordered 接口的顺序获取所有实现 paymentServiceProvider.orderedStream().forEach(PaymentService::someCommonOperation);

场景四:解决循环依赖

虽然 Spring 三级缓存能解决大部分 setter 注入的循环依赖,但对于构造器注入的循环依赖依然束手无策。ObjectProvider 可以轻松破局:

@ComponentpublicclassServiceA{privatefinalObjectProvider<ServiceB> serviceBProvider;publicServiceA(ObjectProvider<ServiceB> serviceBProvider){this.serviceBProvider = serviceBProvider;}publicvoiddoSomething(){// 需要 B 的时候再去拿,此时 B 一定已初始化完毕ServiceB b = serviceBProvider.getObject();}}@ComponentpublicclassServiceB{privatefinalServiceA serviceA;// 正常构造器注入publicServiceB(ServiceA serviceA){this.serviceA = serviceA;}}

五、与其他方案的对比:我该如何选择?

场景推荐方案理由
普通依赖注入@Autowired 构造器注入最简洁、最符合 DI 原则
依赖可能不存在ObjectProvider.getIfAvailable()无需判空、函数式风格
单例中获取原型 BeanObjectProvider最均衡:灵活、低耦合、性能优
运行时多实现动态选择ObjectProvider.stream()原生支持流式处理
仅需简单原型获取@Lookup代码最少,无额外注入
需要完全控制容器ApplicationContext功能最全,但耦合度高
启动顺序问题ObjectProvider唯一的“延迟查找”解

核心决策标准:只要存在 “依赖的创建/解析时机晚于当前 Bean 的初始化时机”,就应该考虑 ObjectProvider


六、结语:为什么说 ObjectProvider 是一颗“时间胶囊”?

回顾开篇的问题,FilterRegistrationBean 之所以无法直接注入 HttpMessageConverter,本质上是 基础设施组件与业务组件初始化阶段的不匹配

ObjectProvider 的伟大之处,不是它提供了什么复杂的功能,而是它改变了我们对依赖注入的思考方式

  • 从“饥渴式”到“按需式”:依赖不一定要在注入点就绪,可以等到真正使用时才获取
  • 从“静态绑定”到“动态解析”:依赖的选择可以从编译期推迟到运行期
  • 从“强依赖”到“可选依赖”:允许依赖的不确定性,并在语言层面优雅处理

这让我想起计算机科学中的一句名言:“计算机科学中的所有问题,都可以通过增加一个间接层来解决”ObjectProvider<T> 正是这样一层优雅的“间接”——它将“依赖”封装成“获取依赖的能力”,将“对象”升级为“对象提供者”。

当你下次在 Spring Boot 启动过程中遇到类似的顺序问题时,不妨问问自己:

“我是否可以在当前 Bean 中,不为具体的依赖,而为它的‘提供者’预留一个位置?”

这个答案,早在 2016 年春天,Spring 4.3 就已经为你准备好了。


附录:ObjectProvider 核心 API 速查表

方法行为典型场景
T getObject()获取 Bean,不存在/不唯一则抛异常必需依赖、原型 Bean
T getIfAvailable()存在则返回,否则返回 null可选依赖
T getIfAvailable(Supplier<T>)不存在则返回默认值提供降级方案
void ifAvailable(Consumer<T>)存在时执行操作函数式风格、零判空
T getIfUnique()唯一则返回,否则返回 null期望单一候选
Stream<T> stream()返回所有匹配 Bean 的流多实现处理
Stream<T> orderedStream()按 @Order 排序的流有序多实现
本文由真实生产问题驱动,结合 Spring 源码与官方文档撰写。

Read more

大话Rust的前生今世

大话Rust的前生今世

(本故事纯属戏说,如有雷同,那绝对是因为Rust太耀眼) 文章目录 * 混沌初开,天神震怒 * 十年磨一剑,霜刃未曾试 * 独门绝技,震惊武林 * 第一式:所有权系统 - 内存管理的太极拳 * 第二式:生命周期 - 变量的生死簿 * 第三式:零成本抽象 - 白嫖的性能 * 攻城略地,诸侯臣服 * WebAssembly:新世界的开拓者 * 区块链:信任的基石 * 操作系统:旧王座的挑战者 * 嵌入式:小车扛大炮 * 生态繁荣,万国来朝 * Crates.io:包罗万象的藏经阁 * 社区:最友好的极客聚集地 * 工具链:程序员的美梦成真 * 群雄逐鹿,谁与争锋 * 未来已来,星辰大海 * 修行之路,痛并快乐 * 传奇继续,代码不朽 * Rust说

By Ne0inhk
计算机毕业设计springboot网上家教管理系统 基于SpringBoot的在线辅导信息管理平台 基于SpringBoot的家教服务数字化运营系统

计算机毕业设计springboot网上家教管理系统 基于SpringBoot的在线辅导信息管理平台 基于SpringBoot的家教服务数字化运营系统

计算机毕业设计springboot网上家教管理系统dos77szd (配套有源码 程序 mysql数据库 论文) 本套源码可以在文本联xi,先看具体系统功能演示视频领取,可分享源码参考。 随着互联网技术的深度渗透与教育信息化进程的持续加速,传统家教行业正经历着从线下撮合到线上智能匹配的转型变革。当前家教市场存在供需信息不对称、教学质量难以追溯、服务流程缺乏标准化等痛点,家长与学生在寻找合适家教资源时面临效率低下、选择困难等问题,而优质教师资源也难以精准触达目标学员群体。在此背景下,构建一套能够实现家教资源数字化管理、教学过程全链路追踪、服务质量可量化评估的在线平台,对于提升家教行业运营效率、保障教学服务品质、促进教育公平具有重要意义。本平台采用SpringBoot框架进行开发,整合Java技术与MySQL数据库,致力于打造连接学员、教师与平台运营方的三方协同管理系统。 本文围绕系统设计与实现展开,涵盖技术选型分析、系统架构规划、数据库建模及核心功能模块开发等关键环节。系统涉及的功能模块包括:注册登录、个人信息修改、系统首页、用户管理、教师管理、课程分类管理、课程信息管理、课程预

By Ne0inhk
Python开发从入门到精通:网络爬虫高级应用与Scrapy框架

Python开发从入门到精通:网络爬虫高级应用与Scrapy框架

《Python开发从入门到精通》设计指南第三十九篇:网络爬虫高级应用与Scrapy框架 一、学习目标与重点 💡 学习目标:掌握Python网络爬虫的高级技巧,包括Scrapy框架、分布式爬虫、动态网页爬取、反爬虫策略等;学习Scrapy、Selenium、BeautifulSoup等库的使用;通过实战案例实现网络爬虫应用。 ⚠️ 学习重点:Scrapy框架、分布式爬虫、动态网页爬取、反爬虫策略、Selenium库、BeautifulSoup库、网络爬虫实战。 39.1 网络爬虫概述 39.1.1 什么是网络爬虫 网络爬虫(Web Crawler)是一种程序,用于自动访问网页并提取信息。网络爬虫的应用场景包括数据分析、搜索引擎、内容聚合等。 39.1.2 网络爬虫的流程 * 发送请求:向网页发送HTTP请求。 * 获取响应:获取网页的HTML内容。 * 解析内容:提取网页中的信息。 * 存储数据:将提取的信息存储到数据库或文件中。

By Ne0inhk
Flutter 组件 fletch 的适配 鸿蒙Harmony 实战 - 驾驭高性能网络爬虫、实现鸿蒙端多并发与自定义拦截器的资产自动化抓取方案

Flutter 组件 fletch 的适配 鸿蒙Harmony 实战 - 驾驭高性能网络爬虫、实现鸿蒙端多并发与自定义拦截器的资产自动化抓取方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 fletch 的适配 鸿蒙Harmony 实战 - 驾驭高性能网络爬虫、实现鸿蒙端多并发与自定义拦截器的资产自动化抓取方案 前言 在数据驱动的鸿蒙(OpenHarmony)应用开发中,很多时候我们需要从外部网络环境大规模采集实时资讯、获取海量资源路径或者是进行自动化的接口探测。传统的 http 库虽然简单,但在面对数十路并发下载、复杂的 Cookie 状态维持以及多级的请求拦截(Interceptor)时,往往显得捉襟见肘。 fletch 正是一款专为高性能、工业级抓取任务设计的 Dart 网络增强库。它不仅支持极致的并发限流,更提供了一套类似拦截器管线的强大插件化能力。 适配到鸿蒙系统后,配合鸿蒙底层的网络切片和能效策略,fletch 能让你的数据采集应用在保持低功耗的同时,展现出前所未有的吞吐力。本文将为你深入剖析 fletch 在鸿蒙实战环境下的深度集成与优化。 一、原理解析 / 概念介绍 1.1

By Ne0inhk