SPRING · IOC CONTAINER · DEPENDENCY INJECTION

你只说要什么,容器负责造好送来

控制反转(IoC):创建对象、把对象拼装起来的活,从你手里交给了容器。依赖注入(DI)是它的做法:你在构造器里写明需要什么,容器造好了送进来。

自己 new每个依赖都由你来造、你来拼
DataSource ds = new HikariDataSource(config);
SqlSessionFactory f = buildFactory(ds);
OrderMapper mapper = buildMapper(f);
PayClient pay = new AlipayClient(appId, key);
OrderService service = new OrderService(mapper, pay);
// 想换成微信支付?改这里,
// 还有所有 new 过 AlipayClient 的地方
交给容器只声明要什么
@Service
public class OrderService {
    private final OrderMapper mapper;
    private final PayClient pay;

    public OrderService(OrderMapper mapper, PayClient pay) {
        this.mapper = mapper;   // 谁来造、造哪个实现,
        this.pay = pay;         // 都不归它管
    }
}
5OrderService
3OrderMapper
2SqlSessionFactory
1DataSource
4PayClient → AlipayClient
容器按依赖倒着造要造 OrderService,先看它要什么;要的东西还没有,就先去造那个。圆圈里的数字是实际的创建顺序:最底下的先造好,再一层层往上交。想换实现,只要换掉容器里登记的那个 PayClient,OrderService 一行不用改。
图纸BeanDefinition

先有图纸,再有对象。写着哪个类、单例还是多例、依赖谁。

扫描、@Bean、自动配置都产出图纸
工厂ApplicationContext

按图纸造对象。里面真正干活的是 DefaultListableBeanFactory。

也就是常说的 IoC 容器
成品架singletonObjects

单例造好后放这里,谁要都给同一个。

一个按名字存的 Map
送货员AutowiredAnnotationBeanPostProcessor

看你要什么类型,去成品架上挑一个送来。挑不出来就报错。

@Resource 归 CommonAnnotationBeanPostProcessor 管

容器里有什么

BeanFactory 是最基础的容器,ApplicationContext 在它外面包了一圈常用功能。平时说的 Spring 容器,指的都是 ApplicationContext。

BeanFactory最基础的容器

  • 按图纸造对象、注入依赖
  • 用到才造(getBean 时)
  • BeanPostProcessor 要手动登记

ApplicationContext平时用的容器

  • 启动时就把单例全造好,配置错了马上发现
  • 自动登记 BeanPostProcessor、BeanFactoryPostProcessor
  • 还管事件(第 08 篇)、配置、国际化、资源加载
DefaultListableBeanFactory 里的几个关键 Map
图纸柜beanDefinitionMap
orderService → 图纸alipayClient → 图纸dataSource → 图纸……
按类型的索引allBeanNamesByType
PayClient → [alipayClient, wechatPayClient]
成品架singletonObjects
orderService → OrderService@1a2balipayClient → AlipayClient@3c4d

getBean 时先看成品架,有就直接给;没有就拿图纸去造,造的过程见 第 04 篇的十四站。

图纸从哪来

写法有好几种,到了容器里都变成同一种东西:BeanDefinition。

组件扫描@Component · @Service · @Repository · @Controller
配置类里的方法@Configuration + @Bean
导入@Import · 自动配置(第 03 篇)
写代码登记registerBean · BeanDefinitionRegistryPostProcessor
一张图纸BeanDefinition · alipayClient
beanClass
com.shop.pay.AlipayClient
scope
singleton(默认)
lazyInit
false:启动时就造
primary
false
依赖
构造器参数:PayProperties
init / destroy
@PostConstruct、@PreDestroy 的方法

@Configuration 里的 @Bean 方法互相调用,会怎样

同样的代码,配置类有没有被代理,结果不一样。

@Configuration默认 proxyBeanMethods = true
@Configuration
public class DbConfig {
    @Bean DataSource dataSource() { return new HikariDataSource(); }

    @Bean JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());   // 调了一次方法
    }
}

配置类被 CGLIB 代理了:dataSource() 被拦下,直接从成品架拿,还是同一个 DataSource。

@Configuration(proxyBeanMethods = false)或者写在 @Component 里
@Configuration(proxyBeanMethods = false)
public class DbConfig {
    @Bean DataSource dataSource() { return new HikariDataSource(); }

    @Bean JdbcTemplate jdbcTemplate(DataSource ds) {  // 改成参数注入
        return new JdbcTemplate(ds);
    }
}

没有代理,再直接调 dataSource() 就会 new 出第二个连接池,而且它不归容器管。所以这种写法要像上面一样,用参数接依赖。

Spring Boot 自己的自动配置类都写成 proxyBeanMethods = false,省掉生成代理的开销,启动更快。

三种注入写法

送货员能把东西送到三个地方:构造器参数、setter 方法、字段。官方推荐构造器。

构造器注入推荐
private final PayClient pay;

public OrderService(PayClient pay) {
    this.pay = pay;
}
  • 字段可以是 final,造出来就是完整的
  • 依赖一目了然,写单元测试直接 new
  • 只有一个构造器时,@Autowired 可以省(Spring 4.3 起)
  • 循环依赖启动就报错,不会藏着(第 04 篇)
setter 注入可选依赖
private PayClient pay;

@Autowired
public void setPay(PayClient pay) {
    this.pay = pay;
}
  • 适合可有可无、以后可能换掉的依赖
  • 对象造出来时,依赖还没到,有一小段不完整的时间
字段注入能用,不推荐
@Autowired
private PayClient pay;
  • 写起来最短
  • 不能 final;脱离容器 new 出来,字段就是 null
  • 依赖多了也看不出来,IDEA 会提示 Field injection is not recommended

注入裁判:同一个类型有好几个,给谁?

OrderService 要一个 PayClient,容器里可能有好几个实现。勾选容器里有哪些、谁标了 @Primary,再选注入点的写法,看 Spring 一步步怎么判。

容器里登记了哪些 PayClient
@Primary 标在
注入点怎么写
    判定顺序按类型找出所有候选 → 有 @Qualifier 先过滤 → 只剩一个就是它 → 有 @Primary 选它 → 有 @Priority 选优先级最高的 → 名字和参数名、字段名一样的 → 还是选不出来就报错。@Resource 例外:先按名字找,找不到才按类型。

    不止“要一个”

    有时候要全部、有时候可有可无、有时候用到才要。换个参数类型就行。

    要全部:List、Map
    List<PayClient> all          // 按 @Order 排好
    Map<String, PayClient> byName // key 是 Bean 名

    策略模式的常见写法:byName.get(type) 按支付方式挑一个,新加实现不用改这里。

    可有可无:Optional
    Optional<PayClient> pay

    没有就是 Optional.empty(),不报错。老写法是 @Autowired(required = false),没有就是 null。

    用到再要:ObjectProvider
    ObjectProvider<PayClient> pay
    pay.getIfAvailable()
    pay.getIfUnique()
    pay.orderedStream()

    注入时不挑,调用时才去拿。注入原型 Bean、可选依赖、打破循环依赖都用它。

    延迟:@Lazy
    public OrderService(@Lazy PayClient pay)

    先注入一个代理,第一次调用方法时才去容器里拿真的。

    配置值:@Value
    @Value("${pay.timeout:3000}") int timeout

    冒号后面是默认值。一组配置多的话,用 @ConfigurationProperties 绑定到一个类上更清楚。

    别这么写:到处 getBean
    ctx.getBean(PayClient.class)

    在业务代码里自己去容器里找,依赖又藏起来了,测试也难写。只在框架级代码里用。

    JDK 8 和 JDK 17:Spring 5 和 Spring 6

    容器、图纸、判定顺序都没变。变的是几个注解的包名、参数名的来源,以及 6.2 新加的 @Fallback。

    Spring 5 · Boot 2

    JDK 8+
    • 也认 javax.annotation.Resource、javax.inject.Inject;JDK 8 自带 javax.annotation,JDK 11 起要另外加依赖
    • 没加 -parameters 编译也能读到构造器参数名(从调试信息里读),按名字兜底能用

    Spring 6 · Boot 3

    JDK 17+
    • 只认 jakarta.annotation.Resource、jakarta.inject.Inject;还写 javax 版的,不报错,也不注入,用的时候才 NPE
    • 6.1 起参数名只从 -parameters 读。没加的话,靠构造器参数名兜底的注入会变成 NoUniqueBeanDefinitionException(Boot 的插件会自动加)
    • 6.2 起有 @Fallback:标了它的 Bean 只在没有别的候选时才用,和 @Primary 正好相反
    没变的:先图纸后对象构造器注入最推荐按类型找,再 Qualifier、Primary、名字List、Map、Optional、ObjectProvider

    常踩的坑

    多数是两种情况:这个对象不是容器造的,或者容器里同类型的不止一个。

    expected single matching bean but found 2

    同一个类型有两个 Bean,又没说要哪个。在其中一个上加 @Primary,或在注入点加 @Qualifier,或者改成注入 List / Map。

    required a bean of type '…' that could not be found

    类不在扫描范围里、忘了加 @Component,或者对应的自动配置被条件挡掉了。启动加 debug=true 看条件报告(第 03 篇)。

    自己 new 出来的对象,里面的 @Autowired 字段是 null

    送货员只给容器造的对象送货。从容器里拿这个对象,或者把依赖从构造器传进去。

    static 字段上的 @Autowired 不生效

    Spring 不往静态字段里注入,只在日志里提一句 Autowired annotation is not supported on static fields。改成普通字段,或在构造器里赋给静态变量。

    参数名明明对上了,注入的却是另一个

    判定时 @Primary 排在名字匹配前面。真要那一个,用 @Qualifier 点名。

    @Bean 方法互相调用,得到了两个对象

    配置类写成了 proxyBeanMethods = false 或者 @Component,方法调用没被拦下。改成用参数注入依赖。

    单例里注入原型 Bean,每次拿到的都是同一个

    单例只造一次,原型也就只在那时注入一次。要每次都新的,注入 ObjectProvider<T>,用的时候 getObject()。

    两个构造器互相依赖,启动报循环依赖

    构造器注入下没法先造一半再说。拆出第三个类,或者在一边的参数上加 @Lazy(第 04 篇)。

    升级 Boot 3 后,@Resource 注入的字段是 null

    还在用 javax.annotation.Resource,Spring 6 不认。换成 jakarta.annotation.Resource,或者改用构造器注入。

    升级 Spring 6.1 后,原来能启动的项目报 found 2

    以前靠构造器参数名选中了某个 Bean,现在没加 -parameters,读不到参数名了。加上编译参数,或者用 @Qualifier 写明。