0%

spring依赖注入

Spring 依赖注入(DI)详解:从原理到实践

依赖注入(Dependency Injection,简称 DI)是 Spring 框架的核心特性之一,是控制反转(IoC)思想的具体实现。它通过容器自动管理对象之间的依赖关系,替代了传统代码中手动创建依赖对象的方式,大幅降低了代码耦合度。本文从 “DI 核心概念” 到 “两种注入方式实战”,系统解析 Spring DI 的工作原理与最佳实践。

DI 与 IoC:理解核心概念

1. 传统开发的痛点:主动依赖导致高耦合

在没有 DI 的时代,对象需要主动创建其依赖的对象,导致代码耦合严重,难以维护和测试:

1
2
3
4
5
6
7
8
9
10
public class Person {
// 主动创建依赖对象,耦合度高
private Car car = new Car();
private House house = new House();

// 业务逻辑依赖于具体的 Car 和 House 实现
public void goOut() {
car.drive(); // 直接依赖 Car 实例
}
}

问题:若 Car 构造器变化(如新增参数),Person 类必须修改;更换 Car 实现(如从 Benz 改为 BMW),需修改 Person 代码。

2. 控制反转(IoC):反转依赖的创建权

IoC 核心思想:将对象的创建和依赖管理交给容器,对象只需 “被动接收” 依赖,无需主动创建。

  • 传统方式:对象 → 主动创建依赖(正转控制);
  • IoC 方式:容器 → 主动注入依赖给对象(反转控制)。

Spring 中,IoC 容器(如 BeanFactoryApplicationContext)是实现者,负责:

  • 管理对象的生命周期(创建、初始化、销毁);
  • 解析对象之间的依赖关系;
  • 在对象需要时自动注入依赖。

3. 依赖注入(DI):IoC 的具体实现

DI 是 IoC 的一种具体表现形式:容器在实例化对象时,自动将其依赖的对象注入进来,对象无需关心依赖的来源和创建过程。

Spring 支持两种主流 IoC 实现方式:

  • 依赖注入(DI):容器主动将依赖注入对象(Spring 采用的方式);
  • 依赖查找(DL):对象主动从容器中查找依赖(如 JNDI lookup,Spring 也支持但不推荐)。

DI 的优势

  • 降低耦合:对象与依赖解耦,依赖变更不影响对象本身;
  • 提高可测试性:依赖可轻松替换为 mock 对象;
  • 减少模板代码:无需手动创建和管理依赖。

Spring DI 的三种注入方式

Spring 支持三种核心依赖注入方式,适用于不同场景:

注入方式 实现方式 适用场景
1. 构造器注入 通过构造器参数注入依赖 强制依赖(对象创建必须有该依赖)
2. Setter 注入 通过 setter 方法注入依赖 可选依赖(对象创建后可动态修改的依赖)
3. 属性注入 通过字段反射直接注入(注解方式) 简单场景,快速开发(如 Spring Boot)

注意:早期 Spring 还支持 “接口注入”(通过实现特定接口注入依赖),但因侵入性强已被淘汰,现代 Spring 开发中无需关注。

1. Setter 方法注入:灵活的可选依赖

Setter 注入是最常用的方式,通过对象的 setter 方法注入依赖,适用于可选依赖(对象可在创建后动态设置或修改依赖)。

(1)注入简单类型(String、基本类型)
1
2
3
4
5
6
<!-- 配置 HelloWorld Bean,通过 setter 注入 name 属性 -->
<bean id="helloWorld" class="com.zhanghe.study.spring4.beans.helloworld.HelloWorld">
<!-- name:对应 setName() 方法(setter 方法名去掉 set 后首字母小写) -->
<!-- value:注入简单类型值(String、int、double 等) -->
<property name="name" value="Spring Hello"/>
</bean>

对应的 Java 类(必须提供 setter 方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloWorld {
private String name;

// 无参构造器(Setter 注入依赖)
public HelloWorld() {}

// 必须提供 setter 方法(Spring 会调用此方法注入值)
public void setName(String name) {
this.name = name;
}

public void sayHello() {
System.out.println("Hello, " + name);
}
}
(2)注入引用类型(其他 Bean)

注入依赖的对象(如 Person 依赖 Car),使用 ref 属性引用目标 Bean 的 id

1
2
3
4
5
6
7
8
9
10
11
<!-- 1. 配置被依赖的 Car Bean -->
<bean id="car" class="com.zhanghe.study.spring4.beans.beantest.Car">
<property name="brand" value="法拉利"/>
<property name="price" value="2000000"/>
</bean>

<!-- 2. 配置 Person Bean,通过 setter 注入 Car 依赖 -->
<bean id="person" class="com.zhanghe.study.spring4.beans.beantest.Person">
<property name="name" value="张三"/> <!-- 简单类型 -->
<property name="car" ref="car"/> <!-- 引用类型:ref 指向 Car 的 id -->
</bean>

对应的 Java 类:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
private String name;
private Car car; // 引用类型依赖

// 无参构造器(必须)
public Person() {}

// 提供 setter 方法
public void setName(String name) { this.name = name; }
public void setCar(Car car) { this.car = car; }
}
(3)Setter 注入的特点
  • 灵活性:对象创建后可通过 setter 方法动态修改依赖;
  • 可选性:依赖可缺省(不配置则为 null);
  • 依赖顺序:若依赖 A 必须在依赖 B 之后注入,需手动控制 setter 调用顺序(或通过 depends-on 辅助)。

2. 构造器注入:强制的必要依赖

构造器注入通过有参构造器注入依赖,适用于强制依赖(对象必须依赖该对象才能创建,如数据库连接池依赖 URL、用户名、密码)。

(1)基本使用:按参数顺序注入
1
2
3
4
5
6
<!-- 配置 Car Bean,通过构造器注入 brand 和 price -->
<bean id="car" class="com.zhanghe.study.spring4.beans.beantest.Car">
<!-- constructor-arg:构造器参数,按顺序匹配 -->
<constructor-arg value="法拉利"/> <!-- 第一个参数:brand(String) -->
<constructor-arg value="2000000"/> <!-- 第二个参数:price(double) -->
</bean>

对应的 Car 类(必须提供有参构造器):

1
2
3
4
5
6
7
8
9
10
public class Car {
private String brand;
private double price;

// 有参构造器(构造器注入依赖)
public Car(String brand, double price) {
this.brand = brand;
this.price = price;
}
}
(2)解决构造器重载的歧义:indextype

当类存在重载的构造器(参数数量相同但类型不同)时,需通过 index(参数位置)和 type(参数类型)精确匹配:

场景:Car 有两个重载构造器
1
2
3
4
5
6
7
8
9
10
11
// 构造器 1:品牌 + 价格(double)
public Car(String brand, double price) {
this.brand = brand;
this.price = price;
}

// 构造器 2:品牌 + 速度(int)
public Car(String brand, int speed) {
this.brand = brand;
this.speed = speed;
}
配置:通过 type 区分构造器
1
2
3
4
5
6
7
8
9
10
11
<!-- 1. 匹配 Car(String brand, double price) -->
<bean id="car1" class="com.zhanghe.study.spring4.beans.beantest.Car">
<constructor-arg value="法拉利" index="0"/> <!-- index=0:第一个参数 -->
<constructor-arg value="2000000.0" type="double"/> <!-- type=double:匹配价格构造器 -->
</bean>

<!-- 2. 匹配 Car(String brand, int speed) -->
<bean id="car2" class="com.zhanghe.study.spring4.beans.beantest.Car">
<constructor-arg value="玛莎拉蒂" index="0"/>
<constructor-arg value="250" type="int"/> <!-- type=int:匹配速度构造器 -->
</bean>
  • index:参数位置(从 0 开始),解决参数顺序问题;
  • type:参数类型的全类名(如 intjava.lang.String),解决重载歧义。
(3)构造器注入的特点
  • 强制性:依赖必须在对象创建时注入,确保对象实例化后即可使用(无 null 风险);

  • 不可变性:依赖可声明为final(构造器中赋值后不可修改),增强线程安全性;

1
2
3
4
5
6
7
8
9
public class Car {
private final String brand; // final 确保依赖不可变
private final double price;

public Car(String brand, double price) {
this.brand = brand;
this.price = price;
}
}
  • 依赖顺序:构造器参数的顺序即依赖注入的顺序,无需额外配置。

3. 属性注入:注解驱动的简洁方式

属性注入通过字段反射直接注入依赖(无需 setter 或构造器),是注解开发中最简洁的方式(如 Spring Boot 常用),通过 @Autowired 注解实现。

基本使用:@Autowired 字段注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service // 标记为 Spring 组件
public class UserService {
// 直接在字段上标注 @Autowired,Spring 会通过反射注入依赖
@Autowired
private UserDao userDao; // 无需 setter 或构造器

public User getUser(Long id) {
return userDao.findById(id); // 直接使用依赖
}
}

// UserDao 组件(被 Spring 管理)
@Repository
public class UserDaoImpl implements UserDao {
@Override
public User findById(Long id) {
// 实现逻辑
}
}

配置要求:需开启组件扫描(@ComponentScan<context:component-scan>),让 Spring 发现并管理 UserServiceUserDao

属性注入的特点
  • 简洁性:代码最少,无需编写 setter 或构造器;
  • 侵入性:依赖 Spring 注解(@Autowired),脱离 Spring 容器难以测试;
  • 局限性:无法注入 final 字段(final 字段必须在构造器中初始化)。

三种注入方式的对比与最佳实践

对比维度 构造器注入 Setter 注入 属性注入
依赖强制性 强制(必须注入,否则实例化失败) 可选(可缺省为 null) 可选(默认必须存在,可配置 required=false
代码侵入性 无(不依赖 Spring 注解) 无(不依赖 Spring 注解) 有(依赖 @Autowired 等注解)
不可变性支持 支持(可注入 final 字段) 不支持(setter 无法修改 final 不支持(final 字段必须构造器注入)
依赖顺序控制 天然支持(参数顺序即注入顺序) 需手动控制(或 depends-on 不支持(注入顺序不确定)
测试友好性 友好(可直接通过构造器传入 mock) 较友好(可通过 setter 传入 mock) 不友好(需反射注入 mock)
适用场景 核心依赖、强制依赖 可选依赖、动态修改的依赖 快速开发、非核心依赖

最佳实践建议

  1. 优先使用构造器注入
    • 确保依赖不可变(final 字段);
    • 明确对象创建的必要条件;
    • 便于单元测试(直接通过构造器传入 mock 对象)。
  2. Setter 注入适用于可选依赖
    • 如 “主题切换”“动态配置更新” 等场景;
    • 允许对象创建后修改依赖。
  3. 属性注入谨慎使用
    • 仅在简单场景或 Spring Boot 快速开发中使用;
    • 避免在核心业务逻辑中使用(不利于测试和维护)。
  4. 混合使用原则
    • 核心依赖(如数据库连接)用构造器注入;
    • 可选依赖(如缓存组件)用 Setter 注入。

总结:DI 如何改变代码设计

Spring 依赖注入通过容器管理依赖关系,彻底改变了传统代码中 “对象主动创建依赖” 的模式,带来三大核心价值:

  1. 解耦:对象与依赖的具体实现分离,更换依赖只需修改配置,无需改动代码;
  2. 可维护性:依赖关系集中在配置中(XML 或注解),一目了然;
  3. 可测试性:依赖可轻松替换为 mock 对象,便于单元测试

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10