← 返回上一页

第2章: IoC 容器与依赖注入

Spring 的核心双引擎 —— 容器帮你创建对象,DI 帮你组装零件

学习进度 0/7

手把手:创建可运行的 Spring 项目

⚠️ 先别急着看理论!

按照下面的步骤,先把一个能跑的项目建起来。复制粘贴就能运行!

1
打开 IntelliJ IDEA → New Project

选择 Maven,GroupId 填 com.example,ArtifactId 填 spring-ioc-demo

2
修改 pom.xml — 添加 Spring 依赖

打开项目根目录的 pom.xml,在 <dependencies> 里添加:

<dependencies> <!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.30</version> </dependency> </dependencies>
3
按照这个目录结构创建文件
spring-ioc-demo/
  src/main/java/com/example/
    AppConfig.java ← 配置类
    dao/
      UserDao.java ← 接口
      UserDaoImpl.java ← 实现类
    service/
      UserService.java ← 接口
      UserServiceImpl.java ← 实现类
    MainApp.java ← 启动入口
4
逐个复制粘贴以下代码到对应文件
AppConfig.java UserDao.java UserDaoImpl.java UserService.java UserServiceImpl.java MainApp.java
文件位置: src/main/java/com/example/AppConfig.java
// 📄 AppConfig.java — Spring 配置类(替代 XML 配置文件) package com.example; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; // @Configuration: 标记为配置类,Spring 启动时会读取它 // @ComponentScan: 告诉 Spring 去哪个包下扫描 @Component/@Service/@Repository 等注解 @Configuration @ComponentScan(basePackages = "com.example") public class AppConfig { // 不需要写任何代码! // Spring 自动扫描 com.example 包下所有带注解的类 }
文件位置: src/main/java/com/example/dao/UserDao.java
// 📄 UserDao.java — 数据访问层接口 package com.example.dao; // 定义接口: 面向接口编程,方便后续切换实现(如 MySQL → MongoDB) public interface UserDao { void save(String username); // 保存用户 }
文件位置: src/main/java/com/example/dao/UserDaoImpl.java
// 📄 UserDaoImpl.java — UserDao 的具体实现 package com.example.dao; import org.springframework.stereotype.Repository; // @Repository: 标记为数据访问层组件 // 效果等同于 @Component,但语义更明确("我是存数据的") // Spring 扫描时会自动创建这个类的实例,放入容器 @Repository public class UserDaoImpl implements UserDao { @Override public void save(String username) { // 实际项目中这里会调用 MyBatis/JPA 操作数据库 System.out.println("[DAO] 保存用户到数据库: " + username); } }
文件位置: src/main/java/com/example/service/UserService.java
// 📄 UserService.java — 业务逻辑层接口 package com.example.service; public interface UserService { void register(String username); // 注册用户 }
文件位置: src/main/java/com/example/service/UserServiceImpl.java
// 📄 UserServiceImpl.java — 业务逻辑层实现(DI 核心示例) package com.example.service; import com.example.dao.UserDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; // @Service: 标记为业务逻辑层组件,Spring 会自动创建并管理 @Service public class UserServiceImpl implements UserService { // final: 依赖不可变,一旦注入就不能更改(更安全) private final UserDao userDao; // @Autowired: 构造器注入(官方推荐!) // Spring 启动时自动找到容器中的 UserDao 实例,传入构造器 // 你没有写 new UserDaoImpl(),Spring 帮你"送货上门"! @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } @Override public void register(String username) { System.out.println("[Service] 开始注册用户: " + username); userDao.save(username); // 调用 DAO 层,此时 userDao 已被 Spring 注入 System.out.println("[Service] 注册完成!"); } }
文件位置: src/main/java/com/example/MainApp.java
// 📄 MainApp.java — 启动入口 package com.example; import com.example.service.UserService; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class MainApp { public static void main(String[] args) { // 第1步: 启动 Spring 容器 // 传入配置类 → Spring 读取 @ComponentScan → 扫描并创建所有 Bean ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); // 第2步: 从容器中取出 UserService // 注意: 没有 new!Spring 已经创建好了,并且 UserDao 也自动注入了 UserService userService = ctx.getBean(UserService.class); // 第3步: 直接使用(内部的 userDao 已经被 Spring 注入好了) userService.register("小明"); // 输出: // [Service] 开始注册用户: 小明 // [DAO] 保存用户到数据库: 小明 // [Service] 注册完成! } }
5
运行 MainApp.java 的 main 方法

右键 MainApp → Run,你会看到:

[Service] 开始注册用户: 小明 [DAO] 保存用户到数据库: 小明 [Service] 注册完成!
💡 你刚刚做了什么?

1. 没有写 new UserDaoImpl() — Spring 容器帮你创建了
2. 没有手动把 UserDao 传给 UserService@Autowired 帮你注入了
3. 这就是 IoC(控制反转)+ DI(依赖注入) 的核心!

什么是 IoC(控制反转)?

🏗️ 生活比喻:自己找工人 vs 找中介

😰 自己找工人(传统方式)
  • 需要水电工 → 自己去找
  • 需要木工 → 自己去找
  • 需要油漆工 → 自己去找
  • 累死!还得管协调!
😎 找中介(IoC 方式)
  • 告诉中介需要什么人
  • 中介帮你找好工人
  • 中介帮你安排顺序
  • 你只管验收!
Spring 容器就是这个"中介" —— 你告诉它需要哪些对象,它帮你创建、组装、管理

代码对比:一看就懂

❌ 传统:自己 new(紧耦合)
public class UserService { // 硬编码写死了用 UserDaoImpl private UserDao dao = new UserDaoImpl(); }
换成 MongoUserDao?得改代码!
✅ IoC:容器注入(松耦合)
public class UserService { // 不管具体实现,容器来决定 private UserDao dao; // 容器自动把 dao "送"进来 }
换实现?改配置就行,代码不动!
💡 IoC 核心口诀:

"不要自己 new,让容器送过来"
控制权从"你的代码" → 反转到"Spring 容器",所以叫控制反转

什么是依赖注入(DI)?

🔌 IoC 是思想,DI 是手段

IoC(控制反转)= 你不要自己 new 对象了,交给容器管
DI(依赖注入)= 容器帮你把需要的对象"送"进来的具体方式
IoC = "不要自己找" | DI = "送货上门"

三种注入方式(点击展开)

🏆 构造器注入 官方推荐 ✅
白话:造房子的时候就把水管埋好 —— 对象创建时就必须传入依赖,之后不能改。
@Service public class UserServiceImpl implements UserService { private final UserDao userDao; // final = 创建后不可变! // @Autowired 可省略(Spring 4.3+ 只有一个构造器时自动注入) // 推荐原因: final 保证依赖不为 null,且不会被意外修改 @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; // Spring 自动传入容器中的 UserDao } }
✅ 优点
  • 依赖不可变(final)
  • 保证不为 null
  • 方便写单元测试
⚠️ 小缺点
  • 依赖多时参数长
  • (提醒你类太大了!)
🔧 Setter 注入 可选依赖用
白话:房子造好后再装空调 —— 对象创建后通过 set 方法装上去,可以换。
@Service public class UserServiceImpl implements UserService { private UserDao userDao; // 没有 final,后续可以修改 // Setter 注入: Spring 先创建对象,再调用 setXxx() 方法注入 @Autowired public void setUserDao(UserDao userDao) { this.userDao = userDao; // 对象创建后通过 setter "装上去" } }
适用场景:依赖是可选的(有没有都行),或需要后期重新设置。
字段注入 不推荐 ❌
白话:最省事但最危险 —— 直接在字段上加注解,Spring 用反射强行塞进去。
@Service public class UserServiceImpl implements UserService { @Autowired // 直接加在字段上,Spring 用反射强行赋值 private UserDao userDao; // ⚠️ 不能用 final!反射无法修改 final 字段 // 看起来最省事,但测试时无法手动传入 Mock 对象 }
⚠️ 为什么不推荐?
  • 不能用 final,依赖可能被修改
  • 单元测试时没法传 Mock 对象
  • 虽然代码最短,但坏处远大于好处
对比项 🏆 构造器 🔧 Setter ⚡ 字段
支持 final
保证非 null
推荐度⭐⭐⭐⭐⭐⭐⭐⭐

@Autowired 魔法棒:多个实现怎么办?

Spring 看到 @Autowired 就去容器里找:"有没有这个类型的 Bean?有就注入!"

问题:如果有多个同类型的 Bean?

比如 UserDao 有 MysqlUserDao 和 MongoUserDao 两个实现,Spring 不知道注入哪个!

方法1: @Qualifier
指名道姓:
"我要那个叫 xxx 的"
@Autowired @Qualifier("mysqlDao") // 指定 Bean 名称 private UserDao dao; // 容器中有多个 UserDao 时,明确要哪个
方法2: @Primary
在实现类上标记:
"我是默认首选"
@Repository @Primary // 标记为首选实现 public class MysqlDao implements UserDao {} // 不加 @Qualifier 时默认用这个
方法3: @Resource
按名称注入:
"Java 标准注解"
@Resource(name="mysqlDao") private UserDao dao; // javax 标准注解,按名称查找 // 与 @Autowired 区别: 先按名称找
💡 记住口诀:

@Autowired = 按类型找 → 多个同类型?用 @Qualifier 指定名字
@Resource = 按名称找 → Java 标准注解(非 Spring 独有)

Bean 作用域:一个还是多个?

🎮 游戏比喻:共享道具 vs 每人一个

singleton(单例)= 共享一个

全服只有一把屠龙刀🗡️
不管谁来取,拿到的都是同一把
Spring 默认就是这种!

prototype(原型)= 每人一个

每次领取都给你新锻造一把🔨
每个人的都不一样
需要 @Scope("prototype")

// 默认 singleton(单例),不用额外声明 // 整个容器中只有一个实例,所有地方共享同一个对象 @Component public class UserService { } // prototype(原型): 每次 getBean() 都创建新实例 // 适合有状态的对象(如购物车,每个用户应该各自独立) @Component @Scope("prototype") public class ShoppingCart { } // 购物车应该每人一个!

互动实验室

🧪 作用域可视化模拟器

点击按钮创建 Bean,观察 singleton 和 prototype 的区别!

Singleton 区域
点击按钮获取 Bean
Prototype 区域
点击按钮获取 Bean
// 观察每次 getBean 的结果...

⚡ IoC + DI 全流程模拟

点击按钮,逐步观察 Spring 容器如何启动、创建 Bean、注入依赖

// 点击按钮按顺序执行...

📝 闯关练习

第 1 关:Spring 官方推荐的注入方式是?

第 2 关:Bean 默认作用域是什么?

第 3 关:@Autowired 默认按什么方式装配?

第 4 关:代码填空 — 补全注解配置

点击空白处选中,再点击下方选项填入

____
____(basePackages = "com.example")
public class AppConfig { }

____
public class UserServiceImpl {
    ____
    private UserDao userDao;
}
@Autowired @Service @Configuration @ComponentScan @Bean @Component

本章小结

🎯 IoC = 控制反转
不要自己 new 对象
让 Spring 容器帮你创建和管理
🔌 DI = 依赖注入
容器把对象"送"给你
首选构造器注入(final + 安全)
🔍 @Autowired 规则
按类型找 Bean
多个同类型 → @Qualifier 指定
📦 作用域
singleton(默认)= 全局一个
prototype = 每次新建一个
← 上一章 下一章 →