乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

IoC 与 DI 核心概念

本章定位:深入理解 Spring 容器的两大基石——IoC(控制反转)与 DI(依赖注入)。掌握 BeanFactory 与 ApplicationContext 的区别,以及三种注入方式的优劣与适用场景。


IoC:控制反转

IoC(Inversion of Control,控制反转) 是一种设计原则,指将对象的创建和依赖关系的管理从程序内部转移到外部容器。

在传统编程中,对象自己负责创建它所依赖的其他对象:

// 传统方式:对象自己控制依赖
public class StudentService {
    private StudentDao studentDao = new StudentDao();  // 自己 new
}

在 IoC 模式下,对象不再自己创建依赖,而是由外部容器负责创建并注入:

// IoC 方式:容器控制依赖
public class StudentService {
    private final StudentDao studentDao;

    public StudentService(StudentDao studentDao) {  // 容器传入
        this.studentDao = studentDao;
    }
}

关键转变:控制权从"对象自身"反转给了"外部容器"。


DI:依赖注入

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

Spring IoC 容器的两个核心接口:

接口定位特点
BeanFactory基础 IoC 容器延迟初始化(lazy-init),资源占用少,功能精简
ApplicationContext高级 IoC 容器继承自 BeanFactory,提供 AOP 集成、消息资源、事件传播、应用层上下文

实际开发中几乎总是使用 ApplicationContext,它是 Spring 推荐的入口点。


BeanFactory vs ApplicationContext

BeanFactory:最简容器

// 使用 BeanFactory 手动加载 XML 配置
BeanFactory factory = new XmlBeanFactory(
    new ClassPathResource("application-context.xml")
);
StudentService service = factory.getBean(StudentService.class);

BeanFactory 采用延迟初始化策略:只有在调用 getBean() 时,才会实例化对应的 Bean。这种方式资源占用少,但功能有限。

ApplicationContext:企业级容器

// 使用 ApplicationContext 加载注解配置
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
StudentService service = context.getBean(StudentService.class);

ApplicationContext 在容器启动时就会预实例化所有单例 Bean(除非标记 @Lazy),并提供以下扩展能力:

  • 国际化支持:MessageSource 接口
  • 事件传播:ApplicationEvent 发布/监听机制
  • 资源加载:统一访问 URL、文件、classpath 资源
  • 自动后处理:自动检测并注册 BeanPostProcessor 和 BeanFactoryPostProcessor

三种依赖注入方式

Spring 支持三种注入方式:

注入方式实现机制优点缺点
构造器注入(Constructor Injection)通过构造方法参数注入依赖不可变、必填依赖保证完整、利于单元测试参数过多时构造方法臃肿
Setter 注入(Setter Injection)通过 setter 方法注入可选依赖灵活、可在创建后重新注入对象可能处于不完整状态
字段注入(Field Injection)@Autowired 直接标注字段代码简洁无法声明 final、不利于测试、隐藏依赖关系

Spring 官方推荐构造器注入。从 Spring 4.x 开始,如果类只有一个构造方法,可以省略 @Autowired 注解。


完整示例:飞翔科技公司的学生管理系统

场景简述

飞翔科技技术部后端开发小崔正在实现"学生管理系统"的核心服务层。架构师白歌要求团队统一使用构造器注入,并对比三种注入方式的差异。

操作前:无 IoC 的紧耦合代码

// 操作前:StudentService.java —— 手动创建依赖,无法替换实现
public class StudentService {
    private StudentDao studentDao = new StudentDao();      // 死耦合
    private EmailSender emailSender = new EmailSender();   // 死耦合
    private SmsSender smsSender = new SmsSender();       // 死耦合

    public void enrollStudent(Student student) {
        studentDao.save(student);
        emailSender.send("admin@learnto.cn", "新学生:" + student.getName());
        smsSender.send(student.getPhone(), "入学成功");
    }
}

问题:

  1. 单元测试时无法 mock StudentDao,必须连接真实数据库
  2. 如果想把 EmailSender 换成 WechatSender,必须修改 StudentService 源码
  3. 如果 SmsSender 初始化很慢(需要连接短信网关),StudentService 的创建也会被拖慢

操作后:三种注入方式对比

方式一:构造器注入(推荐)

@Service
public class StudentService {
    private final StudentDao studentDao;
    private final EmailSender emailSender;
    private final SmsSender smsSender;

    // Spring 4.3+ 单构造器可省略 @Autowired
    public StudentService(StudentDao studentDao,
                          EmailSender emailSender,
                          SmsSender smsSender) {
        this.studentDao = studentDao;
        this.emailSender = emailSender;
        this.smsSender = smsSender;
    }

    @Transactional
    public void enrollStudent(Student student) {
        studentDao.save(student);
        emailSender.send("admin@learnto.cn", "新学生:" + student.getName());
        smsSender.send(student.getPhone(), "入学成功");
    }
}

构造器注入的优势:

  • final 字段保证依赖不可变
  • 对象创建时所有依赖必须齐全,不会出现"半初始化"状态
  • 单元测试可以直接 new StudentService(mockDao, mockEmail, mockSms)

方式二:Setter 注入

@Service
public class StudentService {
    private StudentDao studentDao;
    private EmailSender emailSender;
    private SmsSender smsSender;

    @Autowired
    public void setStudentDao(StudentDao studentDao) {
        this.studentDao = studentDao;
    }

    @Autowired(required = false)  // 可选依赖
    public void setEmailSender(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    @Autowired(required = false)  // 可选依赖
    public void setSmsSender(SmsSender smsSender) {
        this.smsSender = smsSender;
    }
}

Setter 注入的适用场景:

  • 依赖是可选的(如 emailSender 可能未配置)
  • 需要在对象创建后动态替换依赖
  • 循环依赖场景下(虽然应优先重构避免循环依赖)

方式三:字段注入(不推荐)

@Service
public class StudentService {
    @Autowired
    private StudentDao studentDao;      // 无法声明 final

    @Autowired
    private EmailSender emailSender;

    @Autowired
    private SmsSender smsSender;
}

字段注入的问题:

  • 无法声明 final,依赖可能被意外修改
  • 单元测试需要依赖 Spring 容器或反射工具(如 ReflectionTestUtils)
  • 隐藏了类的真实依赖关系,阅读代码时无法从构造器签名看出依赖

容器启动与运行结果

// 启动容器
@Configuration
@ComponentScan("com.feixiang.student")
public class AppConfig {
}

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        StudentService service = ctx.getBean(StudentService.class);

        Student student = new Student("小崔", 22, "计算机科学与技术");
        service.enrollStudent(student);
    }
}
运行输出:
[main] INFO  o.s.c.a.AnnotationConfigApplicationContext - 
    Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d06d69c
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - 
    Creating shared instance of singleton bean 'studentService'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - 
    Autowiring by type from bean name 'studentService' via constructor to 
    bean named 'studentDao'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - 
    Autowiring by type from bean name 'studentService' via constructor to 
    bean named 'emailSender'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - 
    Autowiring by type from bean name 'studentService' via constructor to 
    bean named 'smsSender'
[main] INFO  c.f.s.service.StudentService - 学生小崔入学成功,已发送邮件和短信通知

核心原理:容器启动与依赖注入流程

关键阶段说明:

  1. 加载配置类:读取 @Configuration 类,解析其中的 @Bean 方法
  2. 组件扫描:@ComponentScan 扫描指定包路径,将 @Component、@Service、@Repository 等类注册为 BeanDefinition
  3. 注册 BeanDefinition:所有 Bean 的元数据(类名、作用域、依赖等)被存储到 BeanFactory 的 beanDefinitionMap 中
  4. BeanFactoryPostProcessor:在 Bean 实例化前,允许修改 BeanDefinition(如 @PropertySource 加载的属性值替换)
  5. 实例化:调用构造方法创建 Bean 对象
  6. 属性填充:根据 BeanDefinition 中的依赖信息,通过构造器、Setter 或字段注入依赖
  7. Aware 回调:如果 Bean 实现了 BeanNameAware、ApplicationContextAware 等接口,注入相应资源
  8. BeanPostProcessor:在初始化前后提供扩展点(如 AOP 代理在此阶段创建)
  9. 初始化:执行 @PostConstruct 或 InitializingBean.afterPropertiesSet()
  10. 就绪:单例 Bean 放入 singletonObjects 缓存,后续 getBean() 直接返回

易错场景与面试考点

反例一:字段注入导致单元测试困难

小崔最初使用字段注入,写单元测试时陷入困境:

// ❌ 错误:字段注入的类,测试时需要反射或 Spring 容器
public class StudentServiceTest {
    @Test
    public void testEnrollStudent() {
        StudentService service = new StudentService();  // 编译通过,但运行时空指针!
        // studentDao / emailSender / smsSender 都是 null
        service.enrollStudent(new Student("测试", 20, "测试班"));
    }
}

纠正:使用构造器注入后,测试变得简单直接:

// ✅ 正确:构造器注入的类,测试时直接传入 mock 对象
public class StudentServiceTest {
    @Test
    public void testEnrollStudent() {
        StudentDao mockDao = Mockito.mock(StudentDao.class);
        EmailSender mockEmail = Mockito.mock(EmailSender.class);
        SmsSender mockSms = Mockito.mock(SmsSender.class);

        StudentService service = new StudentService(mockDao, mockEmail, mockSms);
        service.enrollStudent(new Student("测试", 20, "测试班"));

        Mockito.verify(mockDao).save(Mockito.any(Student.class));
        Mockito.verify(mockEmail).send(Mockito.anyString(), Mockito.anyString());
    }
}

反例二:循环依赖的构造器注入陷阱

小崔在开发"班级-学生"双向关联时,不小心造成了循环依赖:

// ❌ 错误:构造器注入的循环依赖,容器启动失败
@Service
public class ClassService {
    private final StudentService studentService;

    public ClassService(StudentService studentService) {  // 循环依赖
        this.studentService = studentService;
    }
}

@Service
public class StudentService {
    private final ClassService classService;

    public StudentService(ClassService classService) {  // 循环依赖
        this.classService = classService;
    }
}

启动报错:

BeanCurrentlyInCreationException: Error creating bean with name 'classService':
Requested bean is currently in creation: Is there an unresolvable circular reference?

原理分析:

Spring 解决循环依赖依赖三级缓存机制,但仅对 singleton 作用域且通过构造器注入以外方式创建的 Bean 有效。构造器注入的循环依赖无法自动解决,因为构造器调用时必须传入完整的依赖对象,而此时依赖对象自身也在创建中。

解决方案:

  1. 重构代码:消除双向依赖,通过 ID 查询替代直接引用
  2. Setter 注入:将其中一个改为 Setter 注入,允许 Spring 通过三级缓存解决
  3. @Lazy 延迟注入:在构造器参数上加 @Lazy,注入的是代理对象而非真实对象
// ✅ 方案三:@Lazy 延迟注入
@Service
public class ClassService {
    private final StudentService studentService;

    public ClassService(@Lazy StudentService studentService) {
        this.studentService = studentService;
    }
}

面试高频题

Q1:IoC 和 DI 有什么区别?

IoC(控制反转)是一种设计原则,指将对象控制权从程序内部转移到外部容器;DI(依赖注入)是 IoC 的具体实现方式之一,通过构造器、Setter 或字段将依赖注入对象。DI 是实现 IoC 的最常用手段,但 IoC 还可以通过依赖查找(Dependency Lookup)等其他方式实现。

Q2:BeanFactory 和 ApplicationContext 有什么区别?

BeanFactory 是基础容器,延迟初始化,资源占用少;ApplicationContext 继承自 BeanFactory,提供 AOP 集成、国际化、事件传播、应用层特定上下文(如 WebApplicationContext),默认立即初始化单例 Bean。实际开发中几乎总是使用 ApplicationContext。

Q3:Spring 官方为什么推荐构造器注入?

① 依赖不可变,可声明 final;② 对象创建时依赖必须齐全,避免"半初始化"状态;③ 不依赖 Spring 容器即可进行单元测试;④ 依赖关系显式暴露在构造器签名中,代码可读性高。

Q4:字段注入有哪些隐患?

① 无法声明 final,依赖可能被修改;② 隐藏依赖关系,类的真实依赖不从 API 层面可见;③ 单元测试需要依赖 Spring 容器或反射;④ 无法在创建后重新注入(无 setter);⑤ 与 Java 的不可变对象设计理念相悖。

上一页
Spring Framework 概述
下一页
@Configuration 详解