SPRING · IOC CONTAINER · DEPENDENCY INJECTION
你只说要什么,容器负责造好送来
控制反转(IoC):创建对象、把对象拼装起来的活,从你手里交给了容器。依赖注入(DI)是它的做法:你在构造器里写明需要什么,容器造好了送进来。
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; // 都不归它管
}
}
BeanDefinition先有图纸,再有对象。写着哪个类、单例还是多例、依赖谁。
扫描、@Bean、自动配置都产出图纸ApplicationContext按图纸造对象。里面真正干活的是 DefaultListableBeanFactory。
也就是常说的 IoC 容器singletonObjects单例造好后放这里,谁要都给同一个。
一个按名字存的 MapAutowiredAnnotationBeanPostProcessor看你要什么类型,去成品架上挑一个送来。挑不出来就报错。
@Resource 归 CommonAnnotationBeanPostProcessor 管容器里有什么
BeanFactory 是最基础的容器,ApplicationContext 在它外面包了一圈常用功能。平时说的 Spring 容器,指的都是 ApplicationContext。
BeanFactory最基础的容器
- 按图纸造对象、注入依赖
- 用到才造(getBean 时)
- BeanPostProcessor 要手动登记
ApplicationContext平时用的容器
- 启动时就把单例全造好,配置错了马上发现
- 自动登记 BeanPostProcessor、BeanFactoryPostProcessor
- 还管事件(第 08 篇)、配置、国际化、资源加载
beanDefinitionMapallBeanNamesByTypesingletonObjectsgetBean 时先看成品架,有就直接给;没有就拿图纸去造,造的过程见 第 04 篇的十四站。
图纸从哪来
写法有好几种,到了容器里都变成同一种东西:BeanDefinition。
@Component · @Service · @Repository · @Controller@Configuration + @Bean@Import · 自动配置(第 03 篇)registerBean · BeanDefinitionRegistryPostProcessor- beanClass
- com.shop.pay.AlipayClient
- scope
- singleton(默认)
- lazyInit
- false:启动时就造
- primary
- false
- 依赖
- 构造器参数:PayProperties
- init / destroy
- @PostConstruct、@PreDestroy 的方法
@Configuration 里的 @Bean 方法互相调用,会怎样
同样的代码,配置类有没有被代理,结果不一样。
@Configuration
public class DbConfig {
@Bean DataSource dataSource() { return new HikariDataSource(); }
@Bean JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // 调了一次方法
}
}
配置类被 CGLIB 代理了:dataSource() 被拦下,直接从成品架拿,还是同一个 DataSource。
@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 篇)
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 一步步怎么判。
@Qualifier 先过滤 → 只剩一个就是它 → 有 @Primary 选它 → 有 @Priority 选优先级最高的 → 名字和参数名、字段名一样的 → 还是选不出来就报错。@Resource 例外:先按名字找,找不到才按类型。不止“要一个”
有时候要全部、有时候可有可无、有时候用到才要。换个参数类型就行。
List<PayClient> all // 按 @Order 排好
Map<String, PayClient> byName // key 是 Bean 名策略模式的常见写法:byName.get(type) 按支付方式挑一个,新加实现不用改这里。
Optional<PayClient> pay没有就是 Optional.empty(),不报错。老写法是 @Autowired(required = false),没有就是 null。
ObjectProvider<PayClient> pay
pay.getIfAvailable()
pay.getIfUnique()
pay.orderedStream()注入时不挑,调用时才去拿。注入原型 Bean、可选依赖、打破循环依赖都用它。
public OrderService(@Lazy PayClient pay)先注入一个代理,第一次调用方法时才去容器里拿真的。
@Value("${pay.timeout:3000}") int timeout冒号后面是默认值。一组配置多的话,用 @ConfigurationProperties 绑定到一个类上更清楚。
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 正好相反
常踩的坑
多数是两种情况:这个对象不是容器造的,或者容器里同类型的不止一个。
同一个类型有两个 Bean,又没说要哪个。在其中一个上加 @Primary,或在注入点加 @Qualifier,或者改成注入 List / Map。
类不在扫描范围里、忘了加 @Component,或者对应的自动配置被条件挡掉了。启动加 debug=true 看条件报告(第 03 篇)。
送货员只给容器造的对象送货。从容器里拿这个对象,或者把依赖从构造器传进去。
Spring 不往静态字段里注入,只在日志里提一句 Autowired annotation is not supported on static fields。改成普通字段,或在构造器里赋给静态变量。
判定时 @Primary 排在名字匹配前面。真要那一个,用 @Qualifier 点名。
配置类写成了 proxyBeanMethods = false 或者 @Component,方法调用没被拦下。改成用参数注入依赖。
单例只造一次,原型也就只在那时注入一次。要每次都新的,注入 ObjectProvider<T>,用的时候 getObject()。
构造器注入下没法先造一半再说。拆出第三个类,或者在一边的参数上加 @Lazy(第 04 篇)。
还在用 javax.annotation.Resource,Spring 6 不认。换成 jakarta.annotation.Resource,或者改用构造器注入。
以前靠构造器参数名选中了某个 Bean,现在没加 -parameters,读不到参数名了。加上编译参数,或者用 @Qualifier 写明。