@Autowired 与 @Resource 依赖注入全过程
“@Autowired 按类型,@Resource 按名称”只是第一层结论。真正执行注入的是 Spring BeanPostProcessor:前者主要由 AutowiredAnnotationBeanPostProcessor 解析,后者由 CommonAnnotationBeanPostProcessor 解析。它们发现注入点、构造 DependencyDescriptor、查找候选、处理名称和限定符,最后通过构造器调用或反射/方法调用写入依赖。
学习目标
- 说清两种注解的来源、处理器、支持位置和执行时机;
- 理解构造器注入为何发生在实例化阶段,字段/Setter为何发生在属性填充阶段;
- 掌握
@Autowired按类型、Qualifier、Primary、Priority、名称的候选选择; - 掌握
@Resource显式名称、默认属性名和类型回退; - 理解 required=false、Optional、Nullable、ObjectProvider 的不同语义;
- 掌握 List、Set、Map、数组和泛型限定注入;
- 理解代理对象、父子容器、Profile/Condition 与注入类型的关系;
- 能排查 NoSuch、NoUnique、UnsatisfiedDependency、BeanNotOfRequiredType 和注入为 null;
- 理解 JDK 8
javax.annotation与 Spring 6jakarta.annotation迁移边界。
一、注解来源和处理器
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring Framework | JSR-250 / Jakarta Annotations |
| JDK8常见包 | org.springframework.beans.factory.annotation.Autowired | javax.annotation.Resource |
| Spring6常见包 | 同左 | jakarta.annotation.Resource |
| 主要处理器 | AutowiredAnnotationBeanPostProcessor | CommonAnnotationBeanPostProcessor |
| 默认思路 | 类型候选解析 | 名称优先,特定条件下类型回退 |
| 构造器 | 支持 | 注解本身通常不用于构造器参数 |
| 字段/Setter方法 | 支持 | 支持 |
@Resource 是标准注解,但在普通 Spring 应用中将其解析为 Spring Bean 的仍是 Spring 处理器。它不意味着每次都直接访问远程 JNDI;CommonAnnotationBeanPostProcessor 可根据配置和名称从 BeanFactory 或 JNDI 解析。
二、注入发生在 Bean 生命周期哪里
flowchart TD
A["解析BeanDefinition"] --> B["选择构造器"]
B --> C["解析构造器参数依赖"]
C --> D["调用构造器完成实例化"]
D --> E["BPP发现字段和Setter注入点"]
E --> F["解析依赖候选"]
F --> G["反射写字段或调用方法"]
G --> H["Aware与初始化回调"]
H --> I["后置处理和AOP代理"]2.1 构造器注入
对象产生前就必须解析依赖,依赖作为构造参数传入。创建后对象立即完整,字段可 final。构造器循环依赖因为双方都没有实例,通常无法解决。
2.2 字段与 Setter 注入
对象先通过构造器实例化,再由后置处理器扫描缓存的注入元数据,通过反射设置字段或调用 Setter/任意注入方法。对象在属性填充完成前处于不完整状态,这也是部分字段循环依赖能提前暴露引用的前提。
2.3 元数据缓存
处理器不会每次调用方法都重新扫描注解,而会按 Bean 类/名称缓存 InjectionMetadata;BeanDefinition 合并、类重载或上下文刷新时缓存可能重建。注入只发生在 Bean 创建阶段,不是每次业务方法调用都查容器。
三、@Autowired 完整候选解析
假设注入:
@Autowired
private PaymentClient paymentClient;主流程:
flowchart TD
A["构造DependencyDescriptor"] --> B["按ResolvableType找候选"]
B --> C["过滤autowireCandidate"]
C --> D["应用Qualifier和泛型限定"]
D --> E{"候选数量"}
E -- "0" --> F["处理required或可选语义"]
E -- "1" --> G["选择唯一候选"]
E -- "多个" --> H["检查Primary"]
H --> I["检查Priority"]
I --> J["检查依赖名称与Bean名"]
J --> K{"得到唯一候选吗"}
K -- "是" --> G
K -- "否" --> L["抛NoUniqueBeanDefinitionException"]准确顺序和细节随 Spring 版本演进,但核心不是“类型相同就随机选一个”。
3.1 @Primary
@Component
@Primary
class DefaultPaymentClient implements PaymentClient { }多个类型候选时作为默认优先候选。若多个候选都标 Primary,仍可能冲突。Primary 不会覆盖显式 Qualifier。
3.2 @Qualifier
public OrderService(
@Qualifier("bankPaymentClient") PaymentClient client) {
this.client = client;
}Qualifier 不是简单“强制按 Bean 名”,它是候选限定元数据,字符串常与 Bean 名/Qualifier 值匹配。可定义元注解表达业务语义:
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface OfflinePayment { }这比把类名字符串散落在业务代码更稳定。
3.3 Priority 与 Order
单值注入的优先候选可参考 @Priority/Ordered 相关规则;集合注入的排序常使用 @Order/Ordered。@Order 通常不等于单值候选的 Primary,不能混为一谈。
3.4 注入点名称回退
多个类型候选且无 Primary/Qualifier 时,Spring 可能用字段名或参数名匹配 Bean 名。参数名是否可用与编译 -parameters、调试信息和 Spring 版本有关。依赖“字段名刚好等于 Bean 名”很隐蔽,生产推荐显式 Qualifier。
四、@Resource 名称优先全过程
@Resource
private PaymentClient bankPaymentClient;未显式设置 name 时,默认名称通常由字段名或 Setter 属性名推导。常见解析:
flowchart TD
A["发现Resource注入点"] --> B{"显式指定name吗"}
B -- "是" --> C["按该名称查找资源"]
B -- "否" --> D["推导字段或属性默认名称"]
D --> E{"存在同名Bean吗"}
E -- "是" --> F["按名称取得并校验类型"]
E -- "否" --> G{"允许fallbackToDefaultTypeMatch吗"}
G -- "是" --> H["按注入点类型解析候选"]
G -- "否" --> I["抛找不到资源异常"]
C --> F4.1 显式 name
@Resource(name = "smsMessageService")
private MessageService messageService;显式 name 表达明确资源名称,不应再期待“名称找不到后随便按类型”。名称存在但类型不兼容会抛 BeanNotOfRequiredTypeException。
4.2 type 属性
@Resource(type=...) 可显式声明类型,但实际候选、名称、JNDI 和 BeanFactory 行为仍由处理器配置决定。通常直接使用字段/Setter类型更清晰,不应同时给出互相冲突的 name/type。
4.3 默认名称回退不是 JSR-250 的万能结论
“先按名称,失败再按类型”是 Spring CommonAnnotationBeanPostProcessor 常见配置行为,不应推广成所有 JSR-250 容器都完全相同;阅读源码和排查必须标注 Spring 版本及处理器配置。
五、两者支持的注入位置
5.1 @Autowired 构造器
public OrderService(PaymentClient client) {
this.client = client;
}Spring 4.3+ 单构造器通常不需要写注解。多构造器时可用 @Autowired(required=true/false) 指示候选,但复杂重载容易产生歧义。
5.2 @Autowired 方法
@Autowired
public void configure(PaymentClient client, AuditService auditService) {
this.client = client;
this.auditService = auditService;
}不限于 JavaBean Setter;整个方法参数一起解析。required=false 方法在依赖不满足时可能整体跳过,而不是给每个参数随意传 null,具体按版本语义验证。
5.3 @Resource Setter
@Resource(name = "bankPaymentClient")
public void setPaymentClient(PaymentClient client) {
this.client = client;
}标准 Resource 的目标通常是字段和方法,不用于构造器参数;需要构造器限定时使用 @Autowired + @Qualifier 或 JSR-330 @Inject + @Named(项目具备依赖时)。
六、为什么优先构造器注入
| 方式 | 优点 | 风险 |
|---|---|---|
| 构造器 | 强制依赖、final、直接测试、对象完整 | 参数过多暴露职责过重 |
| Setter | 真正可选/可替换依赖清晰 | 对象可能短暂不完整 |
| 字段 | 代码短 | 隐藏依赖、难直接new、不能final |
“Setter 性能一定比构造器好”没有普遍意义。容器最终都要解析依赖;设计应优先保证对象不变量和可测试性。构造器参数很多通常说明类承担职责过多,应拆分,而不是改字段注入隐藏问题。
七、可选依赖的四种语义
7.1 required=false
@Autowired(required = false)
private OptionalExporter exporter;没有候选时字段保持默认 null。业务每次使用都要判空;关键能力不应设置 false 后静默缺失。
7.2 Optional<T>
public ExportService(Optional<OptionalExporter> exporter) {
this.exporter = exporter;
}显式表达可能不存在,但实体字段到处使用 Optional 会增加复杂度。多个候选仍需消歧。
7.3 @Nullable
注入点标注可空,容器可在无候选时传 null。要确认使用的是 Spring 支持的 Nullable 注解和版本;null 仍有运行时误用风险。
7.4 ObjectProvider<T>
public ExportService(ObjectProvider<OptionalExporter> provider) {
this.provider = provider;
}支持按需、可选、迭代和有序流。getObject() 无候选会抛异常,getIfAvailable() 才返回 null/默认。它适合插件、Lazy和 singleton 每次取 prototype;不要用它把所有必需依赖推迟到运行时。
八、集合、数组和 Map 注入
public RuleEngine(List<ValidationRule> rules,
Map<String, ValidationRule> ruleMap) {
this.rules = rules;
this.ruleMap = ruleMap;
}- List/数组:所有类型匹配候选,通常按 Order/Ordered 排序;
- Set:所有候选,迭代顺序不要无条件假定;
- Map<String,T>:key 常为 Bean 名,value 是候选;
- 空集合语义与 required/版本有关,应测试;
- Qualifier 可过滤集合候选。
业务不能直接把外部输入当 ruleMap Bean 名调用任意 Bean,应维护安全业务码映射。
九、泛型作为候选限定
interface Handler<T> { void handle(T value); }
class OrderHandler implements Handler<Order> { }
class UserHandler implements Handler<User> { }
@Autowired
private Handler<Order> orderHandler;Spring 使用 ResolvableType 读取字段、参数、接口和父类泛型签名,能把泛型作为隐式限定符。Java 类型擦除不等于 class 文件中所有泛型签名消失。
风险:原始类型 Handler 会丢失限定;JDK/CGLIB代理的类型暴露、桥接方法和 FactoryBean getObjectType 可能影响匹配。复杂泛型注入要写上下文测试。
十、代理对象会怎样影响注入类型
JDK 动态代理实现接口但不是目标实现类子类:
@Autowired
private PaymentService paymentService; // 接口,稳定
@Autowired
private PaymentServiceImpl impl; // JDK代理时可能无法满足CGLIB 代理可按类赋值,但 final/可见性仍有限制。业务依赖应面向接口;排查 BeanNotOfRequiredType 时查看 bean.getClass()、AopUtils 和 FactoryBean#getObjectType()。
十一、父子容器、Profile 和 Condition
找不到 Bean 不一定是没写 @Component:
- Bean 可能在 DispatcherServlet 子上下文,父上下文看不到;
- Profile 未激活;
@Conditional未满足;- 配置类未被 Import;
- ComponentScan 不覆盖;
- 测试 Slice 只加载部分 Bean;
- 同名定义被覆盖/禁用;
- Spring Boot 条件回退注册了另一实现。
用 Condition Evaluation Report、BeanDefinition 列表和上下文层次查证,不能只扩大扫描包。
十二、@Resource 与 JNDI 的关系
Resource 最初用于容器资源注入,name 可指逻辑资源。Spring CommonAnnotationBeanPostProcessor 支持配置 jndiFactory、resourceFactory、alwaysUseJndiLookup 等策略;普通 Spring Bean 名解析和应用服务器 JNDI 不能混为一谈。
部署到应用服务器时,java:comp/env/...、resource-ref 和实际 JNDI 名称映射由容器配置决定。Spring Boot 独立应用通常直接注入 DataSource Bean,不需要为每个 Resource 走 JNDI。
十三、完整可运行 Demo
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
public class InjectionDemo {
interface MessageService { String send(); }
static class EmailService implements MessageService {
public String send() { return "EMAIL"; }
}
static class SmsService implements MessageService {
public String send() { return "SMS"; }
}
static class NoticeService {
private final MessageService constructorService;
@Resource(name = "smsMessageService")
private MessageService resourceService;
@Autowired
NoticeService(
@Qualifier("emailMessageService") MessageService service) {
this.constructorService = service;
}
String result() {
return constructorService.send() + "+" + resourceService.send();
}
}
@Configuration
static class Config {
@Bean @Primary
MessageService defaultMessageService() { return new SmsService(); }
@Bean
MessageService emailMessageService() { return new EmailService(); }
@Bean
MessageService smsMessageService() { return new SmsService(); }
@Bean
NoticeService noticeService(
@Qualifier("emailMessageService") MessageService service) {
return new NoticeService(service);
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
try {
System.out.println(context.getBean(NoticeService.class).result());
} finally {
context.close();
}
}
}期望输出 EMAIL+SMS:构造器 Qualifier 选 Email,字段 Resource 显式名称选 SMS。@Primary 的 default 不会覆盖显式限定。
十四、商业策略链 Demo
@Service
public class NotificationRouter {
private final Map<String, NotificationChannel> channels;
public NotificationRouter(List<NotificationChannel> channelList) {
Map<String, NotificationChannel> mapping = new HashMap<String, NotificationChannel>();
for (NotificationChannel channel : channelList) {
if (mapping.put(channel.code(), channel) != null) {
throw new IllegalStateException("duplicate channel code: " + channel.code());
}
}
this.channels = Collections.unmodifiableMap(mapping);
}
}这里按业务 code() 建映射,而不是把 Bean 名暴露给客户端。启动时检测重复,运行时无匹配返回明确业务错误。所有 Channel singleton 必须线程安全。
十五、常见异常诊断
15.1 NoSuchBeanDefinitionException
读取注入点完整 ResolvableType、Qualifier、上下文层次、Profile/Condition、扫描和配置导入。接口有实现类但实现未注册为 Bean,仍然找不到。
15.2 NoUniqueBeanDefinitionException
异常会列候选名称。判断业务是默认实现、指定实现还是全部策略;使用 Primary、Qualifier、List/Map。不要靠字段改名作为唯一修复,也不要删除合法实现。
15.3 UnsatisfiedDependencyException
是依赖链包装异常,沿最深 cause 查真正 Bean、构造器和配置。例如 A→B→DataSource,最外层报 A 注入失败,根因可能是数据库 URL 缺失。
15.4 BeanNotOfRequiredTypeException
名称找到了 Bean,但实际类型不能赋值。检查同名 Bean、FactoryBean 产品、JDK 代理和 @Resource(name=...) 是否指向错误类型。
15.5 字段仍为 null
常见不是 Spring “没注入”,而是对象手工 new、测试没有启动 Spring、字段 static、注入发生前在构造器使用字段、处理器未注册或该类不是 Bean。字段注入要到实例化后的属性填充才完成,构造器内读取必然尚未注入。
十六、循环依赖与注入方式
构造器 A→B→A 无法实例化任一方,启动立即失败,迫使暴露设计问题。字段/Setter singleton 可能通过早期引用解决,但代理一致性、Spring Boot 默认禁止循环依赖等会限制。能被容器解决不等于设计合理;优先拆职责、提取协调者或事件解耦。
十七、生产排查 Runbook
17.1 注入错实现
记录注入点、实际 Bean class、Bean 名、Qualifier、Primary、Profile、Condition 和代理类型。检查测试 Mock/备用配置是否进入生产。不要记录对象敏感配置。
17.2 本地成功、部署失败
比较构建参数 -parameters、JDK/Spring版本、ClassPath、Profile、环境变量、条件报告、组件扫描和父子上下文。字段名回退依赖编译元数据时尤其容易环境差异,改显式 Qualifier。
17.3 升级 Spring 6 后 Resource 不工作
检查是否仍使用 javax.annotation.Resource,依赖是否迁到 jakarta.annotation-api,第三方库是否支持 Jakarta。Spring 6/Boot 3 基线 Java 17;不能在 JDK 8 中通过仅改 import 升级。
17.4 启动变慢
检查候选数量、超宽扫描、复杂泛型、FactoryBean 类型预测、构造器中远程 I/O 和注入后初始化。依赖解析通常不是主要瓶颈,真正慢常在 Bean 构造/init。
17.5 运行时才报缺 Bean
通常使用 ObjectProvider、Lazy 或条件插件把错误推迟。确认依赖是否真正可选;核心能力应在启动时验证并失败,而不是首个请求才报错。
十八、版本迁移边界
| 环境 | Resource 包 |
|---|---|
| JDK 8 + Spring 5 | 常见 javax.annotation.Resource,API可能来自JDK/依赖 |
| JDK 9/10 | java.xml.ws.annotation模块已标记演进/弃用 |
| JDK 11+ | Java EE模块移除,需显式annotation-api依赖 |
| Spring 6 / Boot 3 | jakarta.annotation.Resource,Java 17基线 |
Spring @Autowired 包名没有随 Jakarta 改名。迁移时同时检查 @PostConstruct、@PreDestroy、Servlet/JPA/Validation 包,不要只替换 Resource。
十九、常见错误与后果
| 错误 | 后果 | 正确方向 |
|---|---|---|
| 多实现依赖字段名碰巧匹配 | 重命名后启动失败/注入变化 | 显式Qualifier |
| 多个Primary | 仍然NoUnique | 只定义一个默认 |
required=false用于核心依赖 | 运行时NPE/静默降级 | 启动失败 |
| 字段注入并在构造器读取 | 构造时为null | 构造器注入 |
| 手工new带注解对象 | 注入和AOP均失效 | 由容器创建 |
| Resource name指错类型 | BeanNotOfRequiredType | 校验名称与接口 |
| Map Bean名直接接受用户输入 | 可调用非预期策略 | 业务码白名单 |
| 注入具体实现遇JDK代理 | 类型不匹配 | 面向接口 |
| Lazy掩盖缺Bean | 首次流量失败 | 核心依赖启动校验 |
| javax/jakarta混用 | 注解不处理/类找不到 | 按版本整体迁移 |
二十、面试标准回答
Autowired 和 Resource 区别
Autowired来自Spring,由AutowiredAnnotationBeanPostProcessor处理,默认按ResolvableType筛选并结合Qualifier、Primary、Priority和名称;支持构造器、字段和方法。Resource来自JSR-250/Jakarta,由CommonAnnotationBeanPostProcessor处理,字段/Setter常按显式或推导名称查找,再按配置决定类型回退。
为什么推荐构造器注入
强制依赖创建时必须提供,对象创建后完整,字段可final,测试可直接new,并会立即暴露构造器循环依赖。字段注入依赖反射,在对象实例化后才填充,隐藏依赖且构造器中尚为null。
多个实现怎么注入
单值使用语义明确的Qualifier或唯一Primary;全部策略使用List/Map并用Order排序,再建立业务码映射。字段名匹配可作为容器回退,但不应作为核心设计契约。
Resource 按名称找不到一定按类型吗
不能无条件这么说。Spring 常见处理器在未显式name且默认名称不存在时,可按fallbackToDefaultTypeMatch进行类型解析;显式name通常要求该资源。具体行为取决于Spring版本和CommonAnnotationBeanPostProcessor配置。
字段有Autowired为什么仍是null
先确认对象是否由Spring创建;字段注入在实例化后属性填充阶段执行,因此构造器中读取仍为null。还要检查static字段、测试上下文、处理器、候选和是否手工new。
二十一、关联知识与验收
掌握验收:
- 能说出两种注解对应的 BeanPostProcessor;
- 能画出构造器注入与字段注入在生命周期中的不同位置;
- 能按类型、Qualifier、Primary、Priority、名称分析多个候选;
- 能解释 Resource 显式名称和默认名称回退差异;
- 能选择 required=false、Optional、Nullable、ObjectProvider;
- 能解释 List/Map 和泛型限定注入;
- 能定位手工new、JDK代理、父子上下文、Profile和Condition导致的注入问题;
- 能说明 javax.annotation 到 jakarta.annotation 迁移为什么不能只改一个import。
