跳到主要内容
约 6 分钟3 张卡

依赖注入

依赖注入由容器把协作对象交给 Bean,例如为订单服务提供仓库与支付客户端。本文先比较构造器、Setter 和字段注入,再说明多个候选 Bean 怎样选择,以及可选依赖和延迟获取怎样表达。最后介绍 @Resource 的名称解析规则。

1. 面试核心​

先记住这几条,再读正文
  • 构造器注入。 构造器注入让 Bean 在构造完成时取得必需依赖,字段可声明为 final,测试可直接传入替代对象。卡 · 构造器注入
  • 单构造器约定。 Spring 4.3 起,单构造器 Bean 可省略 @Autowired。
  • 类型候选选择。 @Autowired 按类型寻找候选,@Qualifier 缩小候选范围,@Primary 为单值注入指定首选项。卡 · 多个候选 Bean
  • 名称解析。 @Resource 优先按名称解析,默认名称缺失时可回退按类型。
  • 可选与歧义。 ObjectProvider.getIfAvailable() 在没有候选时返回 null,但多个候选无法唯一选择时仍会报错。卡 · ObjectProvider

2. 构造器注入​

对象要正常工作所必需的依赖,可以直接写在构造器参数中。这样,创建者必须先提供这些对象,才能完成构造:

@Service
public class OrderService {
private final OrderRepository repository;
private final PaymentClient paymentClient;

public OrderService(
OrderRepository repository,
PaymentClient paymentClient) {
this.repository = repository;
this.paymentClient = paymentClient;
}
}

示例构造完成时已经取得仓库与支付客户端,满足了对象工作所需的基本约束。测试可直接传入桩对象,也就是为测试提供固定行为的替代实现,无需先启动容器。字段还可以声明为 final,让依赖引用在构造后保持不变。

Spring 4.3 起,单构造器 Bean 可省略 @Autowired。存在多个构造器时,应明确指定注入入口。参数过多说明需要检查类的职责和依赖边界,改成字段注入只会隐藏这些依赖。

构造器要求依赖先存在,因此直接依赖环会在创建阶段暴露,具体见 Spring 循环依赖。

3. Setter 注入​

Setter 适合可选或确实需要重新配置的依赖。可选依赖缺失时,对象也要有明确行为,例如默认跳过审计输出:

private AuditSink auditSink = AuditSink.noop();

@Autowired(required = false)
public void setAuditSink(AuditSink auditSink) {
this.auditSink = auditSink;
}

缺少 AuditSink 候选时,容器跳过这个 Setter,保留 AuditSink.noop() 默认实现。有候选时,再调用 Setter 替换它。若缺少依赖就无法工作,应改为构造器参数,避免把空值错误推迟到业务调用时。

Setter 允许构造后替换依赖,因此对象还要面对依赖变化后的行为。只有确实需要重新配置时,才应保留这种可变性。

4. 字段注入​

字段注入让容器直接填充成员变量,声明更短,但依赖从构造器签名中消失了:

@Autowired
private PaymentClient paymentClient;

字段由容器在实例化后填充,因此直接 new 得到的对象还缺少支付客户端。测试需要启动容器、额外装配或借助反射,字段也难以通过正常构造赋值为 final。

框架基础设施和遗留代码中仍会见到字段注入。新业务代码使用构造器,可以让必需依赖直接出现在签名中,也更容易独立测试。

5. 多个候选 Bean​

同一个接口有多个实现时,容器需要明确应该交付哪一个。@Autowired 先按类型寻找候选,也会利用泛型信息区分类型。@Qualifier 再筛选符合限定条件的候选,单值注入还可用 @Primary 指定首选:

OrderService(@Qualifier("online") PaymentClient client) {
this.client = client;
}

online 可以是限定符,也可匹配候选 Bean 名,但匹配范围始终是类型兼容的候选。限定符宜表达稳定的业务角色,例如线上支付,也可定义 @OnlinePayment 这样的业务注解。这样,更换实现类名称时,注入意图仍然清楚。

缺少更强的选择标记时,容器还可用字段名或参数名匹配 Bean 名。Spring 6.1 起,参数名回退需要启用 -parameters 编译选项,让编译产物保留参数名。若最终仍有多个候选无法唯一选择,注入就会报错。

业务需要多个实现共同参与时,可以注入 List<PaymentClient> 收集匹配候选。集合仍包含其他匹配项,@Primary 只为单值注入指定首选。列表顺序可通过 @Order 调整,注入 Map<String, PaymentClient> 时则以 Bean 名为键。

按请求动态选择支付策略时,可以把这张 Map 交给明确的路由器。路由器需要校验传入的键,并定义找不到策略时的回退或报错行为。

6. ObjectProvider​

ObjectProvider<T> 让 Bean 先拿到获取依赖的入口,等实际调用时再向容器解析对象。它适合延迟创建或按需获取较短作用域的对象,不同方法对缺失与歧义的处理如下:

API缺失候选多候选且无法唯一选择
getObject()抛异常抛异常
getIfAvailable()返回 null抛异常
getIfUnique()返回 null返回 null
orderedStream()空流按顺序遍历候选

Optional<T> 也允许没有候选,但仍要求单值解析唯一。required=false 只放宽缺失条件,多个候选造成的歧义仍需用限定符等规则解决。

Provider 把获取时机写进了代码。如果大量使用它只是为了避开循环依赖,结构问题就会延迟到运行时暴露,职责拆分仍需完成。

7. 不同作用域的注入​

singleton 直接注入 prototype 时只解析一次,需要每次取得新实例时,可使用 Provider、方法注入或作用域代理。作用域代理会在调用时查找目标,request Bean 则要求调用时存在有效请求上下文。

按需获取的示例和异步线程限制见 Spring 循环依赖。

8. Resource 名称解析​

注入目标由稳定名称标识时,可以使用 @Resource。它来自通用注解规范,适用于字段和 Setter,优先按名称获取 Bean。Spring 6 使用 jakarta.annotation.Resource:

@Resource(name = "onlinePayment")
private PaymentClient paymentClient;

示例显式指定 onlinePayment,容器需要找到这个名称且类型兼容的 Bean。省略 name 时,默认名称取字段名或 Setter 对应的属性名。只有这个默认名称不存在时,Spring 才可回退到按类型解析。

@Autowired 属于 Spring 注解,以类型候选为起点,并支持构造器。团队应统一注入约定,使用明确的名称或限定符表达选择意图,减少对字段名偶然匹配的依赖。

9. 常见问题​

9.1 接口与依赖边界​

接口适合表达稳定的协作约定,例如支付服务需要提供哪些能力。即使只有一个实现,只要边界清晰、确有替换需求,按接口注入仍有价值。若接口只是逐项复制实现类方法,却没有独立含义,就会增加维护成本。

9.2 可选依赖的默认行为​

10 · 面试题卡片​

10.1 Spring 中有哪些依赖注入方式,怎样选择​

出现公司:交通银行、金山云

考察重点:根据依赖是否必需选择注入方式,解释它对对象可用性和测试的影响,再判断多候选与不同作用域怎样处理。

相关内容:构造器注入至不同作用域的注入。

参考回答

Spring 支持构造器、Setter 和字段注入,必需依赖优先通过构造器提供。

这样,对象构造完成时就具备依赖,字段可声明为 final,测试也能直接传入替代实现。可选且有默认行为的依赖适合 Setter。字段注入则需要容器在构造后填充,隐藏了构造要求,也增加了独立测试的成本。

多个实现可用 @Qualifier 筛选,单值首选可用 @Primary。业务需要动态选择策略时,可注入集合或 Map,再明确路由规则。延迟获取与跨作用域访问可以使用 ObjectProvider 或作用域代理,同时应满足目标作用域的上下文要求。

10.2 Autowired、Qualifier、Primary 与 Resource 怎样选择 Bean?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:说明类型、限定符和名称怎样参与候选选择,并区分单值首选与集合收集的行为。

相关内容:多个候选 Bean、Resource 名称解析。

参考回答

@Autowired 从类型候选中选择,@Qualifier 缩小范围,@Primary 为单值注入指定首选。

字段名或参数名也可参与回退匹配,Spring 6.1 起参数名匹配需要 -parameters。集合注入会收集所有匹配项,Primary 标记只影响单值选择。

@Resource 优先按名称获取。显式指定 name 时,要求名称存在且类型兼容;省略时取字段名或属性名,这个默认名缺失后才可回退按类型查找。

10.3 getIfAvailable 能忽略多个候选造成的歧义吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:区分没有候选与无法唯一选择候选,并按业务需要选择返回 null、报错或遍历全部实现。

相关内容:ObjectProvider。

参考回答

getIfAvailable() 无法忽略候选歧义,多个候选无法唯一选择时仍会报错。

它只在完全没有候选时返回 null。

如果业务希望在缺失或无法唯一选择时都使用默认行为,可用 getIfUnique(),两种情况都会返回 null。需要全部候选时则使用流或集合,让业务代码明确选择方式。

11. 参考资料​