Spring 框架入门:深入理解 IoC 容器与依赖注入

一、Spring 框架概述:为什么它成为 Java 开发的事实标准?

Spring 框架自 2003 年首次发布以来,已经成为 Java 企业级开发的基石。它以 "轻量级"、"非侵入式" 的设计理念,解决了传统 Java 开发中组件耦合紧密、配置繁琐等问题,极大地提高了开发效率和系统可维护性。

Spring 的核心价值体现在以下几个方面:

  • 控制反转(IoC):将对象的创建和依赖管理交给容器,降低组件间耦合
  • 面向切面编程(AOP):分离业务逻辑和横切关注点(如日志、事务)
  • 一站式解决方案:提供数据访问、Web 开发、安全等全方位功能
  • 生态系统丰富:与 Spring Boot、Spring Cloud 等项目无缝衔接
  • 开放性:可以与其他框架(如 MyBatis、Hibernate)轻松集成

本文将从 Spring 最核心的 IoC 容器和依赖注入讲起,帮助你打下坚实的 Spring 基础。

二、控制反转(IoC):Spring 的灵魂所在

2.1 什么是控制反转?

控制反转(Inversion of Control)是一种设计思想,它颠覆了传统的程序设计模式。在传统开发中,对象的创建和依赖关系的维护完全由开发者在代码中控制;而在 IoC 模式下,这些责任被转移到了容器(在 Spring 中就是 IoC 容器)。

传统方式

java

运行

// 手动创建对象
UserService userService = new UserServiceImpl();
// 手动设置依赖
userService.setUserDao(new UserDaoImpl());

IoC 方式

java

运行

// 从容器获取对象,无需关心创建过程和依赖
UserService userService = context.getBean(UserService.class);

这种 "控制权" 的转移带来了诸多好处:

  • 降低组件间的耦合度
  • 提高代码的可测试性
  • 简化对象的创建和管理
  • 便于实现组件的复用和替换

2.2 IoC 容器的工作原理

Spring 的 IoC 容器负责对象的创建、配置和管理,其核心实现是ApplicationContext。容器的工作流程可分为三个阶段:

  1. 初始化阶段

    • 加载配置元数据(XML、注解或 Java 配置)
    • 解析配置信息,生成 Bean 定义
    • 注册 Bean 定义到容器中
  2. Bean 创建阶段

    • 根据 Bean 定义创建对象(实例化)
    • 为对象设置属性(依赖注入)
    • 调用初始化方法
  3. 使用与销毁阶段

    • 应用程序从容器获取 Bean 并使用
    • 容器关闭时,调用 Bean 的销毁方法

2.3 核心容器接口

Spring 提供了多个容器接口,形成了层次化的容器体系:

  • BeanFactory:最基础的容器接口,提供基本的 Bean 管理功能
  • ApplicationContext:继承自 BeanFactory,提供更丰富的功能(事件发布、国际化等)
  • ConfigurableApplicationContext:提供容器配置和生命周期管理的方法

常用的 ApplicationContext 实现类:

  • ClassPathXmlApplicationContext:从类路径加载 XML 配置
  • FileSystemXmlApplicationContext:从文件系统加载 XML 配置
  • AnnotationConfigApplicationContext:基于注解的配置
  • WebApplicationContext:专门用于 Web 应用的容器

三、依赖注入(DI):实现松耦合的关键

依赖注入(Dependency Injection)是 IoC 思想的具体实现方式,指容器在创建对象时自动将其依赖的其他对象注入进来。

3.1 依赖注入的三种方式

3.1.1 构造器注入

通过构造方法传递依赖,确保对象在创建时就处于可用状态。

Service 类

java

运行

public class UserService {
    private UserDao userDao;
    
    // 构造器注入
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    // 业务方法
    public void addUser() {
        userDao.insert();
    }
}

XML 配置

xml

<bean id="userDao" class="com.example.dao.UserDaoImpl"/>

<bean id="userService" class="com.example.service.UserService">
    <!-- 构造器参数注入 -->
    <constructor-arg ref="userDao"/>
</bean>

注解配置

java

运行

@Repository
public class UserDaoImpl implements UserDao { ... }

@Service
public class UserService {
    private final UserDao userDao;
    
    // 构造器注入(Spring 4.3+可省略@Autowired)
    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}
3.1.2 Setter 方法注入

通过 Setter 方法设置依赖,灵活性更高,允许在对象创建后修改依赖。

Service 类

java

运行

public class UserService {
    private UserDao userDao;
    
    // Setter方法
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    
    // 业务方法
    public void addUser() {
        userDao.insert();
    }
}

XML 配置

xml

<bean id="userDao" class="com.example.dao.UserDaoImpl"/>

<bean id="userService" class="com.example.service.UserService">
    <!-- Setter注入 -->
    <property name="userDao" ref="userDao"/>
</bean>

注解配置

java

运行

@Service
public class UserService {
    private UserDao userDao;
    
    // Setter注入
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}
3.1.3 字段注入

直接在字段上使用注解注入,代码最简洁,但测试时难以替换依赖。

java

运行

@Service
public class UserService {
    // 字段注入
    @Autowired
    private UserDao userDao;
    
    public void addUser() {
        userDao.insert();
    }
}

3.2 三种注入方式的对比与选择

注入方式优点缺点适用场景
构造器注入确保依赖不可变、对象创建即完整多个依赖时构造器参数过长必需依赖、核心依赖
Setter 注入灵活性高、支持可选依赖依赖可被多次修改、对象可能处于不完整状态可选依赖、需要动态修改的依赖
字段注入代码简洁、开发效率高耦合框架、测试困难快速开发、简单场景

最佳实践

  • 优先使用构造器注入核心依赖
  • 对于可选依赖使用 Setter 注入
  • 谨慎使用字段注入,尤其是在需要频繁测试的场景

四、Bean 的生命周期:从创建到销毁

理解 Bean 的生命周期有助于更好地控制对象的创建和资源管理。Spring 中 Bean 的完整生命周期包括以下阶段:

  1. 实例化(Instantiation):创建 Bean 的实例
  2. 属性赋值(Populate):为 Bean 的属性设置值
  3. 初始化(Initialization)
    • 调用BeanNameAwaresetBeanName()
    • 调用BeanFactoryAwaresetBeanFactory()
    • 调用ApplicationContextAwaresetApplicationContext()
    • 调用BeanPostProcessorpostProcessBeforeInitialization()
    • 调用InitializingBeanafterPropertiesSet()
    • 调用自定义的初始化方法(init-method
    • 调用BeanPostProcessorpostProcessAfterInitialization()
  4. 使用(In Use):Bean 可以被应用程序使用
  5. 销毁(Destruction)
    • 调用DisposableBeandestroy()
    • 调用自定义的销毁方法(destroy-method

4.1 自定义初始化和销毁方法

XML 配置方式

xml

<bean id="userService" class="com.example.service.UserService"
      init-method="init" destroy-method="cleanup">
    <property name="userDao" ref="userDao"/>
</bean>

注解方式

java

运行

@Service
public class UserService {
    private UserDao userDao;
    
    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    // 自定义初始化方法
    @PostConstruct
    public void init() {
        System.out.println("UserService初始化...");
    }
    
    // 自定义销毁方法
    @PreDestroy
    public void cleanup() {
        System.out.println("UserService销毁...");
    }
}

4.2 Bean 的作用域

Spring 定义了多种 Bean 的作用域,控制 Bean 的创建方式和生命周期:

  • singleton(默认):整个应用中只有一个 Bean 实例
  • prototype:每次请求都创建新的 Bean 实例
  • request:每个 HTTP 请求创建一个实例(Web 环境)
  • session:每个会话创建一个实例(Web 环境)
  • application:整个 Web 应用共享一个实例(Web 环境)
  • websocket:每个 WebSocket 会话创建一个实例

配置方式

xml

<!-- XML配置 -->
<bean id="userService" class="com.example.service.UserService" scope="prototype"/>

java

运行

// 注解配置
@Service
@Scope("prototype")
public class UserService { ... }

五、Spring 配置方式:从 XML 到注解再到 JavaConfig

Spring 支持多种配置方式,从早期的 XML 配置到现在主流的注解和 Java 配置,体现了 "约定优于配置" 的发展趋势。

5.1 XML 配置

XML 配置是 Spring 最早支持的配置方式,适合复杂的配置场景。

xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定义Bean -->
    <bean id="userDao" class="com.example.dao.UserDaoImpl"/>
    
    <bean id="userService" class="com.example.service.UserService">
        <property name="userDao" ref="userDao"/>
    </bean>
    
    <!-- 引入其他配置文件 -->
    <import resource="dataSource.xml"/>
</beans>

5.2 注解配置

注解配置简化了配置工作,将配置信息与代码结合,提高开发效率。

常用注解

  • @Component:通用组件注解
  • @Service:服务层组件
  • @Repository:数据访问层组件
  • @Controller:Web 层组件
  • @Autowired:自动注入依赖
  • @Qualifier:指定注入 Bean 的名称
  • @Value:注入基本类型和字符串

配置类

java

运行

// 开启组件扫描
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // 手动定义Bean(当无法使用注解时)
    @Bean
    public UserDao userDao() {
        return new UserDaoImpl();
    }
}

组件类

java

运行

@Repository
public class UserDaoImpl implements UserDao { ... }

@Service
public class UserService {
    @Autowired
    private UserDao userDao;
    // ...
}

5.3 JavaConfig 配置

JavaConfig 是基于 Java 类的配置方式,类型安全且易于重构,是 Spring Boot 推荐的配置方式。

java

运行

@Configuration
public class AppConfig {
    // 数据源配置
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }
    
    // JdbcTemplate配置
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

5.4 混合配置策略

在实际项目中,通常会根据场景选择合适的配置方式:

  1. 核心业务组件:使用注解配置(@Service、@Repository 等)
  2. 基础设施配置:使用 JavaConfig(数据源、事务管理器等)
  3. 复杂的第三方组件:可保留 XML 配置,通过 @ImportResource 引入

java

运行

@Configuration
@ComponentScan("com.example")
@ImportResource("classpath:legacy-config.xml") // 引入XML配置
public class MainConfig {
    // 新配置...
}

六、实战案例:构建一个简单的 Spring 应用

下面通过一个完整的案例,展示如何使用 Spring 框架构建一个简单的用户管理应用。

6.1 项目结构

plaintext

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── App.java           // 应用入口
│   │           ├── config/
│   │           │   └── AppConfig.java // 配置类
│   │           ├── dao/
│   │           │   ├── UserDao.java   // 数据访问接口
│   │           │   └── UserDaoImpl.java // 数据访问实现
│   │           ├── pojo/
│   │           │   └── User.java      // 实体类
│   │           └── service/
│   │               ├── UserService.java // 服务接口
│   │               └── UserServiceImpl.java // 服务实现
│   └── resources/
└── test/

6.2 代码实现

实体类

java

运行

package com.example.pojo;

public class User {
    private Integer id;
    private String username;
    private String email;
    
    // 构造器、Getter和Setter
    public User() {}
    
    public User(Integer id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }
    
    // Getter和Setter方法省略
}

数据访问接口

java

运行

package com.example.dao;

import com.example.pojo.User;
import java.util.List;

public interface UserDao {
    void addUser(User user);
    User getUserById(Integer id);
    List<User> getAllUsers();
}

数据访问实现

java

运行

package com.example.dao;

import com.example.pojo.User;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class UserDaoImpl implements UserDao {
    // 模拟数据库
    private static final Map<Integer, User> USER_MAP = new HashMap<>();
    private static int nextId = 1;
    
    @Override
    public void addUser(User user) {
        if (user.getId() == null) {
            user.setId(nextId++);
        }
        USER_MAP.put(user.getId(), user);
    }
    
    @Override
    public User getUserById(Integer id) {
        return USER_MAP.get(id);
    }
    
    @Override
    public List<User> getAllUsers() {
        return new ArrayList<>(USER_MAP.values());
    }
}

服务接口

java

运行

package com.example.service;

import com.example.pojo.User;
import java.util.List;

public interface UserService {
    void registerUser(User user);
    User getUserById(Integer id);
    List<User> getAllUsers();
}

服务实现

java

运行

package com.example.service;

import com.example.dao.UserDao;
import com.example.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    public void registerUser(User user) {
        // 简单的业务逻辑
        if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        userDao.addUser(user);
    }
    
    @Override
    public User getUserById(Integer id) {
        return userDao.getUserById(id);
    }
    
    @Override
    public List<User> getAllUsers() {
        return userDao.getAllUsers();
    }
}

配置类

java

运行

package com.example.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // 组件扫描会自动发现并注册Bean
}

应用入口

java

运行

package com.example;

import com.example.config.AppConfig;
import com.example.pojo.User;
import com.example.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
    public static void main(String[] args) {
        // 初始化Spring容器
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        
        // 获取服务Bean
        UserService userService = context.getBean(UserService.class);
        
        // 测试功能
        User user1 = new User(null, "张三", "zhangsan@example.com");
        User user2 = new User(null, "李四", "lisi@example.com");
        
        userService.registerUser(user1);
        userService.registerUser(user2);
        
        System.out.println("所有用户:");
        userService.getAllUsers().forEach(user -> 
            System.out.println(user.getId() + ": " + user.getUsername() + " - " + user.getEmail()));
        
        User user = userService.getUserById(1);
        System.out.println("ID为1的用户:" + user.getUsername());
    }
}

6.3 运行结果

plaintext

所有用户:
1: 张三 - zhangsan@example.com
2: 李四 - lisi@example.com
ID为1的用户:张三

七、常见问题与解决方案

  1. Bean 注入失败(NoSuchBeanDefinitionException)

    • 检查类是否添加了 @Component、@Service 等注解
    • 确认 @ComponentScan 扫描的包路径是否正确
    • 检查依赖是否存在循环依赖问题
  2. 循环依赖问题

    • 尽量避免循环依赖设计
    • 使用 @Lazy 注解延迟加载其中一个依赖
    • 将构造器注入改为 Setter 注入
  3. Bean 的作用域使用不当

    • 理解 singleton 和 prototype 的区别
    • 在 Web 环境正确使用 request/session 作用域
    • 避免在 singleton Bean 中依赖 prototype Bean(会导致只创建一次)
  4. @Autowired 注入多个候选 Bean

    • 使用 @Qualifier 指定 Bean 名称
    • 使用 @Primary 指定首选 Bean
    • 确保 Bean 名称唯一

总结

本文深入讲解了 Spring 框架的核心概念 ——IoC 容器和依赖注入,从理论到实践,全面介绍了 Spring 的工作原理和使用方法。通过理解控制反转思想,掌握依赖注入的三种方式,以及 Bean 的生命周期和配置方式,你已经具备了使用 Spring 框架开发应用的基础能力。

Spring 的 IoC 容器通过接管对象的创建和依赖管理,极大地降低了组件间的耦合度,使代码更加灵活、可维护和可测试。在实际开发中,应根据项目需求选择合适的配置方式和注入策略,遵循 Spring 的设计原则,充分发挥其优势。

下一篇博客将介绍 Spring 的另一个核心特性 —— 面向切面编程(AOP),它与 IoC 一起构成了 Spring 框架的基石,为企业级应用开发提供了强大的支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值