Spring 容器和依赖注入怎么理解:refresh、BeanPostProcessor 和多实现路由

1次阅读
没有评论

Spring 容器这块很容易被讲成一堆类名:BeanFactoryApplicationContextBeanDefinitionBeanFactoryPostProcessorBeanPostProcessor@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 容器启动流程的入口。代码很长,但主线可以记成几步:

  1. 准备上下文环境。
  2. 创建或刷新 BeanFactory。
  3. 准备 BeanFactory 的基础配置。
  4. 执行 BeanFactory 后处理器。
  5. 注册 Bean 后处理器。
  6. 初始化消息、事件、多播器等上下文能力。
  7. 初始化非 lazy 的单例 Bean。
  8. 发布启动完成事件。

里面最值得抓的是两个后处理器:

  • 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@PrimaryList<接口>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 实例上补东西。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-06-20发表,共计3225字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)