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


优雅终结启动顺序噩梦:ObjectProvider ------ Spring 4.3 开始引入
从"饥渴式依赖"到"按需获取",一次依赖注入的思想跃迁
缘起:一个再普通不过的配置类,为何启动就报错?
就在上周,我在维护一个 Spring Boot 4.0 项目时遇到了一个棘手的问题:
java
@Configuration
public class WebmvcEncryptionConfiguration {
@Autowired
private HttpMessageConverter<Object> httpMessageConverter; // 注入总为 null
@Bean
FilterRegistrationBean<EncryptedFilter> encryptedFilterRegistrationBean() {
// 启动时抛出 NullPointerException,httpMessageConverter 未初始化!
return new FilterRegistrationBean<>(new EncryptedFilter(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 说 均为误传
权威证据链:
- Spring 官方博客 (2016年3月)明确写道:"Spring Framework 4.3 引入了
ObjectProvider,它是现有ObjectFactory接口的扩展,提供getIfAvailable和getIfUnique等便捷签名" - 版本时间线:Spring 4.3.0.RC1 发布于 2016年3月,4.3.0.GA 发布于 2016年5月;而 Spring 5.0 在 2017年9月才正式发布
- 历史实证 :有开发者反馈在 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 的本质:
| 维度 | @Autowired |
ObjectProvider<T> |
|---|---|---|
| 获取时机 | Bean 实例化立即解析 | 调用 getObject() 时延迟解析 |
| 依赖强度 | 默认强依赖(required=true) |
可选依赖,允许不存在 |
| 多实例处理 | 必须配合 @Qualifier/@Primary |
支持运行时动态筛选、流式处理 |
| 原型 Bean | 注入固定实例(违背原型语义) | 每次调用 getObject() 获取新实例 |
| 异常处理 | 启动即失败 | 将异常推迟到业务运行时 |
形象的比喻:
@Autowired:"咖啡必须在我进办公室前就放在桌上"ObjectProvider:"给我一张咖啡券,我想喝的时候自己去打"
2.2 Spring 源码级的特殊对待
为什么 ObjectProvider 能"躲过"启动时的依赖解析?秘密藏在 DefaultListableBeanFactory.resolveDependency() 中 :
java
@Override
public Object resolveDependency(DependencyDescriptor descriptor, ...) {
// 1. Optional<T>
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
}
// 2. ObjectFactory<T>、ObjectProvider<T> ------ 重点在这里!
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
// 不触发真正的依赖解析,直接返回一个"懒加载代理"
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
// ... 其他情况
else {
return doResolveDependency(descriptor, ...); // 立即解析
}
}
关键结论 :当 Spring 发现你注入的是 ObjectProvider<T> 时,它不会去容器中查找 T 类型的 Bean ,而是直接给你一个 DependencyObjectProvider 对象。真正的 Bean 查找被推迟到你第一次调用 getObject() 或 getIfAvailable() 的时刻。
这正是解决 FilterRegistrationBean 初始化顺序问题的终极武器------我的 Filter 可以提前注册,但 HttpMessageConverter 可以"按需再取"。
三、实战改造:从"饥渴注入"到"按需获取"
3.1 问题代码的完整解决方案
java
public class EncryptedFilter implements Filter {
private final WebClientConfigProperties properties;
private final RequestMappingHandlerMapping handlerMapping;
private final ObjectProvider<HttpMessageConverter<?>> converterProvider; // 注入提供者
public EncryptedFilter(WebClientConfigProperties properties,
RequestMappingHandlerMapping handlerMapping,
ObjectProvider<HttpMessageConverter<?>> converterProvider) {
this.properties = properties;
this.handlerMapping = handlerMapping;
this.converterProvider = converterProvider;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 真正需要转换器时才去获取------此时容器已完成自动配置
HttpMessageConverter<?> converter = converterProvider.getIfAvailable();
if (converter != null) {
// 使用转换器处理加解密逻辑
}
chain.doFilter(request, response);
}
}
配置类(彻底抛弃字段注入):
java
@Configuration
public class WebmvcEncryptionConfiguration {
@Bean
FilterRegistrationBean<EncryptedFilter> encryptedFilterRegistrationBean(
WebClientConfigProperties webClientConfigProperties,
RequestMappingHandlerMapping requestMappingHandlerMapping,
ObjectProvider<HttpMessageConverter<?>> httpMessageConverterProvider) { // 参数注入
EncryptedFilter filter = new EncryptedFilter(
webClientConfigProperties,
requestMappingHandlerMapping,
httpMessageConverterProvider); // 传递的是 Provider,不是具体的 Converter
FilterRegistrationBean<EncryptedFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
registrationBean.setFilter(filter);
registrationBean.setName("encryptedFilter");
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
✅ 改动极小,但彻底解决了启动顺序问题。
四、ObjectProvider 的四大黄金场景
如果说解决启动顺序只是 ObjectProvider 的"意外之喜",那么下面四个场景才是它真正的设计目标,也是每一位 Spring 开发者都应该掌握的进阶技巧。
场景一:优雅处理可选依赖(替代 @Autowired(required=false))
传统写法------丑陋的空值检查:
java
@Service
public class NotificationService {
@Autowired(required = false)
private SmsService smsService; // 可能为 null
public void sendAlert(String message) {
if (smsService != null) { // 每次都要判空
smsService.send(message);
}
}
}
ObjectProvider 写法------函数式、零判空:
java
@Service
public class NotificationService {
@Autowired
private ObjectProvider<SmsService> smsServiceProvider;
public void sendAlert(String message) {
// 存在才执行,不存在什么都不发生
smsServiceProvider.ifAvailable(sms -> sms.send(message));
// 或者提供默认实现
SmsService sms = smsServiceProvider.getIfAvailable(() -> new MockSmsService());
}
}
场景二:在单例 Bean 中正确获取原型 Bean(最核心用途)
错误示范------90% 开发者踩过的坑 :
java
@Component
@Scope("prototype")
public class TaskProcessor { /* ... */ }
@Service // 默认单例
public class TaskService {
@Autowired
private TaskProcessor taskProcessor; // ❌ 仅在启动时注入一次,之后永远是同一个实例!
public void execute() {
taskProcessor.process(); // 每次用的都是同一个对象
}
}
ObjectProvider 拯救:
java
@Service
public class TaskService {
@Autowired
private ObjectProvider<TaskProcessor> taskProcessorProvider;
public void execute() {
// 每次调用都从容器获取全新的原型实例
TaskProcessor processor = taskProcessorProvider.getObject();
processor.process();
}
}
与 @Lookup 对比 :
| 方式 | 优点 | 缺点 |
|---|---|---|
@Lookup |
注解简洁,无需显式注入 | 仅能用于方法,调试困难 |
ObjectProvider |
灵活、可编程、支持条件判断 | 需注入 Provider 对象 |
ApplicationContext |
功能最全 | 耦合容器,性能稍差 |
结论 :原型 Bean 获取,首选 ObjectProvider。
场景三:动态筛选多个同类型 Bean
当一个接口有多个实现时,传统方式必须指定 @Qualifier 或 @Primary,编译时就决定了用哪一个。
ObjectProvider 实现运行时动态选择 :
java
public interface PaymentService {
boolean supports(String type);
void pay(Order order);
}
@Service
public class PaymentProcessor {
@Autowired
private ObjectProvider<PaymentService> paymentServiceProvider;
public void processPayment(Order order, String paymentType) {
PaymentService service = paymentServiceProvider.stream()
.filter(ps -> ps.supports(paymentType))
.findFirst()
.orElseThrow(() -> new UnsupportedOperationException("不支持的支付方式"));
service.pay(order);
}
}
加上排序支持(Spring 5.1+):
java
// 按照 @Order 或 Ordered 接口的顺序获取所有实现
paymentServiceProvider.orderedStream()
.forEach(PaymentService::someCommonOperation);
场景四:解决循环依赖
虽然 Spring 三级缓存能解决大部分 setter 注入的循环依赖,但对于构造器注入的循环依赖依然束手无策。ObjectProvider 可以轻松破局:
java
@Component
public class ServiceA {
private final ObjectProvider<ServiceB> serviceBProvider;
public ServiceA(ObjectProvider<ServiceB> serviceBProvider) {
this.serviceBProvider = serviceBProvider;
}
public void doSomething() {
// 需要 B 的时候再去拿,此时 B 一定已初始化完毕
ServiceB b = serviceBProvider.getObject();
}
}
@Component
public class ServiceB {
private final ServiceA serviceA; // 正常构造器注入
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
五、与其他方案的对比:我该如何选择?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 普通依赖注入 | @Autowired 构造器注入 |
最简洁、最符合 DI 原则 |
| 依赖可能不存在 | ObjectProvider.getIfAvailable() |
无需判空、函数式风格 |
| 单例中获取原型 Bean | ObjectProvider |
最均衡:灵活、低耦合、性能优 |
| 运行时多实现动态选择 | 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 源码与官方文档撰写。