Spring框架入门:深入理解Spring DI的注入方式

引言

在现代 Java 企业级开发中,Spring 框架 几乎已成为事实上的标准。其核心思想之一------控制反转(Inversion of Control, IoC) ,通过依赖注入(Dependency Injection, DI) 的方式得以实现。DI 不仅降低了组件之间的耦合度,还极大地提升了代码的可测试性、可维护性和可扩展性。

然而,对于初学者而言,Spring 提供了多种 DI 注入方式(如构造器注入、Setter 注入、字段注入等),每种方式都有其适用场景、优缺点和最佳实践。若选择不当,可能导致代码难以维护、测试困难,甚至引发运行时错误。

本文将系统性地讲解 Spring DI 的各种注入方式,涵盖以下内容:

  1. 什么是依赖注入?为什么需要它?
  2. Spring 支持的三种主要注入方式详解
    • 构造器注入(Constructor Injection)
    • Setter 方法注入(Setter Injection)
    • 字段注入(Field Injection)
  3. 基于注解与 XML 配置的实现对比
  4. @Autowired、@Resource、@Inject 的区别
  5. 循环依赖问题及其解决方案
  6. Spring 官方推荐的最佳实践
  7. 实战案例分析
  8. Spring Boot 中的 DI 使用差异

无论你是刚接触 Spring 的新手,还是希望深入理解 DI 机制的中级开发者,本文都将为你提供清晰、全面且实用的指导。


第一章:依赖注入(DI)的基本概念

1.1 什么是依赖注入?

依赖注入(Dependency Injection) 是控制反转(IoC)的一种实现方式。它的核心思想是:对象的依赖关系不由对象自身创建或查找,而是由外部容器(如 Spring)在运行时动态注入

传统方式(高耦合):
复制代码
public class OrderService {
    private PaymentService paymentService = new AlipayPaymentService(); // 紧耦合
}
使用 DI(低耦合):
复制代码
public class OrderService {
    private PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService; // 由外部传入
    }
}

此时,OrderService 不再关心 PaymentService 的具体实现,只需定义接口契约。Spring 容器负责在运行时提供具体的实现类(如 AlipayPaymentServiceWechatPaymentService)。

1.2 为什么需要 DI?

  • 解耦:组件之间不再硬编码依赖,便于替换实现。
  • 可测试性:单元测试时可轻松注入 Mock 对象。
  • 配置灵活性:通过配置文件或注解即可切换不同实现,无需修改代码。
  • 生命周期管理:Spring 统一管理 Bean 的创建、初始化和销毁。

第二章:Spring 支持的三种主要注入方式

Spring 支持三种主流的依赖注入方式:构造器注入Setter 注入字段注入。它们在语义、安全性、可测试性等方面存在显著差异。


2.1 构造器注入(Constructor Injection)

2.1.1 定义

通过类的构造函数传递依赖项。Spring 在创建 Bean 时调用该构造函数完成注入。

2.1.2 注解实现(推荐)
复制代码
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final InventoryService inventoryService;

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

    // 业务方法...
}

注意 :从 Spring 4.3 开始,若类只有一个构造函数,@Autowired 可省略。

2.1.3 XML 实现
复制代码
<bean id="orderService" class="com.example.OrderService">
    <constructor-arg ref="paymentService"/>
    <constructor-arg ref="inventoryService"/>
</bean>
2.1.4 优点
  • 强制依赖 :确保所有必需依赖在对象创建时即被注入,避免 NullPointerException
  • 不可变性 :依赖项可声明为 final,保证线程安全。
  • 易于测试 :直接通过 new OrderService(mockPayment, mockInventory) 即可构造对象。
  • 符合单一职责原则:若构造函数参数过多(>3),说明类职责过重,需重构。
2.1.5 缺点
  • 对于可选依赖,需提供多个构造函数(不推荐)。
  • 在存在循环依赖时可能报错(见第五章)。
2.1.6 Spring 官方态度

Spring 官方文档明确推荐:优先使用构造器注入

原因:它能保证组件在完全初始化后才被使用。


2.2 Setter 方法注入(Setter Injection)

2.2.1 定义

通过公共的 setter 方法注入依赖。Spring 在 Bean 实例化后调用这些方法。

2.2.2 注解实现
复制代码
@Service
public class OrderService {

    private PaymentService paymentService;
    private NotificationService notificationService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @Autowired
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
}
2.2.3 XML 实现
复制代码
<bean id="orderService" class="com.example.OrderService">
    <property name="paymentService" ref="paymentService"/>
    <property name="notificationService" ref="notificationService"/>
</bean>
2.2.4 优点
  • 支持可选依赖:某些依赖可在运行时动态设置或更改。
  • 适用于循环依赖:Spring 能通过"提前暴露引用"解决部分循环依赖问题。
  • 兼容旧代码:许多遗留系统采用此方式。
2.2.5 缺点
  • 对象可能处于不完整状态:若 setter 未被调用,依赖为 null。
  • 无法保证不可变性 :依赖项不能设为 final
  • 测试稍复杂:需先构造对象,再逐个调用 setter。
2.2.6 适用场景
  • 可选依赖(如日志服务、监控插件)。
  • 需要在运行时重新配置依赖的场景(罕见)。

2.3 字段注入(Field Injection)

2.3.1 定义

直接在字段上使用 @Autowired 注解,由 Spring 反射注入。

2.3.2 实现方式
复制代码
@Service
public class OrderService {

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private InventoryService inventoryService;
}

这是目前最常见但也最具争议的方式

2.3.3 优点
  • 代码简洁:无需构造函数或 setter,减少样板代码。
  • 开发速度快:IDE 自动生成字段注入非常方便。
2.3.4 缺点(严重!)
  • 违反封装原则:私有字段被外部强制修改。
  • 无法用于 final 字段:失去不可变性保障。
  • 单元测试困难:必须依赖 Spring 容器或使用反射才能注入 Mock。
  • 隐藏依赖关系:从类签名无法看出其依赖项,降低可读性。
  • 无法在非 Spring 环境中使用:如普通 Java 应用、静态工具类。
2.3.5 官方与社区态度
  • Spring 官方不推荐字段注入,仅作为"便捷选项"。
  • Google、阿里巴巴等大厂编码规范明确禁止字段注入
  • 《Effective Java》作者 Joshua Bloch 强烈反对通过反射破坏封装

建议:仅在简单 Demo 或临时调试时使用,生产代码应避免。


第三章:基于注解 vs XML 配置的 DI 实现

3.1 注解驱动(Annotation-based)

优势:
  • 代码与配置一体化,直观。
  • 减少 XML 文件数量,适合微服务架构。
  • 支持 @ComponentScan 自动发现 Bean。
示例:
复制代码
@Configuration
@ComponentScan("com.example")
public class AppConfig {}

3.2 XML 配置(XML-based)

优势:
  • 配置集中管理,无需修改 Java 代码即可切换实现。
  • 适合大型团队协作(开发与运维分离)。
  • 更强的表达能力(如复杂的集合注入、SpEL 表达式)。
示例:
复制代码
<beans>
    <context:component-scan base-package="com.example"/>
    
    <bean id="paymentService" class="com.example.AlipayPaymentService"/>
    
    <bean id="orderService" class="com.example.OrderService">
        <constructor-arg ref="paymentService"/>
    </bean>
</beans>

3.3 混合使用

Spring 允许注解与 XML 混合使用,例如:

  • 核心服务用 XML 配置;
  • 业务模块用注解自动扫描。

建议统一风格,避免维护混乱。


第四章:@Autowired、@Resource、@Inject 的区别

Spring 支持多种注入注解,它们来源不同,行为略有差异。

注解 来源 匹配策略 是否 Spring 特有
@Autowired Spring By Type (类型匹配),可用 @Qualifier 指定名称
@Resource JSR-250(Java 标准) By Name(默认按字段名),找不到则 By Type
@Inject JSR-330(Java 标准) By Type ,需配合 @Named 指定名称

4.1 @Autowired(最常用)

复制代码
@Autowired
private PaymentService paymentService; // 按类型查找

@Autowired
@Qualifier("alipayPaymentService")
private PaymentService paymentService; // 指定 Bean 名称

4.2 @Resource(Java 标准)

复制代码
@Resource
private PaymentService paymentService; // 默认查找名为 "paymentService" 的 Bean

@Resource(name = "wechatPaymentService")
private PaymentService wechatPay;

4.3 @Inject(JSR-330)

需引入 javax.inject 依赖:

复制代码
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

@Inject
@Named("alipayPaymentService")
private PaymentService paymentService;

4.4 如何选择?

  • Spring 项目 :优先使用 @Autowired(功能最全,支持 required=false)。
  • 跨框架兼容 :使用 @Inject(如同时使用 Guice)。
  • 习惯按名称注入 :可使用 @Resource

⚠️ 注意:@Autowired 默认要求依赖必须存在,否则启动失败。可通过 @Autowired(required = false) 关闭。


第五章:循环依赖问题及其解决方案

5.1 什么是循环依赖?

两个或多个 Bean 相互依赖,形成闭环:

复制代码
@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

5.2 Spring 如何处理?

Spring 通过三级缓存 机制解决单例 Bean 的 setter 循环依赖

  1. 一级缓存(singletonObjects):存放完全初始化的 Bean。
  2. 二级缓存(earlySingletonObjects):存放早期暴露的 Bean(尚未完成属性注入)。
  3. 三级缓存(singletonFactories):存放 ObjectFactory,用于生成代理对象。
流程简述:
  • 创建 A 时,发现依赖 B;
  • 暂存 A 的 ObjectFactory 到三级缓存;
  • 转去创建 B;
  • B 发现依赖 A,从三级缓存获取 A 的早期引用;
  • B 初始化完成,放入一级缓存;
  • 回到 A,注入 B,完成初始化。

结论setter 注入 + 单例 可解决循环依赖。

5.3 构造器注入的循环依赖

复制代码
public class A {
    public A(B b) { ... }
}

public class B {
    public B(A a) { ... }
}

Spring 无法解决!因为构造器要求所有依赖必须先存在,形成死锁。

报错

复制代码
BeanCurrentlyInCreationException: Circular reference involving...

5.4 解决方案

  1. 重构代码:打破循环,引入中介者模式或事件驱动。

  2. 改用 setter 注入(不推荐,掩盖设计问题)。

  3. 使用 @Lazy 延迟加载

    @Service
    public class A {
    public A(@Lazy B b) {
    this.b = b;
    }
    }

最佳实践:循环依赖通常是设计缺陷的信号,应优先考虑重构。


第六章:Spring 官方推荐的最佳实践

根据 Spring 官方文档 和社区共识:

✅ 推荐做法:

  1. 优先使用构造器注入:用于必需依赖。
  2. 使用 setter 注入:用于可选依赖。
  3. 避免字段注入:尤其在生产代码中。
  4. 依赖项声明为 final:保证不可变性。
  5. 限制构造函数参数数量(≤3):超过则考虑聚合或拆分类。
  6. 使用接口编程:依赖抽象而非具体实现。

❌ 反模式:

  • 在 singleton Bean 中保存可变状态。
  • 过度使用 @Autowired(required = false)
  • 忽视循环依赖的设计问题。

第七章:实战案例

案例:订单处理系统

需求:
  • 订单服务依赖支付、库存、通知三个服务。
  • 支付和库存为必需依赖,通知为可选。
正确实现(构造器 + setter):
复制代码
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private NotificationService notificationService; // 可选

    // 必需依赖通过构造器注入
    public OrderService(PaymentService paymentService, 
                        InventoryService inventoryService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }

    // 可选依赖通过 setter 注入
    @Autowired(required = false)
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void placeOrder(Order order) {
        paymentService.charge(order.getAmount());
        inventoryService.reduceStock(order.getItems());
        if (notificationService != null) {
            notificationService.sendConfirmation(order.getUserId());
        }
    }
}

✅ 优点:

  • 必需依赖不可为空;
  • 可选依赖灵活配置;
  • 易于单元测试;
  • 符合 Spring 最佳实践。

第八章:Spring Boot 中的 DI 差异

Spring Boot 延续了 Spring 的 DI 机制,但在使用上有以下特点:

8.1 自动配置简化注入

  • @SpringBootApplication 自动启用组件扫描。
  • Starter 依赖自动注册常用 Bean(如 DataSourceRestTemplate)。

8.2 构造器注入成为主流

  • Spring Boot 官方示例大量使用构造器注入。

  • Lombok 的 @RequiredArgsConstructor 进一步简化代码:

    @Service
    @RequiredArgsConstructor
    public class OrderService {
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    }

8.3 测试支持更完善

  • @SpringBootTest 支持完整上下文注入。

  • @MockBean 可轻松替换依赖:

    @SpringBootTest
    class OrderServiceTest {

    复制代码
      @MockBean
      private PaymentService paymentService;
    
      @Autowired
      private OrderService orderService;
    
      @Test
      void testPlaceOrder() {
          when(paymentService.charge(any())).thenReturn(true);
          orderService.placeOrder(...);
          verify(paymentService).charge(...);
      }

    }


结语

依赖注入是 Spring 框架的基石,而选择合适的注入方式则是编写高质量代码的关键。构造器注入应作为首选,它不仅符合面向对象设计原则,还能有效提升代码的健壮性和可测试性。字段注入虽便捷,但代价是牺牲了代码的清晰性与可维护性。

记住:好的设计不是让代码跑起来,而是让代码在未来依然容易理解和修改

希望本文能帮助你深入理解 Spring DI 的本质,并在实际项目中做出更明智的技术决策。

参考资料

  1. Spring Framework 官方文档:Dependency Injection
  2. 《Spring 实战(第6版)》
  3. Spring GitHub Issues & Discussions
  4. Google Java Style Guide
  5. Alibaba Java Coding Guidelines

相关推荐
避避风港31 分钟前
转发与重定向
java·servlet
毕设源码-钟学长38 分钟前
【开题答辩全过程】以 基于springboot和协同过滤算法的线上点餐系统为例,包含答辩的问题和答案
java·spring boot·后端
计算机毕设小月哥1 小时前
【Hadoop+Spark+python毕设】中风患者数据可视化分析系统、计算机毕业设计、包括数据爬取、Spark、数据分析、数据可视化、Hadoop
后端·python·mysql
q***44151 小时前
Spring Security 新版本配置
java·后端·spring
计算机毕设匠心工作室1 小时前
【python大数据毕设实战】强迫症特征与影响因素数据分析系统、Hadoop、计算机毕业设计、包括数据爬取、数据分析、数据可视化、机器学习、实战教学
后端·python·mysql
o***74171 小时前
Springboot中SLF4J详解
java·spring boot·后端
孤独斗士1 小时前
maven的pom文件总结
java·开发语言
雨中散步撒哈拉2 小时前
18、做中学 | 初升高 | 考场一 | 面向过程-家庭收支记账软件
开发语言·后端·golang
CoderYanger2 小时前
递归、搜索与回溯-记忆化搜索:38.最长递增子序列
java·算法·leetcode·1024程序员节