Spring 容器这块很容易被讲成一堆类名:BeanFactory、ApplicationContext、BeanDefinition、BeanFactoryPostProcessor、BeanPostProcessor、@Autowired、@Qualifier。如果只背名词,很快就散了。
我更喜欢把它拆成三个问题:容器保存了什么,启动时做了什么,注入时遇到多个候选怎么选。
先把 BeanFactory 和 ApplicationContext 分开
BeanFactory 是 Spring 容器的核心接口,最朴素的能力就是按名字或类型拿 Bean。
在它之上又叠了很多能力:
ListableBeanFactory:可以枚举 Bean。HierarchicalBeanFactory:支持父子容器。ConfigurableBeanFactory:支持配置、作用域、后处理器等扩展。AutowireCapableBeanFactory:支持自动装配。
ApplicationContext 可以理解成更完整的应用上下文。它在 BeanFactory 基础上加了事件、国际化、资源加载、环境变量、生命周期管理等能力。
日常 Spring Boot 项目里,我们面对的基本都是 ApplicationContext。但理解底层时,仍然要记住:Bean 的定义、实例化、依赖注入这些核心动作,底座还是 BeanFactory 体系。
容器里真正重要的是 BeanDefinition
容器不是一开始就直接拿着一堆对象。更准确地说,它先拿到一堆 Bean 的定义,也就是 BeanDefinition。
一个 BeanDefinition 大致描述这些信息:
- Bean 的 class 是什么。
- 作用域是 singleton 还是 prototype。
- 是否 lazy。
- 构造参数是什么。
- 属性依赖是什么。
- 初始化和销毁方法是什么。
可以把 BeanDefinition 当成对象的图纸。容器启动时,先把图纸收集好,再按规则实例化对象、填充依赖、执行初始化扩展。
refresh 是容器启动主线
AbstractApplicationContext.refresh() 是理解 Spring 容器启动流程的入口。代码很长,但主线可以记成几步:
- 准备上下文环境。
- 创建或刷新 BeanFactory。
- 准备 BeanFactory 的基础配置。
- 执行 BeanFactory 后处理器。
- 注册 Bean 后处理器。
- 初始化消息、事件、多播器等上下文能力。
- 初始化非 lazy 的单例 Bean。
- 发布启动完成事件。
里面最值得抓的是两个后处理器:
BeanFactoryPostProcessor改的是 BeanDefinition。BeanPostProcessor改的是 Bean 实例。
一个发生在实例化之前,一个发生在实例化之后。很多 Spring 扩展点都是靠这两个阶段插进去的。
BeanFactoryPostProcessor 改图纸
BeanFactoryPostProcessor 的时机比较早。它拿到的是 BeanFactory 和 BeanDefinition,还没真正创建业务对象。
典型用途包括:
- 读取配置,把占位符替换成真实值。
- 修改某些 BeanDefinition。
- 扫描并注册额外 Bean。
因为此时对象还没创建,所以它适合改“图纸”,不适合操作业务 Bean 实例。
BeanPostProcessor 改实例
BeanPostProcessor 在 Bean 实例创建后、初始化前后介入。它能拿到真实对象,所以很多增强都发生在这里。
例如:
- 初始化前后做自定义处理。
- 包一层代理对象。
- 处理某些注解。
- 接入 AOP。
如果你看到某个 Bean 注入进去后已经不是原始类,而是代理类,通常就和 BeanPostProcessor、AOP 代理创建有关。
这也是 Spring 很强的一点:业务类可以只写普通对象,容器在生命周期中给它补上依赖、代理、事务、切面和其他基础设施。
@Autowired 的默认策略
@Autowired 默认按类型注入。接口只有一个实现时,很顺。
@Autowired
private UserService userService;
如果同一个接口有多个实现,容器就不知道该选哪一个,常见异常是 NoUniqueBeanDefinitionException。
解决方式通常有几种:
- 字段名和 Bean 名一致,让它按名称兜底。
- 使用
@Qualifier("xxx")明确指定。 - 给默认实现加
@Primary。 - 使用
@Resource(name = "xxx")按名称注入。
我更推荐业务代码里不要依赖“字段名碰巧匹配”。多个实现时,直接用 @Qualifier 或策略路由会更清楚。
多实现路由不要滥用 if else
很多业务都会有“一个接口多个实现”的场景,比如支付方式、通知渠道、导出格式、物流公司、优惠策略。
如果调用方写一堆 if else:
if (type == A) {
aService.handle();
} else if (type == B) {
bService.handle();
}
后面实现一多,调用方会越来越臃肿。
更稳的方式是让每个实现暴露自己的路由键,再统一收成一张 Map:
@Component
public class HandlerFactory {
private final Map<String, Handler> handlers;
public HandlerFactory(List<Handler> handlerList) {
this.handlers = handlerList.stream()
.collect(Collectors.toMap(Handler::type, Function.identity()));
}
public Handler get(String type) {
return handlers.get(type);
}
}
这样新增实现时,只要新增一个组件,不用改调用方的分支结构。
JDK SPI 和 Spring 策略表不是一回事
JDK SPI 适合第三方扩展,例如 JDBC 驱动这种“接口在平台里,实现由外部厂商提供”的场景。它通过 META-INF/services/<接口全限定名> 在运行时发现实现。
但普通业务系统里的多实现路由,通常不需要上 SPI。你已经在 Spring 容器里了,用 @Component、@Qualifier、@Primary、List<接口>、Map<String, 接口> 更直接。
Dubbo 这类框架会在 SPI 基础上做更多扩展,例如按名称获取扩展、自适应扩展、包装类增强、依赖注入等。那是框架级扩展点,和业务代码里的策略选择不是一个层级。
注解只是标记,关键是处理它的人
注解本身不做事,它只是元数据。真正让注解生效的是编译器、运行时反射或框架处理器。
几个元注解要先知道:
@Retention决定注解保留到源码、字节码还是运行时。@Target决定注解能贴在类、方法、字段还是参数上。@Documented决定是否进入文档。@Inherited决定类级注解是否能被子类继承。
Spring 里很多注解都要求运行时可见,因为容器要在启动或实例处理阶段读取它们。你自定义注解时,如果后面要用反射读取,@Retention(RetentionPolicy.RUNTIME) 基本少不了。
小结
Spring 容器可以先按这条线理解:
BeanDefinition是对象图纸。BeanFactory是 Bean 创建和管理的底座。ApplicationContext是完整应用上下文。refresh()是启动主流程。BeanFactoryPostProcessor改图纸。BeanPostProcessor改实例。@Autowired默认按类型,多实现时要明确选择策略。- 普通业务多实现路由优先用 Spring 容器能力,不要过早上 SPI。
看懂这条线以后,AOP、事务、条件装配、自动配置就不会显得那么神秘了。它们大多是在容器生命周期的某个阶段,往 BeanDefinition 或 Bean 实例上补东西。




