@Autowired 报"找不到 bean"的那一行,往往不是注解写错了,而是那个类根本没进容器。Spring 的报错已经给了排查路径,只是很少有人读完:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.yg.order.OrderService required a bean of type
'com.yg.stock.StockClient' that could not be found.
Action:
Consider defining a bean of type 'com.yg.stock.StockClient' in your configuration.
1. 这个报错真正在说什么
| 原因 | 现象特征 | 处理 |
|---|---|---|
| 类不在扫描路径下 | 包名与启动类不同源,@Component 形同虚设 |
放到启动类同级或子包 |
| 注解缺失 | 类没有任何 @Component 家族注解 |
加注解,或用 @Bean |
| 是接口但无实现 | 报 "required a bean of type 接口" | 补实现类 |
| 多实现无法选择 | expected single matching bean but found 2 |
@Primary / @Qualifier |
读法很关键:Parameter 0 of constructor in X 说明容器已经在创建 X,缺的是第 0 个构造参数。先确定谁没进容器,别先怀疑注入写法。
2. IoC 反转的到底是什么
| 环节 | 没有容器 | 有容器 |
|---|---|---|
| 谁 new 对象 | 你的代码 | 容器 |
| 谁决定实现类 | 编译期写死 | 运行时按注册与条件定 |
| 谁装依赖 | 手工传参 | 按类型/名称注入 |
| 谁管销毁 | 你自己或不管 | 容器调销毁回调 |
反转的是对象创建权与依赖装配权:代码从"要什么就 new 什么"变成"声明需要什么,容器负责给"。
3. 容器启动做了什么
| 阶段 | 动作 | 失败时的典型报错 |
|---|---|---|
| 扫描与注册 | 找注解、生成 BeanDefinition |
路径外的类静默消失 |
| 实例化 | 调构造器 | 构造器参数找不到 bean |
| 注入 | 填充字段/setter | NoUniqueBeanDefinitionException |
| 初始化 | BeanPostProcessor → @PostConstruct → afterPropertiesSet → init-method |
初始化抛异常 → 启动失败 |
关键认知:注册与实例化是两个阶段。"类写了但没注册"与"注册了但实例化失败"是两类故障,报错位置不同。
4. 三种注入方式
// ① 字段注入:省字,难测
@Autowired private StockClient stockClient;
// ② setter 注入:适合可选依赖
@Autowired(required = false) public void setStockClient(StockClient c) { this.stockClient = c; }
// ③ 构造器注入:单构造器可省 @Autowired(Spring 4.3+)
private final StockClient stockClient;
public OrderService(StockClient c) { this.stockClient = c; }
| 维度 | 字段注入 | setter 注入 | 构造器注入 |
|---|---|---|---|
| 不可变(final) | 否 | 否 | 是 |
| 脱离容器单测 | 需反射 | 可直接 new | 可直接 new |
| 循环依赖 | 靠三级缓存绕过 | 能 | 启动即失败 |
| 依赖多时是否显眼 | 否,易堆到十几个 | 否 | 是 |
5. 为什么推荐构造器注入
// 单测里不需要 Spring
OrderService svc = new OrderService(new FakeStockClient());
三个理由都不抽象:不可变(final 保证依赖不被中途换掉);可测(不用启动容器、不用反射);循环依赖暴露得早(构造器环在启动时就炸,而不是等某个请求触发异常)。第三条最值钱:它把设计问题从运行期挪到了启动期。
6. @Component 家族与 @Bean
| 注解 | 语义 | 用在哪 |
|---|---|---|
@Component |
通用组件 | 说不清层级的类 |
@Service |
业务服务层 | 领域逻辑、事务边界 |
@Repository |
持久层 | 额外做持久层异常转换 |
@Bean |
方法级注册 | 无法加注解的类(如 DataSource) |
@Service 与 @Component 注册行为完全等价,差别只有语义;@Repository 多一层异常转换。@Bean 是手工装配入口:jar 里的类加不了注解,只能在配置类里 new 出来交给容器。
7. 作用域与"单例里注入 prototype"
| 作用域 | 生命周期 | 用在 |
|---|---|---|
singleton(默认) |
容器内唯一,关闭时销毁 | 无状态服务 |
prototype |
每次获取新建,容器不管销毁 | 有状态、短生命周期对象 |
@Service
public class TaskService {
@Autowired private TaskContext context; // 只在创建时注入一次
@Autowired private ObjectProvider<TaskContext> p;
public void run() { TaskContext fresh = p.getObject(); } // 每次新的
}
单例里注入 prototype,拿到的是注入那一刻的那一个,之后不变。要每次都新:ObjectProvider、@Lookup,或声明 proxyMode = ScopedProxyMode.TARGET_CLASS。
8. 生命周期钩子的执行顺序
| 顺序 | 回调 | 归属 |
|---|---|---|
| 1 | 构造器 | Java |
| 2 | 属性填充 / 依赖注入 | 容器 |
| 3 | BeanNameAware 等 Aware 接口 |
Spring 接口 |
| 4 | postProcessBeforeInitialization |
BeanPostProcessor |
| 5 | @PostConstruct |
Jakarta 注解 |
| 6 | afterPropertiesSet → 自定义 initMethod |
Spring 接口 / 配置 |
| 7 | postProcessAfterInitialization(AOP 代理常在此生成) |
BeanPostProcessor |
| 8 | @PreDestroy → destroy → destroyMethod |
关闭时 |
@Component
public class LifeProbe implements InitializingBean, DisposableBean {
public LifeProbe() { print("1 ctor"); }
@PostConstruct void post() { print("2 @PostConstruct"); }
public void afterPropertiesSet() { print("3 afterProps"); }
@PreDestroy void pre() { print("4 @PreDestroy"); }
public void destroy() { print("5 destroy"); }
void print(String s) { System.out.println(s); }
}
打印顺序固定为 1 → 2 → 3 → 4 → 5(按 Spring 6.x 核对)。记住第 7 步:AOP 代理在初始化末尾织入——构造器里调本类的 @Transactional 方法没有事务,原因就是代理还不存在。
9. 条件装配
@Configuration
public class StockConfig {
@Bean @ConditionalOnProperty(name = "stock.mode", havingValue = "remote")
public StockClient remoteStockClient() { return new RemoteStockClient(); }
@Bean @ConditionalOnMissingBean(StockClient.class)
public StockClient localStockClient() { return new LocalStockClient(); }
}
| 条件注解 | 判据 | 典型用途 |
|---|---|---|
@ConditionalOnProperty |
配置项等于指定值 | 开关某个实现 |
@ConditionalOnMissingBean |
容器里没有该类型 | 给默认实现,允许覆盖 |
@ConditionalOnClass |
类路径存在该类 | 可选依赖 |
自动配置是这套机制的规模化使用:Boot 3.x 读 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.x 用 spring.factories),每类挂一串 @ConditionalOn*,不满足就整类跳过。
10. 循环依赖与三级缓存
| 缓存 | 内容 | 作用 |
|---|---|---|
一级 singletonObjects |
完整可用的单例 | 正常获取 |
二级 earlySingletonObjects |
提前暴露的半成品 | 让 B 先拿到 A 的引用 |
三级 singletonFactories |
生成早期引用的工厂 | 需要 AOP 时也能提前拿到代理 |
A↔B 互注入:造 A → 依赖 B → 造 B → B 依赖 A → 从三级缓存取 A 的早期引用 → B 完成 → A 完成。这套流程只对 setter/字段注入有效:构造器注入时 A 还没实例,没有引用可提前暴露,直接抛 BeanCurrentlyInCreationException。Boot 2.6 起默认禁止循环依赖,需显式开 spring.main.allow-circular-references=true;默认禁止是对的,环通常意味着职责划错。
11. AOP 代理与"同类内部调用不生效"
@Service
public class OrderService {
@Transactional public void create(Order o) { ... }
public void batchCreate(List<Order> list) {
list.forEach(this::create); // this 不是代理 → 无事务
}
}
| 调用路径 | 经过代理 | 事务/异步生效 |
|---|---|---|
外部 → 代理 → create |
是 | 是 |
this.create(...) |
否 | 否 |
注入自身代理 → self.create(...) |
是 | 是 |
同一原因让 @Async、@Cacheable、@PreAuthorize 的自调用全失效。修法:拆类、注入自身(@Lazy 破除构造期循环)、或 AopContext.currentProxy()(需 exposeProxy=true)。
12. 多实现冲突:@Primary 与 @Qualifier
public interface StockClient { void deduct(String sku, int qty); }
@Service @Primary class RemoteStockClient implements StockClient { ... }
@Service("localStock") class LocalStockClient implements StockClient { ... }
// 精确选择,优先级高于 @Primary
public OrderService(@Qualifier("localStock") StockClient client) { ... }
选择规则:@Qualifier 指定名字 > 参数名与 bean 名匹配 > @Primary > 抛 NoUniqueBeanDefinitionException。参数名恰好叫 localStock 会"自动生效",这是最易误导人的隐式行为。
13. 最小示例:冲突 → 解决 → 验顺序
// ① 两个实现未指定 → 启动失败:found 2: remoteStockClient,localStockClient
// ② 加 @Primary 定默认;③ 用 @Qualifier 精确选择
@Service
public class Probe implements InitializingBean {
private final StockClient client;
public Probe(@Qualifier("localStock") StockClient c) { this.client = c; }
@PostConstruct void post() { System.out.println("post"); }
public void afterPropertiesSet() { System.out.println("afterProps"); }
}
启动日志里 post 在 afterPropertiesSet 之前,且 client 是 LocalStockClient 实例。
14. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 找不到 bean | 不在扫描路径 / 没注解 / 接口无实现 | 对齐包路径或补 @Bean |
@Transactional 不生效 |
自调用、非 public、异常被吞 | 走代理调用,核对异常类型 |
| 启动报循环依赖 | 构造器互相依赖 | 拆类或引入接口 |
| prototype 每次同一个 | 注入进单例只发生一次 | ObjectProvider / @Lookup |
15. 边界
| 情况 | 更合适的做法 |
|---|---|
| 依赖图复杂到需要画图 | 说明分层出问题,先拆模块 |
| 循环依赖打不开 | 优先改设计,开关只是止血 |
一句话:IoC 管的是对象的创建与装配,它不会替你决定分层与职责。
16. 自测
- "Parameter 0 of constructor in X" 说明容器卡在哪一步?
- 注册与实例化分开,带来哪两类不同故障?
- 构造器注入相比字段注入,多给出哪三个保证?
- 单例里注入 prototype,为什么拿到的总是同一个实例?
@PostConstruct与afterPropertiesSet谁先执行,AOP 代理在哪一步生成?- 为什么
this.create()上的@Transactional不生效,而注入自身代理就生效?