理解 Spring 依赖注入:从构造器注入到集合与条件 Bean

理解 Spring 依赖注入:从构造器注入到集合与条件 Bean

在 Spring 项目中,我们经常会看到下面几种依赖注入方式:

java 复制代码
@Autowired
private PaymentService paymentService;

或者:

java 复制代码
@Resource
private PaymentService paymentService;

但在很多较新的 Spring 项目里,还会出现一种看起来"没有任何注入注解"的写法:

java 复制代码
@RestController
public class PaymentController {

    private final List<PaymentHandler> handlers;
    private final Map<String, PaymentHandler> handlerMap;

    public PaymentController(
            List<PaymentHandler> handlers,
            Map<String, PaymentHandler> handlerMap) {
        this.handlers = handlers;
        this.handlerMap = handlerMap;
    }
}

这里既没有 @Autowired,也没有 @Resource,但 Spring 仍然能够把依赖传进来。

原因并不神秘:只要一个 Spring Bean 只有一个构造器,Spring 就可以直接把这个构造器当作依赖注入入口。 从 Spring 4.3 开始,单构造器场景通常不需要再显式标注 @Autowired

这也是理解 Spring 依赖注入机制的一个很好的入口。


一、先区分两个概念:注册 Bean 与注入 Bean

Spring 的依赖注入通常可以拆成两个阶段:

  1. 先把对象交给 Spring 管理;
  2. 再把这些对象注入到需要它们的其他对象中。

1. 将对象注册为 Bean

常见方式包括:

  • @Component:通用组件;
  • @Service:通常用于应用服务、领域服务等业务组件;
  • @Repository:通常用于持久化访问组件,并提供部分数据访问异常转换能力;
  • @Controller:Spring MVC 控制器;
  • @RestController:REST 接口控制器;
  • @Bean:通过配置类中的方法显式创建 Bean。

例如:

java 复制代码
public interface MessageSender {
    void send(String message);
}
java 复制代码
@Service
public class EmailMessageSender implements MessageSender {

    @Override
    public void send(String message) {
        // 发送邮件
    }
}

此时 EmailMessageSender 会进入 Spring 容器,后续其他 Bean 才有可能注入它。

2. 从容器中解析并注入依赖

当 Spring 创建某个 Bean 时,会分析它需要哪些依赖,然后从容器中寻找匹配对象。

因此,依赖注入的本质可以简单理解为:

Spring 创建对象时,自动帮你把这个对象依赖的其他对象找出来,并传进去。


二、为什么构造器没有 @Autowired 也能注入

假设有下面的控制器:

java 复制代码
@RestController
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
}

只要 OrderController 本身是 Spring Bean,并且它只有这一个构造器,Spring 在创建它时就会发现:

要创建 OrderController,必须先获得一个 OrderService

于是 Spring 会从容器中寻找能够赋值给 OrderService 的 Bean,然后调用构造器:

text 复制代码
Spring 容器
   │
   ├── 找到 OrderService Bean
   │
   └── new OrderController(orderService)

因此下面两种写法在单构造器场景中效果通常相同:

java 复制代码
public OrderController(OrderService orderService) {
    this.orderService = orderService;
}
java 复制代码
@Autowired
public OrderController(OrderService orderService) {
    this.orderService = orderService;
}

实际项目中通常更推荐第一种,因为信息更少,也更容易保持类的简洁。


三、为什么通常推荐构造器注入

Spring 支持字段注入、Setter 注入和构造器注入,但业务代码中通常更推荐构造器注入。

例如:

java 复制代码
@Service
public class CheckoutService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public CheckoutService(
            InventoryService inventoryService,
            PaymentService paymentService) {
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }
}

它有几个明显优势。

1. 依赖关系清晰

只看构造器就能知道这个类运行所需的依赖。

2. 必需依赖不能被遗漏

对象创建完成时,必需依赖已经全部提供,不需要等待字段二次注入。

3. 更适合不可变设计

依赖可以声明为 final

java 复制代码
private final PaymentService paymentService;

对象创建后依赖关系不会再被随意修改。

4. 更方便单元测试

测试时可以直接手工构造:

java 复制代码
PaymentService paymentService = new FakePaymentService();
CheckoutService service = new CheckoutService(
        new FakeInventoryService(),
        paymentService
);

不必为了测试一个普通 Java 类启动 Spring 容器。


四、一个接口有多个实现时,Spring 怎么处理

真正值得理解的是这种场景:

java 复制代码
public interface StorageService {
    void store(String data);
}

有两个实现:

java 复制代码
@Service
public class LocalStorageService implements StorageService {

    @Override
    public void store(String data) {
        // 保存到本地
    }
}
java 复制代码
@Service
public class CloudStorageService implements StorageService {

    @Override
    public void store(String data) {
        // 保存到云端
    }
}

此时容器里存在两个 StorageService

如果直接注入:

java 复制代码
public FileService(StorageService storageService) {
    this.storageService = storageService;
}

Spring 无法仅凭类型判断应该选择哪个实现,通常会出现候选 Bean 不唯一的问题。

解决这种问题有几种常见方式。


五、@Primary:提供默认实现

如果多个实现中有一个应该作为默认选择,可以使用 @Primary

java 复制代码
@Service
@Primary
public class CloudStorageService implements StorageService {

    @Override
    public void store(String data) {
        // 默认保存到云端
    }
}

此时:

java 复制代码
public FileService(StorageService storageService) {
    this.storageService = storageService;
}

Spring 会优先选择标记了 @Primary 的实现。

适合的语义是:

同一接口有多个实现,但其中一个是系统默认实现。

例如:

  • 默认支付渠道;
  • 默认对象存储;
  • 默认大模型客户端;
  • 默认消息发送器。

六、@Qualifier:明确指定 Bean

如果调用方明确知道自己需要哪个实现,通常使用 @Qualifier

java 复制代码
@Service("localStorage")
public class LocalStorageService implements StorageService {
}
java 复制代码
@Service("cloudStorage")
public class CloudStorageService implements StorageService {
}

注入时:

java 复制代码
public BackupService(
        @Qualifier("cloudStorage") StorageService storageService) {
    this.storageService = storageService;
}

可以把它理解为:

  • @Primary 表示"默认选它";
  • @Qualifier 表示"这里明确要它"。

当业务中出现多个同类型实现时,这两个注解非常常见。


七、List<T> 注入:一次拿到某个接口的全部实现

Spring 不只能注入单个 Bean,还可以注入某个类型的所有候选 Bean。

例如:

java 复制代码
@Component
public class ValidationEngine {

    private final List<ValidationRule> rules;

    public ValidationEngine(List<ValidationRule> rules) {
        this.rules = rules;
    }
}

假设系统中有:

text 复制代码
EmailValidationRule
PhoneValidationRule
RiskValidationRule

它们都实现 ValidationRule,那么 Spring 会自动组装为:

text 复制代码
List<ValidationRule>
    ├── EmailValidationRule
    ├── PhoneValidationRule
    └── RiskValidationRule

这种方式特别适合:

  • 责任链;
  • 规则引擎;
  • 多个 Handler 顺序执行;
  • 多个扩展点依次处理;
  • 插件式业务能力。

@Order 在这里有什么作用

如果集合中的实现需要固定顺序,可以使用 @Order

java 复制代码
@Component
@Order(10)
public class BasicValidationRule implements ValidationRule {
}
java 复制代码
@Component
@Order(20)
public class RiskValidationRule implements ValidationRule {
}

那么注入 List<ValidationRule> 时,Spring 可以按照相应的顺序进行排列。

需要注意:

@Order 主要用于影响有序集合、扩展点、拦截器等场景中的顺序,不应简单理解为"控制 Bean 的创建先后"。

如果确实存在 Bean 初始化依赖,应优先通过真实依赖关系或 @DependsOn 表达,而不是依赖 @Order


八、Map<String, T> 注入:Bean 名称直接变成路由键

这是 Spring 中非常实用的一种能力。

例如定义支付接口:

java 复制代码
public interface PaymentHandler {
    PaymentResult pay(PaymentCommand command);
}

实现类:

java 复制代码
@Component("wechat")
public class WechatPaymentHandler implements PaymentHandler {
}
java 复制代码
@Component("alipay")
public class AlipayPaymentHandler implements PaymentHandler {
}

然后直接注入:

java 复制代码
@Service
public class PaymentRouter {

    private final Map<String, PaymentHandler> handlers;

    public PaymentRouter(Map<String, PaymentHandler> handlers) {
        this.handlers = handlers;
    }

    public PaymentResult pay(
            String channel,
            PaymentCommand command) {

        PaymentHandler handler = handlers.get(channel);

        if (handler == null) {
            throw new IllegalArgumentException(
                    "Unsupported payment channel: " + channel
            );
        }

        return handler.pay(command);
    }
}

Spring 注入的内容可以理解为:

text 复制代码
{
    "wechat" -> WechatPaymentHandler,
    "alipay" -> AlipayPaymentHandler
}

其中:

  • Map 的 key 是 Bean 名称;
  • Map 的 value 是对应的 Bean 实例。

这样就能把大量:

text 复制代码
if channel == ...
else if channel == ...
else if channel == ...

替换为基于 Bean 注册表的路由。

这种模式非常适合:

  • 支付渠道;
  • 奖励发放策略;
  • 文件解析器;
  • 消息处理器;
  • AI 模型适配器;
  • 不同业务类型的策略实现。

本质上,它是一种非常轻量的 策略模式 + Spring 容器注册表


九、Bean 名称从哪里来

如果显式指定:

java 复制代码
@Service("wechat")
public class WechatPaymentHandler implements PaymentHandler {
}

Bean 名称就是:

text 复制代码
wechat

如果没有显式指定:

java 复制代码
@Service
public class WechatPaymentHandler implements PaymentHandler {
}

默认名称通常会根据类名生成,例如:

text 复制代码
wechatPaymentHandler

因此,如果准备使用:

java 复制代码
Map<String, PaymentHandler>

做业务路由,建议 Bean 名称本身就是稳定、明确的业务 key,而不要过度依赖默认类名转换规则。


十、可选依赖:不要把 required = false 理解成"自动防空指针"

Spring 支持可选依赖,例如:

java 复制代码
@Autowired(required = false)
private AuditClient auditClient;

它的真实含义是:

如果容器中没有可注入的 AuditClient,Spring 不因为这次依赖解析而直接让 Bean 创建失败。

但字段本身仍然可能是 null

也就是说:

java 复制代码
auditClient.record();

依然可能出现空指针。

因此,现代 Spring 代码更推荐显式表达"这个依赖可能不存在"。

方式一:Optional<T>

java 复制代码
public AuditService(Optional<AuditClient> auditClient) {
    this.auditClient = auditClient;
}

使用:

java 复制代码
auditClient.ifPresent(client -> client.record());

方式二:ObjectProvider<T>

java 复制代码
public AuditService(ObjectProvider<AuditClient> provider) {
    this.provider = provider;
}

需要时再获取:

java 复制代码
AuditClient client = provider.getIfAvailable();

ObjectProvider 还适合延迟获取、多实现遍历等场景。


十一、@Bean:不是所有对象都必须写 @Component

有些对象来自第三方库,我们无法修改源码,也就不能在它们的类上添加 @Component

这时可以通过配置类创建 Bean:

java 复制代码
@Configuration
public class ClientConfiguration {

    @Bean
    public ApiClient apiClient(ApiProperties properties) {
        return new ApiClient(
                properties.getEndpoint(),
                properties.getToken()
        );
    }
}

之后 ApiClient 和普通 Spring Bean 没有本质区别,可以继续被其他组件注入。

常见场景包括:

  • 第三方 SDK;
  • HTTP Client;
  • Redis Client;
  • 云服务客户端;
  • 自定义线程池;
  • 序列化器。

十二、@ConditionalOnMissingBean:只在用户没有提供实现时创建默认 Bean

Spring Boot 自动配置经常需要解决一个问题:

框架提供默认实现,但业务方如果自己实现了,就优先使用业务方的。

典型写法:

java 复制代码
@Configuration
public class StorageAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(StorageService.class)
    public StorageService defaultStorageService() {
        return new LocalStorageService();
    }
}

逻辑是:

text 复制代码
容器里已经有 StorageService?
        │
   ┌────┴────┐
   │         │
  有         没有
   │         │
 不创建      创建默认实现

这也是很多 Spring Boot Starter 能做到"开箱即用,同时允许覆盖默认实现"的关键机制。

适合:

  • SDK;
  • Starter;
  • 基础设施组件;
  • 默认策略实现;
  • 可插拔框架。

十三、@ConditionalOnProperty:通过配置开关决定是否创建 Bean

有些组件只应该在配置开启时存在。

例如:

yaml 复制代码
feature:
  audit:
    enabled: true

配置:

java 复制代码
@Configuration
public class AuditConfiguration {

    @Bean
    @ConditionalOnProperty(
            prefix = "feature.audit",
            name = "enabled",
            havingValue = "true"
    )
    public AuditClient auditClient() {
        return new AuditClient();
    }
}

当:

yaml 复制代码
enabled: true

时创建 Bean;关闭时则不创建。

这比在业务代码中到处写:

java 复制代码
if (auditEnabled) {
    ...
}

更适合控制一个完整组件是否进入 Spring 容器。

常见用途:

  • 是否启用某个外部服务;
  • 是否启用消息队列;
  • 是否启用 AI 能力;
  • 是否启用审计模块;
  • Starter 功能开关。

十四、自定义 Condition:把复杂的创建规则封装起来

如果仅通过配置项已经无法描述 Bean 的创建条件,可以自定义 Condition

例如:

java 复制代码
public class LinuxEnvironmentCondition implements Condition {

    @Override
    public boolean matches(
            ConditionContext context,
            AnnotatedTypeMetadata metadata) {

        String os = System.getProperty("os.name", "");
        return os.toLowerCase().contains("linux");
    }
}

使用:

java 复制代码
@Bean
@Conditional(LinuxEnvironmentCondition.class)
public NativeWorker nativeWorker() {
    return new NativeWorker();
}

这种方式适合处理:

  • 操作系统;
  • classpath 是否存在某个类;
  • 多个配置组合;
  • 特定运行环境;
  • 自定义框架能力检测。

如果只是一个简单的布尔配置开关,优先使用 @ConditionalOnProperty,通常不需要自己实现 Condition


十五、@Profile:根据运行环境启用不同 Bean

对于开发、测试、生产环境,有时需要使用不同实现。

例如:

java 复制代码
@Service
@Profile("dev")
public class MockSmsSender implements SmsSender {
}
java 复制代码
@Service
@Profile("prod")
public class RealSmsSender implements SmsSender {
}

配置:

yaml 复制代码
spring:
  profiles:
    active: dev

则开发环境只启用 MockSmsSender

典型应用包括:

  • 开发环境使用模拟支付;
  • 测试环境使用内存存储;
  • 生产环境启用真实云服务;
  • 不同部署环境使用不同实现。

需要注意:

如果某个 Bean 只在部分 Profile 下存在,那么依赖它的组件也要正确处理这种条件关系。可以通过:

  • 同样限制 Profile;
  • Optional<T>
  • ObjectProvider<T>
  • 条件配置;

来表达,而不是默认认为 required=false 就解决了所有问题。


十六、@Lazy:延迟 Bean 的创建

默认情况下,大多数单例 Bean 会在应用上下文初始化阶段创建。

使用:

java 复制代码
@Service
@Lazy
public class HeavyModelClient {
}

后,Spring 可以延迟到真正需要这个 Bean 时再初始化。

适合:

  • 初始化成本很高的对象;
  • 启动时不一定会使用的功能;
  • 某些外部 SDK;
  • 大模型客户端或重量级资源。

不过不要把 @Lazy 当作常规性能优化手段。大量延迟初始化会让错误从"启动阶段"推迟到"运行阶段",反而降低问题暴露的及时性。


十七、@Scope("prototype"):每次获取都创建新实例

Spring Bean 默认通常是单例作用域:

text 复制代码
ApplicationContext
      │
      └── 一个 Bean 实例被反复复用

如果定义:

java 复制代码
@Component
@Scope("prototype")
public class PipelineContext {
}

那么每次向容器请求这个 Bean 时,Spring 都会创建新的实例。

适合:

  • 每次任务需要独立状态;
  • 每条工作流需要独立上下文;
  • 每次构建责任链都需要新的可变对象。

但要注意一个常见问题:

如果把 prototype Bean 直接注入 singleton Bean 的构造器,通常只会在 singleton 创建时解析一次。

如果希望每次使用都获得新的 prototype 实例,可以配合 ObjectProvider

java 复制代码
public TaskRunner(ObjectProvider<PipelineContext> provider) {
    this.provider = provider;
}

然后:

java 复制代码
PipelineContext context = provider.getObject();

十八、@DependsOn:明确表达初始化先后依赖

某些 Bean 虽然没有直接的 Java 参数依赖,但初始化时确实要求另一个 Bean 已经完成创建,可以使用:

java 复制代码
@Component
@DependsOn("schemaInitializer")
public class ReportService {
}

不过这通常属于基础设施级能力。

业务代码中,如果 A 真正依赖 B,优先让依赖直接出现在构造器中:

java 复制代码
public ReportService(SchemaInitializer initializer) {
    this.initializer = initializer;
}

这种依赖关系更显式,也更容易测试。


十九、Environment 与配置属性注入

Spring 可以注入 Environment

java 复制代码
@Component
public class RuntimeInfo {

    private final Environment environment;

    public RuntimeInfo(Environment environment) {
        this.environment = environment;
    }
}

然后读取:

java 复制代码
String region = environment.getProperty("app.region");

不过,如果一组配置属于同一个业务组件,通常更推荐使用 @ConfigurationProperties

java 复制代码
@ConfigurationProperties(prefix = "storage")
public class StorageProperties {

    private String endpoint;
    private String bucket;

    // getter / setter
}

相比零散调用 Environment#getProperty(),配置对象更加:

  • 类型安全;
  • 容易测试;
  • 容易维护;
  • 适合 IDE 提示;
  • 能表达完整配置结构。

二十、@ImportResource@PropertySource:兼容旧式配置

现代 Spring Boot 项目通常主要依赖:

  • Java Configuration;
  • 自动配置;
  • application.yml / application.properties

但有些老项目或第三方组件仍然使用 XML:

java 复制代码
@SpringBootApplication
@ImportResource("classpath:legacy/beans.xml")
public class Application {
}

这种方式允许 Spring Boot 项目继续加载传统 XML Bean 配置。

@PropertySource 则可以显式引入额外的 properties 文件。

这类机制更多用于:

  • 老系统迁移;
  • 兼容旧组件;
  • 渐进式 Spring Boot 改造。

对于全新项目,一般没有必要主动回到 XML 配置方式。


二十一、@EnableScheduling@Async 不属于依赖注入,但经常一起出现

这两个注解和 Bean 管理关系密切,但严格来说,它们不是"注入方式"。

@EnableScheduling

用于开启 Spring 定时任务能力:

java 复制代码
@SpringBootApplication
@EnableScheduling
public class Application {
}

之后可以使用:

java 复制代码
@Scheduled(fixedDelay = 60_000)
public void refreshCache() {
}

@Async

用于让符合条件的方法通过异步执行器运行。

例如:

java 复制代码
@Async
public void exportReport(Long taskId) {
    // 执行耗时导出
}

实际使用时还需要关注:

  • 是否启用了异步能力;
  • 使用哪个线程池;
  • 异常如何处理;
  • Spring AOP 代理是否生效;
  • 同类内部直接调用是否绕过代理。

因此,不能简单把 @Async 理解成"加上注解就一定异步"。


二十二、Spring 注入 ListMap 时内部大致发生了什么

不需要一开始就深入 Spring 源码,可以先建立一个正确的整体模型。

假设有:

java 复制代码
public PaymentRouter(
        List<PaymentHandler> handlers,
        Map<String, PaymentHandler> handlerMap) {
    ...
}

Spring 创建 PaymentRouter 时,大致会经历:

text 复制代码
1. 读取 PaymentRouter 的 BeanDefinition
        ↓
2. 确定应该使用哪个构造器
        ↓
3. 分析构造器参数
        ↓
4. 发现需要 List<PaymentHandler>
        ↓
5. 找到所有 PaymentHandler Bean
        ↓
6. 组装成 List
        ↓
7. 发现需要 Map<String, PaymentHandler>
        ↓
8. 再按 Bean 名称 + Bean 实例组装成 Map
        ↓
9. 调用构造器创建 PaymentRouter

可以把它概括成一句话:

Spring 不只是"按类型找一个 Bean",它还会根据依赖声明的结构决定如何组织多个候选 Bean。


二十三、从源码角度看,核心职责分别在哪里

如果以后需要继续阅读 Spring 源码,可以重点关注几个职责层次。

1. BeanDefinition:描述"应该创建什么"

Spring 会把组件扫描、@Bean、XML 等配置转换为 Bean 定义信息。

它描述的不是最终对象本身,而是类似:

text 复制代码
Bean 类型是什么
Bean 名称是什么
作用域是什么
构造方式是什么
是否 Lazy
有哪些依赖信息

2. 构造器解析:决定"用哪个构造器,参数从哪里来"

Spring 创建对象时,需要判断:

  • 使用哪个构造器;
  • 构造器每个参数需要什么类型;
  • 参数是否唯一;
  • 是否需要处理集合类型。

ConstructorResolver 等组件承担了这类工作。

3. BeanFactory:从容器中解析候选对象

DefaultListableBeanFactory 是 Spring 中非常核心的 BeanFactory 实现之一。

它负责完成类似:

text 复制代码
给我一个 PaymentHandler
给我所有 PaymentHandler
给我名为 alipay 的 PaymentHandler

这类候选 Bean 解析工作。

4. AutowiredAnnotationBeanPostProcessor:处理注入元数据

它主要参与 @Autowired@Value 等注入元数据的处理,并参与构造器候选识别等过程。

需要注意的是:

"单构造器无需 @Autowired 也能注入"并不意味着 Spring 完全绕过了依赖解析机制,只是这个场景不再要求开发者显式写出 @Autowired


二十四、最值得掌握的几个组合

如果只准备记住实际项目中最常见的一部分,可以优先掌握下面几种。

1. 普通依赖:构造器注入

java 复制代码
public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

适用于绝大多数普通业务组件。

2. 多实现 + 默认实现:@Primary

java 复制代码
@Primary
@Service
public class DefaultSearchService implements SearchService {
}

适用于"多个实现,但有默认值"。

3. 多实现 + 明确指定:@Qualifier

java 复制代码
public Service(
        @Qualifier("fastModel") ModelClient client) {
}

适用于调用方明确指定实现。

4. 多实现 + 全部执行:List<T>

java 复制代码
public RuleEngine(List<Rule> rules) {
}

适用于责任链、规则集和插件集合。

5. 多实现 + 按 key 路由:Map<String, T>

java 复制代码
public ParserRouter(Map<String, Parser> parsers) {
}

适用于策略选择和业务路由。

6. 可选 Bean:Optional<T> / ObjectProvider<T>

java 复制代码
public Service(Optional<AuditClient> client) {
}

适用于功能可能关闭的情况。

7. 默认组件:@ConditionalOnMissingBean

适用于 Starter、SDK、基础设施组件。

8. 功能开关:@ConditionalOnProperty

适用于通过配置启停完整能力。


二十五、一个完整的小例子:不用 if-else 实现策略路由

假设系统支持三种文档解析方式:

java 复制代码
public interface DocumentParser {
    ParsedDocument parse(byte[] content);
}

实现:

java 复制代码
@Component("pdf")
public class PdfDocumentParser implements DocumentParser {
}
java 复制代码
@Component("markdown")
public class MarkdownDocumentParser implements DocumentParser {
}
java 复制代码
@Component("html")
public class HtmlDocumentParser implements DocumentParser {
}

路由器:

java 复制代码
@Component
public class DocumentParserRegistry {

    private final Map<String, DocumentParser> parsers;

    public DocumentParserRegistry(
            Map<String, DocumentParser> parsers) {
        this.parsers = parsers;
    }

    public DocumentParser get(String type) {
        DocumentParser parser = parsers.get(type);

        if (parser == null) {
            throw new IllegalArgumentException(
                    "Unsupported document type: " + type
            );
        }

        return parser;
    }
}

调用方只需要:

java 复制代码
DocumentParser parser = registry.get(type);
return parser.parse(content);

以后增加一种 docx

java 复制代码
@Component("docx")
public class DocxDocumentParser implements DocumentParser {
}

原来的路由代码完全不需要修改。

这正是集合注入真正有价值的地方:

把"新增策略"从修改旧代码,转变为注册一个新的实现类。

它不仅减少了 if-else,还天然更符合开闭原则。


二十六、常见误区

误区一:没有 @Autowired 就不是依赖注入

不是。

单构造器 Bean 通常可以直接进行构造器注入。

误区二:@Order 就是 Bean 的启动顺序

不准确。

@Order 更多用于排序,例如集合注入、拦截器或扩展链。初始化依赖应通过真实依赖关系、@DependsOn 等机制表达。

误区三:@Autowired(required = false) 可以避免空指针

不能。

它只能让"依赖不存在"不一定导致注入阶段失败,后续代码仍必须处理 null。更推荐 OptionalObjectProvider

误区四:所有可关闭的组件都应该用 required = false

通常不是最佳设计。

如果组件是否存在由配置决定,更适合使用:

text 复制代码
@ConditionalOnProperty
@Profile
@ConditionalOnMissingBean

从 Bean 创建阶段就表达清楚。

误区五:Map 注入就是普通 Java Map

它当然是 Map,但它最重要的价值在于:

text 复制代码
Bean 名称 -> Bean 实例

可以直接充当 Spring 管理的策略注册表。


二十七、总结

Spring 依赖注入并不只是 @Autowired@Resource 两个注解。

真正需要理解的是:Spring 容器能够根据"依赖类型 + Bean 元数据 + 注入位置"自动解析对象之间的关系。

可以建立下面这个简单模型:

text 复制代码
@Component / @Service / @Bean
            │
            ▼
       Bean 注册到容器
            │
            ▼
      Spring 创建其他 Bean
            │
            ▼
       分析构造器参数
            │
   ┌────────┼──────────┐
   ▼        ▼          ▼
 单个 T   List<T>   Map<String,T>
   │        │          │
找一个     找全部     Bean 名称 -> Bean
   └────────┴──────────┘
            │
            ▼
         调用构造器

在日常项目中,可以优先遵循下面的思路:

  1. 普通必需依赖优先使用构造器注入;
  2. 一个接口多个实现时,根据语义选择 @Primary@QualifierList<T>Map<String, T>
  3. 可选依赖优先考虑 OptionalObjectProvider
  4. 功能是否存在,尽量在 Bean 创建阶段通过 ConditionalProfile 表达;
  5. Map<String, T> 非常适合策略路由、插件注册和消除大量分支判断;
  6. 不要把 Bean 排序、Bean 创建顺序和依赖注入混为一谈。

当这些机制真正用熟以后,Spring 就不再只是一个"帮你自动 new 对象"的框架,而更像一个负责组件注册、依赖解析、策略装配和生命周期管理的运行时容器。

相关推荐
颜进强1 小时前
Claude Code - 26 效率三件套:cc-switch 切模型 · codeburn 算成本 · claude-hud 看状态
前端·后端·ai编程
LiLiYuan.1 小时前
【字符串常量池】
java·开发语言·面试
Sylvia33.1 小时前
从轮询到推送:足球数据API架构演进与火星数据技术拆解
java·服务器·网络·python·websocket·架构
cfm_29141 小时前
高并发系统缓存全解
java·缓存
掘金者阿豪1 小时前
时序数据库选型被写入峰值带偏了?金仓超表让我少加了不少班
后端
16月6日-晴2 小时前
Java面向对象进阶—static
java·开发语言
大黄评测2 小时前
jQuery 遍历方法 each 实战:循环处理列表数据
后端
大勇前进2 小时前
jQuery 事件委托原理:彻底解决动态生成元素绑定失效问题
后端