一、前言:什么是 IoC?为什么要用 Spring?

1.1 传统开发的痛点

在没有 Spring 的时代,我们创建对象需要手动 new

问题

  • 对象之间高度耦合,更换实现类需要修改代码
  • 对象的生命周期全靠程序员手动管理
  • 无法灵活配置参数,硬编码严重

1.2 IoC 思想

IoC(Inversion of Control,控制反转):对象的创建和依赖关系的维护,交给 Spring 容器来管理,而不是在代码中手动 new

  • DI(Dependency Injection,依赖注入) 是实现 IoC 的核心方式
  • 容器"主动"将依赖的 Bean 注入到需要它的地方
  • 开发者只需要声明"我需要什么",容器负责"怎么给你"

1.3 本项目的技术栈

技术 版本 作用
Spring Context 5.3.20 IoC 容器核心
Lombok 1.18.44 简化样板代码
JUnit 3.8.1 单元测试
Maven - 项目构建

二、项目整体结构

2.1 目录结构

Spring-Ioc/
├── src/main/java/org/example/
│   ├── MainApplication.java          // 启动入口
│   ├── config/                       // 配置类
│   │   ├── AppConfig.java            // 主配置类
│   │   ├── DataSourceConfig.java     // 数据源配置(@Bean)
│   │   └── AppProperties.java        // 属性注入演示
│   ├── dao/                          // 数据访问层
│   │   ├── UserDao.java              // DAO接口
│   │   ├── UserDaoMysqlImpl.java     // MySQL实现(@Primary)
│   │   ├── UserDaoOracleimpl.java    // Oracle实现
│   │   └── OldUserDao.java           // 演示@PostConstruct/@PreDestroy
│   ├── service/                      // 业务层
│   │   ├── UserService.java          // 构造器注入 + @Qualifier
│   │   ├── CollectionInjectService.java // 集合注入(List/Map)
│   │   ├── OrderService.java         // @Lazy懒加载
│   │   └── TestService.java          // @Autowired(required=false)
│   ├── factory/
│   │   └── ConnectionFactoryBean.java // FactoryBean工厂模式
│   ├── processor/
│   │   └── MyBeanPostProcessor.java  // BeanPostProcessor后置处理器
│   └── condition/
│       └── matches.java              // 条件装配Condition
├── src/main/resources/
│   └── app.properties                // 外部配置文件
└── pom.xml

2.2 核心业务场景

本项目模拟了一个简化的数据访问场景:

  • UserDao 是数据访问接口,有 MySQL 和 Oracle 两种实现
  • UserService 注入 UserDao 完成业务调用
  • 展示 Spring IoC 容器中 Bean 的注册、注入、生命周期等完整流程

三、核心知识点详解

3.1 IoC 容器的创建与启动

3.1.1 AnnotationConfigApplicationContext

Spring 提供了两种 IoC 容器实现:

  • ClassPathXmlApplicationContext:基于 XML 配置文件
  • AnnotationConfigApplicationContext:基于 Java 注解(本项目使用)

启动入口代码

// MainApplication.java
public class MainApplication {
    public static void main(String[] args) {
        // 1. 创建基于注解的IoC容器,并指定配置类
        AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        
        System.out.println("IOC容器创建成功");
        
        // 2. 从容器中获取Bean
        UserService userService = context.getBean(UserService.class);
        userService.getUserInfo();
        
        // 3. 关闭容器
        context.close();
    }
}

关键点

  • AnnotationConfigApplicationContext(AppConfig.class):传入配置类,容器会自动扫描并注册 Bean
  • getBean():按类型或名称获取 Bean
  • context.close():关闭容器,触发 Bean 的销毁生命周期

3.2 Bean 的注册方式

Spring 提供了多种注册 Bean 到容器的方式:

3.2.1 组件扫描注解(@Component / @Service / @Repository

这是最常用的方式,通过类注解标记组件,再配合 @ComponentScan 自动扫描。

主配置类

// AppConfig.java
@Configuration                    // 标记为配置类
@ComponentScan("org.example")    // 自动扫描该包及子包下的所有组件
@Import({DataSourceConfig.class, AppProperties.class})  // 导入其他配置类
public class AppConfig {
    // 配置类本身可以为空,主要作用是告诉Spring去哪里找Bean
}

各层组件注解使用

// DAO层:@Repository
@Repository
public class UserDaoMysqlImpl implements UserDao { ... }

// Service层:@Service  
@Service
public class UserService { ... }

// 通用组件:@Component
@Component
public class ConnectionFactoryBean { ... }
注解 作用含义 使用位置(场景) 补充说明
@Component 通用组件,把类交给 Spring IOC 容器管理 不属于 Controller、Service、Dao 的普通工具类、通用组件 另外三个注解本质就是@Component的衍生注解
@Service 业务逻辑层组件 Service 业务层,存放业务处理代码 语义化注解,功能等同于 @Component,只是用来标识业务层
@Repository 数据访问层组件 Mapper/DAO 层,和数据库交互 MyBatis 开发中一般不写,Mapper 使用@Mapper注解
@Controller Web 控制器 页面跳转的控制器,返回视图页面 返回页面,不返回 JSON 数据
@RestController REST 风格控制器 接口开发,前后端分离项目 @Controller + @ResponseBody组合,直接返回 JSON

注意:它们在功能上没有本质区别,只是为了代码可读性和分层规范。

3.2.2 @Bean 手动注册

当需要注册第三方类或需要复杂初始化逻辑时,使用 @Bean

// DataSourceConfig.java
public class DataSourceConfig {
    
    @Value("${db.driver}")       //@Value从配置文件里读取配置值,自动注入给类的成员变量
    private String driver;
    
    @Value("${db.url}")
    private String url;
    
    @Value("${db.username}")
    private String username;
    
    @Value("${db.password}")
    private String password;
    
    @Bean                          // 注册Bean到容器
    @Scope("singleton")            // 作用域:单例(默认)
    public MyDataSource myDataSource() {
        MyDataSource myDataSource = new MyDataSource();
        myDataSource.setDriver(driver);
        myDataSource.setUrl(url);
        myDataSource.setUsername(username);
        myDataSource.setPassword(password);
        return myDataSource;
    }
    
    public static class MyDataSource {
        private String driver;
        private String url;
        private String username;
        private String password;
        // getter/setter 省略...
        public void connect() {
            System.out.println("连接数据库: " + url);
        }
        public void close() {
            System.out.println("关闭数据库连接");
        }
    }
}

注意DataSourceConfig 没有 @Component 注解,无法被 @ComponentScan 扫描,必须通过 @Import 手动导入。

3.2.3 @Import 导入配置类
@Import({DataSourceConfig.class, AppProperties.class})

@Import 的作用:

  • 导入一个或多个配置类
  • 被导入的类不需要 @Component 注解
  • 适合导入第三方库的配置类

3.3 Bean 的作用域

通过 @Scope 注解控制 Bean 的创建策略:

作用域 说明 创建时机
singleton(默认) 单例,容器中只有一个实例 容器启动时创建
prototype 原型,每次获取都创建新实例 每次 getBean() 时创建
request Web环境,每次HTTP请求一个实例 请求时创建
session Web环境,每次会话一个实例 会话时创建
application Web环境,整个应用一个实例 应用启动时创建
@Bean
@Scope("prototype")  // 每次调用都创建新的MyDataSource实例
public MyDataSource myDataSource() {
    return new MyDataSource();
}

3.4 依赖注入(DI)

3.4.1 @Autowired 自动装配

@Autowired 按照类型自动装配,Spring 会自动查找容器中匹配的 Bean。

三种注入方式

(1)构造器注入(推荐)
// UserService.java
@Service
public class UserService {
    
    private final UserDao userDao;
    
    @Autowired  // Spring自动注入UserDao的实现类
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    public void getUserInfo() {
        System.out.println("UserService业务层开始执行");
        userDao.queryUser();
    }
}

优点

  • 依赖不可变(final 修饰)
  • 强制注入,保证依赖不为空
  • 便于单元测试

Spring 4.3+ 新特性:当类只有一个构造器时,@Autowired 可以省略。

(2)Setter 注入
@Service
public class UserService {
    
    private UserDao userDao;
    
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}
(3)字段注入
@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
}
3.4.2 @Qualifier 解决多 Bean 冲突

当存在多个同类型 Bean 实现时,使用 @Qualifier 指定注入哪个:

// UserService.java
@Service
public class UserService {
    
    private final UserDao userDao;
    
    @Autowired
    public UserService(
        @Qualifier("userDaoMysqlImpl")  // 指定注入MySQL实现
        UserDao userDao
    ) {
        this.userDao = userDao;
    }
}

Bean 的命名规则

  • 默认:类名首字母小写,如 UserDaoMysqlImpl → userDaoMysqlImpl
  • 自定义:@Repository("自定义名称")
3.4.3 @Primary 优先级标记

如果某个 Bean 是默认首选注入的,可以使用 @Primary

// UserDaoMysqlImpl.java
@Repository
@Primary  // 当没有@Qualifier时,优先注入此Bean
public class UserDaoMysqlImpl implements UserDao { ... }

@Primary 与 @Qualifier 的关系

  • @Primary:标记"默认选项",当没有显式指定时生效
  • @Qualifier:精确指定,优先级高于 @Primary
3.4.4 @Autowired(required = false) 允许为空

当依赖不是必须的时,可以设置 required = false

// TestService.java
@Service
public class TestService {
    
    @Autowired(required = false)  // 如果找不到OrderService,不会报错
    private OrderService orderService;
}
3.4.5 集合注入

Spring 支持自动将所有实现某接口的 Bean 注入到集合中:

// CollectionInjectService.java
@Service
public class CollectionInjectService {
    
    // 注入所有UserDao实现的List
    @Autowired
    private List<UserDao> userDaoList;
    
    // 注入所有UserDao实现的Map(key为Bean名称,value为Bean实例)
    @Autowired
    private Map<String, UserDao> userDaoMap;
    
    public void printBeans() {
        System.out.println("=====集合注入List=====");
        userDaoList.forEach(UserDao::queryUser);
        
        System.out.println("=====集合注入Map=====");
        userDaoMap.forEach((key, value) -> {
            System.out.println(key + " -> " + value);
            value.queryUser();
        });
    }
}

运行结果

=====集合注入List=====
查询数据库用户信息          // UserDaoMysqlImpl
查询Oracle数据库用户信息    // UserDaoOracleimpl
=====集合注入Map=====
userDaoMysqlImpl -> com.example.dao.UserDaoMysqlImpl@xxxx
userDaoOracleimpl -> com.example.dao.UserDaoOracleimpl@xxxx

3.5 Bean 的生命周期

Spring Bean 的生命周期分为三个阶段:创建 → 初始化 → 销毁

3.5.1 完整生命周期流程
① 实例化Bean(调用构造方法)
    ↓
② 属性赋值(依赖注入 @Autowired)
    ↓
③ Aware回调(如BeanNameAware、BeanFactoryAware)
    ↓
④ BeanPostProcessor前置处理(postProcessBeforeInitialization)
    ↓
⑤ @PostConstruct 注解方法执行
    ↓
⑥ InitializingBean.afterPropertiesSet() 方法执行
    ↓
⑦ 自定义初始化方法(如 @Bean(initMethod = "init"))
    ↓
⑧ BeanPostProcessor后置处理(postProcessAfterInitialization)
    ↓
⑨ Bean就绪,可以使用
    ↓ →(容器关闭时)
⑩ @PreDestroy 注解方法执行
    ↓
⑪ DisposableBean.destroy() 方法执行
    ↓
⑫ 自定义销毁方法(如 @Bean(destroyMethod = "destroy"))
3.5.2 代码演示

UserDaoMysqlImpl 完整演示了生命周期的关键节点:

// UserDaoMysqlImpl.java
@Repository
@Primary
public class UserDaoMysqlImpl 
    implements UserDao, 
               InitializingBean,    // 方式1:实现接口
               DisposableBean {     // 方式1:实现接口
    
    @Override
    public void queryUser() {
        System.out.println("查询数据库用户信息");
    }
    
    // 初始化 - 方式1:实现InitializingBean接口
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("【接口】InitializingBean.afterPropertiesSet() - 初始化完成");
    }
    
    // 销毁 - 方式1:实现DisposableBean接口
    @Override
    public void destroy() throws Exception {
        System.out.println("【接口】DisposableBean.destroy() - 销毁完成");
    }
    
    // 初始化 - 方式2:使用@PostConstruct注解(JSR-250标准)
    @PostConstruct
    public void annoInit() {
        System.out.println("【注解】@PostConstruct - 初始化");
    }
    
    // 销毁 - 方式2:使用@PreDestroy注解(JSR-250标准)
    @PreDestroy
    public void annoDestroy() {
        System.out.println("【注解】@PreDestroy - 销毁");
    }
}

执行顺序

【注解】@PostConstruct - 初始化          ← 先执行!
【接口】InitializingBean.afterPropertiesSet() - 初始化完成

【注解】@PreDestroy - 销毁              ← 先执行!
【接口】DisposableBean.destroy() - 销毁完成
3.5.3 三种初始化/销毁方式对比
方式 初始化注解/接口 销毁注解/接口 说明
JSR-250注解 @PostConstruct @PreDestroy 推荐,与Spring解耦
Spring接口 InitializingBean DisposableBean 与Spring耦合
@Bean属性 @Bean(initMethod) @Bean(destroyMethod) 自定义方法名

最佳实践:优先使用 @PostConstruct / @PreDestroy,不与Spring API耦合。


3.6 延迟加载 @Lazy

默认情况下,单例 Bean 在容器启动时就会创建。使用 @Lazy 可以延迟到第一次使用时才创建:

// OrderService.java
@Service
@Lazy  // 懒加载
public class OrderService {
    
    public OrderService() {
        System.out.println("OrderService构造函数被调用");
    }
}

对比

// 无@Lazy:容器启动时立即创建
// 有@Lazy:直到调用getBean(OrderService.class)时才创建

OrderService orderService = context.getBean(OrderService.class);
// 此时才打印:OrderService构造函数被调用

@Lazy 的使用场景

  • Bean 创建开销大,不想在容器启动时创建
  • 解决循环依赖问题
  • 非核心 Bean,希望按需加载

3.7 FactoryBean 工厂模式

FactoryBean<T> 是 Spring 提供的工厂 Bean 接口,用于创建复杂对象。

3.7.1 FactoryBean 与 BeanFactory 的区别
概念 说明
BeanFactory Spring IoC 容器的顶层接口,负责管理 Bean 的整个生命周期
FactoryBean<T> 一个特殊的 Bean,用于创建其他 Bean,返回的是 getObject() 的结果
3.7.2 FactoryBean 使用示例
// ConnectionFactoryBean.java
@Component
public class ConnectionFactoryBean 
    implements FactoryBean<ConnectionFactoryBean.MyConnection> {
    
    // 返回实际要注册到容器的Bean
    @Override
    public MyConnection getObject() throws Exception {
        return new MyConnection();  // 自定义创建逻辑
    }
    
    // 返回Bean的类型
    @Override
    public Class<?> getObjectType() {
        return MyConnection.class;
    }
    
    // 是否单例
    @Override
    public boolean isSingleton() {
        return true;  // 默认就是true
    }
    
    public static class MyConnection {
        public void connect() {
            System.out.println("连接数据库");
        }
    }
}

使用

// 当注入ConnectionFactoryBean时,实际注入的是getObject()返回的MyConnection
@Autowired
private ConnectionFactoryBean.MyConnection myConnection;  // 得到MyConnection实例

注意:如果需要获取 FactoryBean 本身,而不是其产生的对象,可以使用 & 前缀:

ConnectionFactoryBean factoryBean = context.getBean("&connectionFactoryBean");

3.8 BeanPostProcessor 后置处理器

BeanPostProcessor 是 Spring 容器的扩展点,允许在 Bean 初始化前后进行自定义处理。

3.8.1 使用场景
  • Bean 的全局初始化/销毁逻辑
  • AOP 的底层实现
  • 日志记录、权限校验等横切关注点
  • 对特定类型的 Bean 进行增强
3.8.2 代码实现
// MyBeanPostProcessor.java
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
    
    // Bean初始化**之前**执行
    @Override
    public Object postProcessBeforeInitialization(
            Object bean,        // 当前Bean实例
            String beanName     // 当前Bean的名称
    ) throws BeansException {
        System.out.println("【Before】后置处理器初始化前:" + beanName);
        // 可以返回原始bean,也可以返回包装后的bean
        return bean;
    }
    
    // Bean初始化**之后**执行
    @Override
    public Object postProcessAfterInitialization(
            Object bean,
            String beanName
    ) throws BeansException {
        System.out.println("【After】后置处理器初始化后:" + beanName);
        return bean;
    }
}
3.8.3 执行时机
Bean实例化
    ↓
属性注入
    ↓
postProcessBeforeInitialization()  ← 这里执行
    ↓
@PostConstruct / afterPropertiesSet()
    ↓
postProcessAfterInitialization()   ← 这里执行
    ↓
Bean就绪

注意BeanPostProcessor 会对所有 Bean 生效,不是只对某一个 Bean。


3.9 属性注入与 SpEL 表达式

3.9.1 @PropertySource 加载配置文件
// AppProperties.java
@PropertySource("classpath:app.properties")  // 加载外部配置
@Configuration
public class AppProperties {
    
    @Value("${app.project.name}")   // 注入配置文件中的值
    private String projectName;
    
    // getter...
}
3.9.2 app.properties 配置文件
# 数据库连接配置
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/spring_ioc?useSSL=false&serverTimezone=UTC
db.username=root
db.password=123456

# 应用配置
app.project.name=Spring Ioc
3.9.3 @Value 的三种用法
// 1. 注入配置文件值
@Value("${app.project.name}")
private String projectName;

// 2. SpEL 表达式 - 计算
@Value("#{10 * 2}")
private Integer num;  // 结果:20

// 3. SpEL 表达式 - 调用其他Bean的方法
@Value("#{userDaoMysqlImpl.toString()}")
private String userDaoMysqlImplStr;

语法对比

  • ${...}:属性占位符,获取配置文件中的值
  • #{...}:SpEL 表达式,支持复杂计算和 Bean 引用

3.10 条件装配 @Conditional

通过实现 Condition 接口,根据条件决定 Bean 是否注册到容器。

3.10.1 实现 Condition 接口
// matches.java
public class matches implements Condition {
    
    @Override
    public boolean matches(
            ConditionContext context,          // 条件上下文
            AnnotatedTypeMetadata metadata     // 注解元数据
    ) {
        // 获取配置文件中的db.type属性,默认值为"mysql"
        String env = context.getEnvironment().getProperty("db.type", "mysql");
        // 只有当db.type=mysql时,条件成立
        return "mysql".equals(env);
    }
}
3.10.2 使用条件注解
@Service
@Conditional(matches.class)  // 只有matches()返回true时才注册
public class SomeService { ... }

Spring Boot 中的自动配置就是基于此机制实现的,例如:

  • 当 classpath 中有 MySQL 驱动时,自动配置 MySQL 数据源
  • 当 classpath 中有 Redis 客户端时,自动配置 RedisTemplate

四、完整运行流程

4.1 启动 MainApplication

public class MainApplication {
    public static void main(String[] args) {
        // 1. 创建容器
        AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        
        // 2. 获取并使用Bean
        UserService userService = context.getBean(UserService.class);
        userService.getUserInfo();
        
        // 3. 使用@Bean注册的数据源
        DataSourceConfig.MyDataSource ds = 
            context.getBean(DataSourceConfig.MyDataSource.class);
        ds.connect();
        ds.close();
        
        // 4. 集合注入演示
        CollectionInjectService collector = 
            context.getBean(CollectionInjectService.class);
        collector.printBeans();
        
        // 5. 懒加载演示(第一次使用时才创建)
        OrderService orderService = context.getBean(OrderService.class);
        System.out.println(orderService);
        
        // 6. 关闭容器
        context.close();
    }
}

4.2 预期输出顺序

IOC容器创建成功

=== BeanPostProcessor ===
【Before】后置处理器初始化前:userDaoMysqlImpl
【注解】@PostConstruct - 初始化
【接口】InitializingBean.afterPropertiesSet() - 初始化完成
【After】后置处理器初始化后:userDaoMysqlImpl

=== UserService 业务调用 ===
UserService业务层开始执行
查询数据库用户信息

=== 数据源使用 ===
数据源bean: MyDataSource{driver='com.mysql.cj.jdbc.Driver', url='jdbc:mysql://localhost:3306/spring_ioc...'}
连接数据库: jdbc:mysql://localhost:3306/spring_ioc...
关闭数据库连接

=== 集合注入 ===
=====集合注入List=====
查询数据库用户信息
查询Oracle数据库用户信息
=====集合注入Map=====
userDaoMysqlImpl -> com.example.dao.UserDaoMysqlImpl@xxxx
userDaoOracleimpl -> com.example.dao.UserDaoOracleimpl@xxxx

=== 懒加载 ===
OrderService构造函数被调用
com.example.service.OrderService@xxxx

=== 容器关闭 ===
【注解】@PreDestroy - 销毁
【接口】DisposableBean.destroy() - 销毁完成


五、知识点总结

5.1 IoC 核心流程一句话总结

开发者告诉 Spring "我需要什么"(通过注解声明),Spring 自动完成 "创建 → 注入 → 管理生命周期" 的全过程。

5.2 各知识点关联图

┌─────────────────────────────────────────────────────────────┐
│                    Spring IoC 容器                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  【Bean注册】                                                │
│  ├── @Component / @Service / @Repository  (组件扫描)         │
│  ├── @Bean + @Scope                        (手动注册+作用域)  │
│  └── @Import                               (导入配置类)      │
│                                                             │
│  【依赖注入】                                                │
│  ├── @Autowired + @Qualifier               (按类型+名称)     │
│  ├── @Primary                              (优先级标记)      │
│  ├── @Autowired(required=false)            (允许为空)        │
│  └── List/Map 集合注入                     (批量收集)        │
│                                                             │
│  【生命周期】                                                │
│  ├── 实例化 → 属性注入 → 初始化 → 使用 → 销毁                 │
│  ├── @PostConstruct / @PreDestroy          (注解方式)        │
│  └── InitializingBean / DisposableBean     (接口方式)        │
│                                                             │
│  【扩展机制】                                                │
│  ├── @Lazy                                 (延迟加载)        │
│  ├── FactoryBean<T>                        (工厂模式)        │
│  ├── BeanPostProcessor                     (后置处理器)      │
│  └── @Conditional + Condition              (条件装配)        │
│                                                             │
│  【配置管理】                                                │
│  ├── @PropertySource + @Value              (属性注入)        │
│  └── SpEL表达式 #{...}                      (动态计算)       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.3 面试高频考点

问题 核心答案
Bean 的生命周期? 实例化→属性注入→Aware→BPP前置→@PostConstruct→InitializingBean→BPP后置→销毁
@Autowired 和 @Resource 的区别? @Autowired是Spring的,按类型注入;@Resource是JSR-250的,默认按名称注入
BeanFactory 和 FactoryBean 的区别? BeanFactory是容器接口;FactoryBean是创建Bean的工厂接口
单例 Bean 的线程安全问题? Spring中单例Bean默认是线程安全的(无状态时);有状态时需手动处理
循环依赖怎么解决? Spring通过三级缓存提前暴露引用,解决构造器注入外的循环依赖
@Lazy 的作用? 延迟加载,解决循环依赖,减少启动时间

六、结语

Spring IoC 是整个 Spring 框架的基石,理解了它,才能进一步学习 Spring AOP、Spring Boot、Spring Cloud 等高级特性。

建议学习路径:

  1. 动手实践:复制本项目代码,修改配置,观察运行结果
  2. 调试跟踪:打断点观察 Bean 的创建和注入过程
  3. 源码阅读:从 AnnotationConfigApplicationContext 开始,逐步深入理解容器的底层实现

"IoC 不是技术,而是一种设计思想——控制权从程序代码转移到了容器。"

更多推荐