2431 字
约 8 分钟
15
S07-B08 · Spring IoC 入门:容器、注入与生命周期

@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. 自测

  1. "Parameter 0 of constructor in X" 说明容器卡在哪一步?
  2. 注册与实例化分开,带来哪两类不同故障?
  3. 构造器注入相比字段注入,多给出哪三个保证?
  4. 单例里注入 prototype,为什么拿到的总是同一个实例?
  5. @PostConstruct 与 afterPropertiesSet 谁先执行,AOP 代理在哪一步生成?
  6. 为什么 this.create() 上的 @Transactional 不生效,而注入自身代理就生效?
S07-B08 · Spring IoC 入门:容器、注入与生命周期
https://www.clxhxhhr.top/posts/6139/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。