Java微服务(一):1.0 Spring6 - Java开发框架的始祖

1. [核心思想] Spring 启示录:从设计原则到控制反转

摘要: 本章从 OCP 开闭原则DIP 依赖倒置原则 出发,引出 Spring 的核心思想——控制反转 (IoC),为理解其解耦能力奠定理论基石。


1.1. 软件开发的“初心”:OCP 开闭原则

我们进行软件开发时,最核心的目标之一就是构建易于维护和扩展的系统。开闭原则(Open-Closed Principle, OCP)正是指导我们实现这一目标的基础性原则,它要求一个软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭

在传统的、层级分明的代码结构中,我们常常会遇到一个棘手的问题:高耦合。这意味着代码模块之间紧密地绑定在一起。

image-20250809200224645

从上图可以很明显地看出,上层 是依赖 下层 的。UserController 依赖 UserServiceImpl,而 UserServiceImpl 又依赖 UserDaoImplForMySQL。这种依赖关系会导致一个连锁反应:下层模块一旦发生任何改动,上层模块极有可能需要随之修改。这便是所谓的“牵一发而动全身”。

当我们需要为软件增加新功能时,我们应当通过 增加新的代码(例如,新的类)来实现,而不是去修改那些已经被测试过且运行正常的旧代码。修改旧代码的风险在于,它可能会引入未知的缺陷,迫使我们对整个项目进行全方位回归测试,这是一个极其耗时且繁琐的过程。


1.2. 解耦的钥匙:依赖倒置原则 (DIP)

为了破解高耦合的困局,实现开闭原则,我们需要一把关键的“钥匙”——依赖倒置原则 (Dependence Inversion Principle, DIP)。它倡导我们应该 面向抽象(接口)编程,而不是面向具体实现编程

核心提示:包括开闭原则、依赖倒置原则在内的软件设计七大原则,它们的共同目标都是在为“解耦”服务。

image.png

你可能会说,上图中的代码已经遵循了“面向接口编程”的规范。确实,UserService 依赖的是 UserDao 接口。但问题出在对象的创建上:new UserDaoImplForOracle() 这行代码,让我们再次与具体的实现类产生了耦合。

依赖倒置原则的目标是让 上层不再依赖下层,实现依赖关系的“倒置”。完全符合依赖倒置原则的理想代码中,UserService 只持有 UserDao 接口的引用,完全不出现 new 任何具体实现类的代码。

这种理想的代码会带来一个显而易见的问题:userDao 引用是 null,运行时必然会导致空指针异常。要解决这个异常,我们必须回答两个核心问题:

  • 第一个问题:谁来负责对象的创建 ?(谁来执行 new UserDaoImplForOracle()?)
  • 第二个问题:谁来负责把创建的对象赋到属性上 ?(谁来把对象赋值给 userDao 属性?)

值得庆幸的是,Spring 框架正是为解决这两个核心问题而生的。它能帮我们创建对象,并自动地将这些对象赋值给需要的属性,建立它们之间的依赖关系。

这种将对象的创建权对象关系的管理权从我们的业务代码中移交出去的编程范式,就引出了 Spring 的核心思想——控制反转


1.3. Spring 的灵魂:控制反转 (IoC) 与依赖注入 (DI)

控制反转Inversion of Control, 缩写为 IoC),是面向对象编程中的一种核心设计思想,其主要目的就是用来 降低代码之间的耦合度。它的核心理念是:将对象的创建权交出去,将对象和对象之间关系的管理权交出去,由一个独立的第三方容器来负责这一切的创建与维护工作

IoC 是一种现代设计思想,其理念与 GoF 23 种经典设计模式一脉相承,但因其出现较晚而未被收录。

控制反转(IoC)是一种思想,而 依赖注入Dependency Injection, 缩写为 DI)是实现这种思想最常见、最重要的方式。我们将在后续章节中详细学习,DI 的具体实现又包括两种主要方式:

  • Set 方法注入
  • 构造方法注入

Spring 框架,正是一个完美实现了 IoC 思想,并以 DI 作为其核心机制的顶级容器框架


2. [宏观视角] Spring 框架概述

摘要: 本章我们将建立对 Spring 框架的整体认知,梳理 Spring 6.x 的核心模块构成,并总结其轻量非侵入IoCAOP等关键特性。


2.1. Spring 简介:为简化开发而生

Spring 是一个由 Rod Johnson 创建的开源 Java 框架,它的诞生是为了解决早期企业级应用(特别是 EJB)开发的复杂性、臃肿以及难以测试等问题。我们可以将 Spring 的核心价值概括为:它是一个轻量级的控制反转(IoC)面向切面(AOP)的容器框架。

Spring 的最终目标是为我们简化开发,让我们能将精力完全集中在核心业务逻辑的实现上,而尽可能少地关注那些非业务性的代码,如事务控制、安全日志等。从简单性、可测试性和松耦合的角度出发,任何 Java 应用都可以从 Spring 中受益。


2.2. Spring 6.x 核心模块全景图

进入 Spring 6.x 时代,整个框架由大约 8 个模块组成,这些模块按功能被清晰地划分到不同的组中。理解这些模块的构成,有助于我们从宏观上把握 Spring 的能力版图。

我们通过下面这张图,可以对 Spring 框架的核心构成有一个直观的理解。这些模块就像乐高积木,我们可以按需组合,构建出强大的应用程序。

image.png

下表清晰地总结了这些核心模块的功能:

模块 (Module)核心功能描述
Spring Core(框架基石) 实现了 控制反转 (IoC),是所有功能的基础。
Spring AOP(切面编程) 提供完整的 面向切面编程 支持,用于解耦业务与横切关注点。
Spring DAO(数据访问) 对原生 JDBC 进行了抽象和封装,简化了数据访问代码。
Spring ORM(对象关系映射) 提供对 MyBatisHibernate 等主流 ORM 框架的集成支持。
Spring Context(应用上下文) 提供国际化、事件传播、JNDI 等企业级服务,是框架功能的延伸。
Spring Web MVC(Web 框架) Spring 自带的、成熟的 MVC 框架,用于开发传统的 Servlet Web 应用。
Spring Webflux(响应式 Web) 完全异步、非阻塞的 响应式 Web 框架,适用于高并发场景。
Spring Web(集成支持) 用于集成 Struts 等早期的第三方 Web 框架。

在现代开发中,我们主要使用 Spring Web MVCSpring Webflux,而 Spring Web 模块已较少使用

我们上篇系列教程以 Spring Core 为准,对于一些 Springboot 的知识,在 Spring 框架中其实也可以直接使用,所以我们会以对比的形式,大家可以直接学习 SpringBoot 的相关语法,原生 Spring6 涉及到 xml 配置的语法简单过一下即可,开发不会再使用!


2.3. Spring 的核心特性总结

Spring 之所以能在众多框架中脱颖而出,得益于其一系列优雅且强大的核心特性。我们可以将其归纳如下:

核心特性说明
轻量无论是 JAR 包大小还是运行时资源开销,Spring 都极其轻量,对系统性能影响微乎其微。
非侵入式业务代码不依赖于 Spring 的特定 API,可以轻松地在不同环境中复用和测试。
控制反转 (IoC)Spring 的灵魂。通过 IoC 技术促进松耦合,由容器被动地将依赖注入对象,而非对象主动索取。
面向切面 (AOP)提供对 AOP 的丰富支持,允许我们将业务逻辑与系统服务(如事务、日志)优雅地分离。
容器Spring 是一个强大的容器,负责管理对象的配置、创建、装配及其完整的生命周期。
框架Spring 将简单的组件通过声明式(XML 或注解)的方式组合成复杂的应用,并提供丰富的基础设施。

2.4. [重要] 本教程技术栈版本总览

为了确保内容的前沿性与准确性,我们在本教程中将统一使用以下技术栈版本:

  • 开发工具 (IDE): IntelliJ IDEA 2024.1.4
  • 构建工具 (Build Tool): Apache Maven 3.9.11
  • 项目 JDK: Java 17
  • Spring Framework: 6.2.9
  • 测试框架 (Testing): JUnit 5.13.4

请注意,Spring Framework 6.x 要求 JDK 的最低版本为 Java 17。在开始学习前,请务必确保您的开发环境符合要求。


3. [奠基实践] 第一个 Spring 程序

摘要: 本章我们将使用 Apache Maven 从零搭建一个经典的 Spring 项目。我们将掌握 Spring 依赖的引入、核心配置文件的编写,并最终从 IoC 容器中获取第一个由 Spring 管理的 Bean 对象。

学习指引: 本章将涉及大量 XML 配置和手动创建容器的代码。请务必注意,这在现代 Spring Boot 开发中已 几乎完全被自动化配置和注解所取代。学习本章的目的是为了理解 Spring IoC 的底层工作原理,无需死记硬背 XML 语法


3.1. 项目搭建与依赖配置

我们将使用 Maven 来构建我们的第一个 Spring 项目。

3.1.1. 创建标准 Maven 项目

首先,我们使用 IDEA 创建一个标准的 Maven 项目,并规划出如下的目录结构:

image-20250809205228154

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
. 📂 spring6
├── 📄 pom.xml
└── 📂 src/
│ └── 📂 main/
│ └── 📂 java/
│ └── 📂 com/
│ └── 📂 example/
│ ├── 📄 Main.java
│ └── 📂 spring6/
│ └── 📂 bean/
│ ├── 📄 User.java

│ └── 📂 resources/
│ ├── 📄 beans.xml
│ └── 📂 test/
│ └── 📂 java/
│ └── 📂 com/
│ └── 📂 example/
│ └── 📂 spring6/
│ └── 📂 test/
│ ├── 📄 Spring6Test.java
└── 📂 target/

3.1.2. 引入核心依赖

接下来,我们在 pom.xml 文件中声明项目所需的依赖。对于基础 IoC 应用,我们仅需引入 spring-context。同时,我们使用业界标准的 JUnit 5 作为测试框架。

文件路径: spring6/pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>spring6</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.9</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>

SpringBoot 简化: Spring Boot 通过 “Starters” 机制(如 spring-boot-starter)极大地简化了依赖管理,我们无需再逐个添加这些基础依赖。


3.2. 编写第一个 Spring 程序

环境就绪后,我们通过三步来完成第一个 Spring 程序的编写。

3.2.1. 定义一个简单的 Bean

Bean 是 Spring IoC 容器管理的基本单元,本质上就是一个 POJO。

文件路径: src/main/java/com/example/spring6/bean/User.java

1
2
3
4
5
6
7
package com.example.spring6.bean;

public class User {
public User() {
System.out.println("User 的无参数构造方法执行。");
}
}

3.2.2. 创建 Spring 核心配置文件

我们需要一个 XML 配置文件来告诉 Spring 容器需要管理哪些 Bean。

文件路径: src/main/resources/beans.xml

1
2
3
4
5
6
7
8
<?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 id="userBean" class="com.example.spring6.bean.User"/>

</beans>

SpringBoot 简化: 在 Spring Boot 中,我们几乎不再使用 XML 文件。通过在 User 类上添加 @Component 注解,它就会被自动扫描并注册为 Bean。

3.2.3. 编写单元测试获取 Bean

最后,我们编写一个 JUnit 5 测试用例来手动启动 Spring 容器,并从中获取 User 对象。

文件路径: com\example\spring6\test\Spring6Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.spring6.test;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Spring6Test {

@Test
public void testFirst() {
// 1. 手动创建 Spring 容器对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");

// 2. 从容器中获取 Bean
Object userBean = applicationContext.getBean("userBean");
System.out.println(userBean);
}
}

SpringBoot 简化: Spring Boot 应用的启动入口是 main 方法中的 SpringApplication.run(),它会自动创建并初始化容器,我们无需手动 new 一个 ApplicationContext


3.3. IoC 工作机制深度剖析

第一个程序成功运行的背后,隐藏着 Spring IoC 容器的许多核心工作机制。我们以面试问答的形式,来深入剖析这些细节。

面试官深度追问
今天 下午 2:30

刚才的程序跑通了,我们来深挖一下。<bean> 标签的 id 属性可以重复吗?

学习者

不可以。在同一个 Spring 配置文件中,beanid 必须是唯一的,它就像是对象的身份证号。如果重复,容器在启动时就会抛出异常。

很好。那 Spring 底层是如何创建 User 对象的?是不是必须要有无参数构造方法?

学习者

是的。Spring IoC 容器本质上是通过 Java 的反射机制 来实例化对象的。它会获取 class 属性指定的类,然后调用该类的 无参构造函数 来创建实例。因此,被 Spring 管理的 Bean 必须提供一个无参数构造器。

原来如此。那 Spring 把这些创建好的 Bean 实例存放在哪里了呢?

学习者

Spring 容器内部会维护一个类似 Map<String, Object> 的数据结构,我们通常称之为“单例池” (Singleton Cache)。配置的每个 <bean> 都会被实例化成一个对象,并以其 id 为键 (key),以对象实例为值 (value) 存放在这个 Map 中。

getBean() 方法的返回值是 Object,如果我想直接调用 User 的方法,每次都要强转,有没有更便捷的方式?

学习者

有的。getBean() 方法有一个重载版本,可以传入一个 Class 类型的参数。像这样:User user = applicationContext.getBean("userBean", User.class); 这样获取到的直接就是指定类型的对象,无需手动强转。

不错。最后问一个,ApplicationContext 和它的父接口 BeanFactory 有什么区别?

学习者

BeanFactory 是 Spring IoC 容器的顶级接口,定义了获取 Bean 的最基本方法,是“Bean 工厂”的本质。而 ApplicationContext 是它的子接口,功能更强大。它除了继承 BeanFactory 的所有功能外,还额外提供了对国际化事件发布AOP 等企业级特性的支持。我们通常推荐使用 ApplicationContext


3.4. [推荐实践] 集成 Log4j2 日志框架

专业的应用程序离不开日志系统。从 Spring 5 开始,官方推荐使用 Log4j2 作为集成的日志框架。

3.4.1. 添加依赖

我们需要在 pom.xml 中添加 log4j-corelog4j-slf4j2-impl 两个依赖。

文件路径: spring6/pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<project>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.9</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.25.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.25.1</version>
</dependency>
</dependencies>
</project>

3.4.2. 添加配置文件

按照约定,我们需要在 src/main/resources 目录下创建一个名为 log4j2.xml 的配置文件。

文件路径: src/main/resources/log4j2.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<root level="DEBUG">
<appender-ref ref="spring6log"/>
</root>
</loggers>
<appenders>
<console name="spring6log" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-5level %logger{36} - %msg%n"/>
</console>
</appenders>
</configuration>

SpringBoot 简化: Spring Boot 的 spring-boot-starter-logging 提供了开箱即用的日志功能(默认 Logback),我们通常只需在 application.properties 中配置级别即可。

总结: 至此,我们已经完整地搭建了一个最小化的、基于 Maven 和 XML 配置的 Spring IoC 应用,并为其配备了专业的日志系统。这是后续所有学习的坚实基础。


4. [核心实践] 依赖注入详解:构筑对象关系的艺术

摘要: IoC 是一种思想,而 DI 是其最重要的实现。本章是 Spring 学习的重中之重。我们将系统性地学习如何通过 XML 配置,将不同类型的依赖(其他 Bean、字面量、集合等)注入到目标对象中,并掌握 p/c 命名空间等简化配置的实用技巧。

4.1. 注入的两种主要方式:Setter 注入 vs 构造器注入

依赖注入(DI)的本质,是容器将一个对象所依赖的其他对象(或值)“注入”到该对象中的过程。Spring 提供了两种主要的注入方式:

  • Setter 注入: 这是最常用、最灵活的方式。容器先通过无参构造器创建 Bean 实例,然后调用该实例的 setXxx() 方法来完成依赖注入。它的优点是允许我们选择性地注入依赖,非常灵活。
  • 构造器注入: 容器通过调用 Bean 的构造方法,在实例化 Bean 的同时就完成依赖注入。它的优点是能保证依赖在对象创建时就已就绪,通常用于注入那些必不可少的、不可变的依赖。

我们将从最经典的 Setter 注入开始,深入探索其各种应用场景。


4.2. Setter 注入深度实践

4.2.1. 注入其他 Bean 对象 (ref)

这是最常见的场景:一个 Bean 依赖于另一个 Bean。例如,UserService 的运行需要依赖一个 UserDao 对象来操作数据库。

1. 准备代码

我们在之前的 spring6 项目中继续操作,首先创建 UserDaoUserService 两个类。

文件路径: src/main/java/com/example/spring6/bean/UserDao.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.spring6.bean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 数据访问层 Bean
public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

public void insert() {
logger.info("数据库正在保存用户数据...");
}
}

文件路径: src/main/java/com/example/spring6/bean/UserService.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.bean;

// 服务层 Bean
public class UserService {
// UserService 依赖 UserDao
private UserDao userDao;

// 关键:Spring 容器将通过调用此 set 方法,将 UserDao 的实例注入进来
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
// 调用依赖对象的方法
userDao.insert();
}
}

2. 配置与测试

接下来,我们需要在 Spring 的 XML 配置文件中,明确声明这两个 Bean 以及它们之间的依赖关系。

文件路径: src/main/resources/beans.xml (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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 id="userBean" class="com.example.spring6.bean.User"/>
<bean id="userDaoBean" class="com.example.spring6.bean.UserDao"/>
<bean id="userServiceBean" class="com.example.spring6.bean.UserService">
<!--
使用 <property> 标签来完成 Setter 注入
- name 属性:值是 Java 类中的属性名 (userDao)
Spring 会根据这个名字推导出对应的 setter 方法 (setUserDao)
- ref 属性: 值是另一个 Bean 的 id
ref 是 "reference" (引用) 的缩写
它告诉 Spring 容器,需要将 id 为 userDaoBean 的那个对象注入进来
-->
<property name="userDao" ref="userDaoBean"/>
</bean>

</beans>

测试代码:

文件路径: src/test/java/com/example/spring6/test/DITest.java (新增测试类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.spring6.test;

import com.example.spring6.bean.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DITest {
@Test
public void testDIBySetter() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
userService.saveUser();
}
}

在 Spring Boot 中,我们可以使用注解方式来简化 Bean 的注入和配置。

Java 代码:
文件路径: src/main/java/com/example/spring6/bean/UserDao.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.spring6.bean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

// 数据访问层 Bean
@Component
public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

public void insert() {
logger.info("数据库正在保存用户数据...");
}
}

Java 代码:
文件路径: src/main/java/com/example/spring6/bean/UserService.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.spring6.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

// 服务层 Bean
@Service
public class UserService {
// Spring Boot 自动注入 UserDao
private final UserDao userDao;

@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
userDao.insert();
}
}

测试代码:
文件路径: src/test/java/com/example/spring6/test/DITest.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.test;

import com.example.spring6.bean.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DITest {
@Autowired
private UserService userService; // 自动注入

@Test
public void testDIBySetter() {
userService.saveUser();
}
}

4.2.2. 注入字面量/简单类型 (value)

除了注入对象,我们还可以注入字符串、数字、布尔值等简单类型,这些值被称为“字面量”。

1. 修改 User

我们为之前的 User 类添加 name (String) 和 age (int) 属性,并提供对应的 setter 方法。

文件路径: src/main/java/com/example/spring6/bean/User.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.spring6.bean;

public class User {
private String name;
private int age;

public User() {} // 保留无参构造器

public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }

@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}

2. 配置与测试

配置思路解读
当注入的是字面量时,我们同样使用 <property> 标签,但与之配合的是 value 属性,而不是 ref 属性。下面我们对比三种主流的配置方案。

这种方式最简单,Java 类是纯净的 POJO,不需要任何 Spring 注解。

XML 配置:

1
2
3
4
<bean id="userBean" class="com.example.spring6.bean.User">
<property name="name" value="Prorise"/>
<property name="age" value="25"/>
</bean>

Java 代码:

1
2
3
4
5
6
// User.java (无需任何注解)
public class User {
private String name;
private int age;
// ... setters
}

这种混合方式下,Bean 的定义仍在 XML 中,但值的注入可以通过注解完成。前提是必须在 XML 中开启注解处理

XML 配置:

1
2
3
<context:annotation-config/>

<bean id="userBean" class="com.example.spring6.bean.User"/>

Java 代码:

1
2
3
4
5
6
7
8
// User.java (无需 @Component)
public class User {
@Value("Prorise") // 使用 @Value 注入值
private String name;
@Value("25")
private int age;
// ... setters
}

这是现代 Spring/Spring Boot 的标准做法,XML 中只需开启组件扫描即可。

XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 开启注解配置 -->
<context:annotation-config/>

<!-- 组件扫描,自动扫描指定包下的注解 -->
<context:component-scan base-package="com.example.spring6.bean"/>

Java 代码:

1
2
3
4
5
6
7
8
9
// User.java
@Component("userBean") // 使用 @Component 将其声明为 Bean
public class User {
@Value("Prorise")
private String name;
@Value("25")
private int age;
// ...
}

测试代码:

1
2
3
4
5
6
7
@Test
public void testSimpleTypeDI() {
// 确保加载了包含以上三种配置之一的 beans.xml 文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}

4.2.3. 注入集合类型 (List, Set, Map, Properties)

Spring 提供了专门的 <list>, <set>, <map>, <props> 标签来为集合类型的属性注入值。

1. 新增 Person

文件路径: src/main/java/com/example/spring6/bean/Person.java (新增文件)

1
2
3
4
5
6
7
8
@Setter
@ToString
public class Person {
private List<String> interests;
private Set<String> phones;
private Map<String, String> family;
private Properties dbConfig;
}

2. 配置与测试

配置思路解读:
这种方式将所有配置硬编码在 XML 文件中,每个集合类型都有其对应的专属标签。

XML 配置:
文件路径: src/main/resources/beans-collection.xml (新增配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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 id="personBean" class="com.example.spring6.bean.Person">
<property name="interests">
<list>
<value>编程</value>
<value>游戏</value>
<value>旅行</value>
</list>
</property>

<property name="phones">
<set>
<value>13888888888</value>
<value>13999999999</value>
</set>
</property>

<property name="family">
<map>
<entry key="father" value="老王"/>
<entry key="mother" value="老李"/>
</map>
</property>

<property name="dbConfig">
<props>
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/db</prop>
</props>
</property>
</bean>
</beans>

测试代码:

1
2
3
4
5
6
@Test
public void testCollectionDI() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans-collection.xml");
Person person = context.getBean("personBean", Person.class);
System.out.println(person);
}

配置思路解读:
Spring Boot 的核心思想是 约定大于配置配置外部化。我们将所有配置数据移至 application.yml 文件中,并通过 @ConfigurationProperties 注解,以类型安全的方式将这些数据绑定到 Java 对象上。

YAML 配置:
文件路径: src/main/resources/application.yml (新增或修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将所有配置结构化地定义在 yml 文件中
person:
interests:
- 编程
- 游戏
- 旅行
phones:
- 13888888888
- 13999999999
family:
father: 老王
mother: 老李
db-config: # yml 中的 kebab-case (短横线) 会自动映射到 Java 的 camelCase (驼峰)
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db

Java 代码:
文件路径: src/main/java/com/example/spring6/bean/Person.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.bean;
// ... imports
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

// 1. 将其声明为一个 Spring Bean
@Component
// 2. 声明此类用于绑定配置文件中 "person" 前缀的属性
@ConfigurationProperties(prefix = "person")
@Setter
@ToString
public class Person {
private List<String> interests;
private Set<String> phones;
private Map<String, String> family;
private Properties dbConfig;
}

测试代码 (Spring Boot 风格):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DiApplicationTests {
@Autowired
private Person person; // 直接注入

@Test
void testConfigurationProperties() {
System.out.println(person);
}
}

4.2.4. 处理特殊值:注入 null 与含特殊符号的 CDATA

注入 null

如果你需要明确地将一个 null 注入给属性,可以使用 <null/> 标签。

1
2
3
4
<bean id="userBean" class="com.example.spring6.bean.User">
<property name="name">
<null/> </property>
</bean>

注入含特殊符号的字符串

XML 中 <>& 等是特殊字符。如果你的字符串值包含它们,需要使用 CDATA 块来包裹,以避免 XML 解析错误。

1
2
3
4
5
<bean id="mathBean" class="com.example.spring6.bean.MathBean">
<property name="expression">
<value><![CDATA[ a < b && b > c ]]></value>
</property>
</bean>

4.3. 构造器注入的应用场景与配置

当一个依赖是 必需的,我们希望在对象创建时就保证其存在,这时就应该使用构造器注入。

1. 修改 UserService

我们将 UserService 修改为通过构造器接收 UserDao

文件路径: src/main/java/com/example/spring6/bean/UserService.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.spring6.bean;

public class UserService {
// 推荐将依赖声明为 final,确保不可变
private final UserDao userDao;

// 使用构造器进行注入
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
userDao.insert();
}
}

2. 配置与测试

1
2
3
<bean id="service1" class="com.example.spring6.bean.UserService">
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>

注意:@Service 以及相关注解属于 springframework, 并不只属于 Spring-boot,所以在没有 Springboot 的环境也是可以运行的,不要搞混!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.spring6.bean;

import org.springframework.stereotype.Service;

@Service
public class UserService {
// 推荐将依赖声明为 final,确保不可变
private final UserDao userDao;

// 使用构造器进行注入
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
userDao.insert();
}
}

由于我们之前开启过组件扫描了,所以我们将 beans.xmluserServiceBean 删除即可


4.4. 简化 XML:p 命名空间与 c 命名空间

SpringBoot 简化: 由于现代开发全面转向注解,pc 命名空间已成为历史,我们了解即可,完全无需记忆

为了简化 XML 的冗长写法,Spring 提供了 pc 两个命名空间。

  • p 命名空间:用于简化 Setter 注入 (p for property)。
  • c 命名空间:用于简化 构造器注入 (c for constructor)。

1. 添加命名空间声明

首先,需要在 <beans> 根标签上添加 pc 的声明。

1
2
3
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c" ...>

2. 使用示例

1
2
3
4
5
6
7
8
9
<bean id="user1" class="com.example.spring6.bean.User">
<property name="name" value="AnZhiYu"/>
</bean>
<bean id="user2" class="com.example.spring6.bean.User" p:name="Prorise"/>

<bean id="service1" class="com.example.spring6.bean.UserService">
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>
<bean id="service2" class="com.example.spring6.bean.UserService" c:userDao-ref="userDaoBean"/>

4.5. 外部化配置:引入 .properties 属性文件

在实际项目中,数据库连接信息等敏感、易变的数据不应硬编码在 XML 中。正确的做法是将其放在外部的 .properties 文件中,由 Spring 动态加载。

1. 创建 jdbc.properties 文件

文件路径: src/main/resources/jdbc.properties (新增文件)

1
2
3
4
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/testdb
db.username=root
db.password=123456

2. 添加 context 命名空间并加载文件

我们需要 context 命名空间下的 <context:property-placeholder> 标签来加载属性文件。

文件路径: src/main/resources/beans-db.xml (新增配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:property-placeholder location="classpath:jdbc.properties"/>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
</beans>

SpringBoot 简化: Spring Boot 约定大于配置,它会自动加载 src/main/resources/application.propertiesapplication.yml 文件。我们只需在类中使用 @Value("${db.driver}") 注解即可直接注入属性值,或通过 @ConfigurationProperties 进行类型安全的属性绑定,比 XML 配置简洁得多。

3.Spring Boot 示例

在 Spring Boot 中,我们可以通过直接在 application.properties 文件中进行配置,使用 @Value@ConfigurationProperties 注解来实现外部化配置的功能。

application.properties 配置:
文件路径: src/main/resources/application.properties (新增或修改)

1
2
3
4
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/testdb
db.username=root
db.password=123456

Java 代码:
文件路径: src/main/java/com/example/spring6/config/DataSourceConfig.java (新增配置类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.spring6.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;

@Configuration
public class DataSourceConfig {

@Value("${db.driver}")
private String driverClassName;

@Value("${db.url}")
private String url;

@Value("${db.username}")
private String username;

@Value("${db.password}")
private String password;

@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}

application.yml 配置:
文件路径: src/main/resources/application.yml (替代 application.properties,如果需要使用 YAML 格式)

1
2
3
4
5
db:
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/testdb
username: root
password: 123456

Java 代码: 使用 @ConfigurationProperties 绑定配置属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.spring6.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "db")
public class DataSourceConfig {

private String driver;
private String url;
private String username;
private String password;

@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}

// Getters and Setters
}

本章总结: 至此,我们已经全面掌握了在 Spring 经典 XML 配置中进行依赖注入的各种核心技能。虽然现代开发已更多地转向注解,但理解这些基于 XML 的配置原理,对于我们深入把握 Spring 的 IoC 本质、排查疑难问题具有不可替代的价值。


5. [进阶配置] Bean 的高级管理

摘要: 掌握了基础的 DI 配置后,本章我们将深入探索 Bean 的更多高级特性。我们将学习 Bean 的作用域如何影响其实例数量,Bean 的完整生命周期流程,以及 Spring 创建 Bean 的多种底层方式,包括强大的 FactoryBean。


5.1. Bean 的作用域 (Scope)

在 Spring 中,作用域 (Scope) 决定了 Spring IoC 容器如何创建和管理 Bean 的实例。简单来说,它回答了这样一个问题:“当我从容器中请求一个 Bean 时,是每次都给我一个新的对象,还是始终给我同一个?”。正确地使用作用域对于应用的性能和状态管理至关重要。

Spring 定义了多种作用域,但最核心、最常用的只有两种:singleton (单例) 和 prototype (原型/多例)。

5.1.1. 详解 singleton:唯一的实例

singleton 是 Spring 默认 的作用域。当一个 Bean 的作用域为 singleton 时,无论你从容器中获取多少次该 Bean,Spring 容器都 只会返回同一个共享的实例

1. 特性与创建时机

  • 唯一实例: 在整个 Spring IoC 容器的生命周期内,一个 Bean ID 只对应一个对象实例。
  • 创建时机: 默认情况下,singleton 作用域的 Bean 在 容器启动和初始化时 就会被创建并放入一个缓存区(俗称“单例池”),等待后续的注入和调用。

2. 实践验证

我们在 spring6 项目中进行验证。

文件路径: src/main/java/com/example/spring6/bean/SpringBean.java (新增文件)

1
2
3
4
5
6
7
package com.example.spring6.bean;

public class SpringBean {
public SpringBean() {
System.out.println("SpringBean 的无参数构造方法执行了...");
}
}

文件路径: src/main/resources/spring-scope.xml (新增配置文件)

1
2
3
4
5
6
7
8
<?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 id="sb" class="com.example.spring6.bean.SpringBean"/>

</beans>

测试代码:
文件路径: src/test/java/com/example/spring6/test/ScopeTest.java (新增测试类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.spring6.test;

import com.example.spring6.bean.SpringBean;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ScopeTest {
@Test
public void testScope() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println("sb1 = " + sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println("sb2 = " + sb2);


}
@Test
public void testSingletonCreateTiming() {
// 仅创建容器,不获取 Bean
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
}

}

1
2
3
SpringBean 的无参数构造方法执行了...
sb1 = com.example.spring6.bean.SpringBean@2c88b9fc
sb2 = com.example.spring6.bean.SpringBean@2c88b9fc

结果分析: 构造方法只执行了一次,并且两次获取到的对象地址是完全相同的,证明了 singleton 的唯一性。

运行 testSingletonCreateTiming 测试:

1
SpringBean 的无参数构造方法执行了...

结果分析: 即使我们没有调用 getBean(),构造方法依然在容器初始化时就被执行了,证明了其“饿汉式”的创建时机。


5.1.2. 详解 prototype:多变的实例

singleton 相对,prototype 作用域的 Bean 每次被请求时,Spring 容器都会 创建一个全新的实例 返回。

1. 特性与创建时机

  • 全新实例: 每一次 getBean() 调用或每一次注入操作,都会触发一次全新的对象创建过程。
  • 创建时机: prototype 作用域的 Bean 是 懒加载 的。只有当它被实际请求(getBean())时,容器才会去创建它的实例。
  • 生命周期管理: Spring 容器在创建并初始化 prototype Bean 后,就会将其交给调用方,不再追踪其后续的生命周期。这意味着 Spring 不会为 prototype Bean 调用其销毁方法 (destroy-method)。

2. 实践验证

文件路径: src/main/resources/spring-scope.xml (修改)

1
<bean id="sb" class="com.example.spring6.bean.SpringBean" scope="prototype"/>

测试代码: 使用与上面完全相同的 ScopeTest.java

运行 testSingletonScope 测试 (现在 sb Bean 是 prototype):

1
2
3
4
SpringBean 的无参数构造方法执行了...
sb1 = com.example.spring6.bean.SpringBean@5316e95f
SpringBean 的无参数构造方法执行了...
sb2 = com.example.spring6.bean.SpringBean@3f053c80

结果分析: 构造方法被执行了两次,并且两次获取到的对象地址是不同的,证明了 prototype 的多例性。

运行 testSingletonCreateTiming 测试 (现在 sb Bean 是 prototype):

1
(无任何输出)

结果分析: 仅创建容器而没有 getBean() 时,构造方法完全没有被执行,证明了其“懒加载”的创建时机。


5.1.3. Spring Boot 对比:使用 @Scope 注解

在 Spring Boot 中,我们使用 @Scope 注解来声明 Bean 的作用域,这比 XML 配置更加直观和便捷。

通过在 <bean> 标签上设置 scope 属性来定义作用域。

1
2
3
<bean id="singletonBean" class="com.example.spring6.bean.SpringBean" />

<bean id="prototypeBean" class="com.example.spring6.bean.SpringBean" scope="prototype"/>

通过在 Bean 的声明注解(如 @Component, @Service)之上添加 @Scope 注解来定义作用域。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SpringBean.java
package com.example.spring6.bean;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component // 1. 首先,它必须是一个被 Spring 管理的 Bean
@Scope("prototype") // 2. 使用 @Scope 注解指定其作用域为 prototype
public class SpringBean {
public SpringBean() {
// 为了方便观察,每次创建都打印线程名
System.out.println("SpringBean constructor executed by thread: " + Thread.currentThread().getName());
}
}

5.2. Bean 的实例化方式

我们知道,Spring 创建 Bean 最常规的方式是调用其无参构造函数。但在真实的开发世界里,对象的创建过程远比 new User() 要复杂得多。本节,我们将聚焦于那些“非常规”的实例化场景,并探索 Spring 是如何通过多种灵活的机制来优雅地应对它们的。

5.2.1. 构造方法实例化 (回顾)

这是标准场景,我们不再赘述。当一个类拥有公开的无参构造器时,Spring 默认通过它来创建实例。

1
<bean id="userBean" class="com.example.spring6.bean.User"/>

5.2.2. 静态工厂方法 (factory-method)

“痛点”场景:

假设我们需要集成一个公司内部的遗留工具库。这个库中有一个 LegacyApiClient 类,它的构造函数是 private 的,我们无法直接 new 它。幸运的是,这个类提供了一个静态方法 public static LegacyApiClient getInstance() 来获取其单例对象。那么,我们如何在 Spring XML 中配置并管理这个由静态方法创建的对象呢?

解决方案:
Spring 提供了 factory-method 属性,专门用于调用一个静态方法来创建 Bean 实例。

1. 准备代码 (模拟遗留库)

文件路径: src/main/java/com/example/spring6/bean/LegacyApiClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.spring6.bean;

// 模拟一个无法直接 new 的遗留类
public class LegacyApiClient {
// 构造器是私有的
private LegacyApiClient() {
System.out.println("遗留 API 客户端实例已创建...");
}

// 提供一个公共的静态工厂方法
public static LegacyApiClient getInstance() {
return new LegacyApiClient();
}
}

2. 配置与测试

XML 配置: src/main/resources/spring-instantiation.xml (新增配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
<?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">

<!-- LegacyApiClient.getInstance() 是一个 public static 方法 -->
<bean id="apiClient"
class="com.example.spring6.bean.LegacyApiClient"
factory-method="getInstance"/>

</beans>

测试代码:

1
2
3
4
5
6
@Test
public void testStaticFactory() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-instantiation.xml");
LegacyApiClient client = context.getBean("apiClient", LegacyApiClient.class);
System.out.println(client);
}

结论: 我们成功地让 Spring 通过调用 LegacyApiClient.getInstance() 方法,将这个原本无法直接实例化的对象纳入了容器管理。


5.2.3. 实例工厂方法 (factory-bean)

“痛点”场景:

现在情况变得更复杂。我们需要一个数据库连接池 ConnectionPool 对象,但这个对象不能直接 new,必须由一个 ConnectionPoolManager 的实例来创建。更关键的是,这个 manager 对象本身在创建 pool 之前,需要先进行配置(比如设置最大连接数 maxConnections)。我们如何让 Spring 先创建并配置好 manager,然后再调用这个配置好的 manager 实例的 createPool() 方法来创建 pool Bean 呢?

解决方案:
这就是 factory-bean 属性的用武之地。它允许我们指定一个已经存在的 Bean 实例作为工厂,来创建另一个 Bean。

1. 准备代码

文件路径: src/main/java/com/example/spring6/bean/ConnectionPool.java

1
2
3
package com.example.spring6.bean;
// 模拟的连接池产品
public class ConnectionPool {}

文件路径: src/main/java/com/example/spring6/bean/ConnectionPoolManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.spring6.bean;

// 模拟的连接池管理器(实例工厂)
public class ConnectionPoolManager {
private int maxConnections;

public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
}

// 实例方法,负责创建 ConnectionPool 实例
public ConnectionPool createPool() {
System.out.println("实例工厂 ConnectionPoolManager 正在创建连接池,最大连接数:" + this.maxConnections);
return new ConnectionPool();
}
}

2. 配置与测试

XML 配置: src/main/resources/spring-instantiation.xml (修改)

1
2
3
4
5
6
7
<bean id="poolManager" class="com.example.spring6.bean.ConnectionPoolManager">
<property name="maxConnections" value="10"/>
</bean>

<bean id="connectionPool"
factory-bean="poolManager"
factory-method="createPool"/>

测试代码:

1
2
3
4
5
6
@Test
public void testInstanceFactory() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-instantiation.xml");
ConnectionPool pool = context.getBean("connectionPool", ConnectionPool.class);
System.out.println(pool);
}

5.2.4. FactoryBean 接口:一种特殊的工厂 Bean

“痛点”场景:

在项目中,我们可能需要根据配置文件中的一个 type 值(例如 ‘simple’ 或 ‘complex’)来决定创建一个 SimpleMessageConverter 还是 ComplexMessageConverter。这个创建逻辑如果散落在业务代码中会非常混乱。我们希望将这种复杂的、有条件的创建逻辑封装成一个可重用的 Spring 组件,让这个“工厂”本身可以被配置,并由 Spring 来调用它生产最终的 Bean。

解决方案:
Spring 提供了一个完美的、框架原生的解决方案:实现 FactoryBean 接口。它允许我们将复杂的实例化逻辑封装在一个类中,使其成为一个专门为 Spring 生产 Bean 的“工厂 Bean”。

1. 准备代码

文件路径: src/main/java/com/example/spring6/bean/MessageConverter.java

1
2
3
4
5
6
7
package com.example.spring6.bean;

// 产品接口
public interface MessageConverter {}
// 两种具体实现
class SimpleMessageConverter implements MessageConverter {}
class ComplexMessageConverter implements MessageConverter {}

文件路径: src/main/java/com/example/spring6/bean/MessageConverterFactoryBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.spring6.bean;
import org.springframework.beans.factory.FactoryBean;

// 1. 实现 FactoryBean 接口
public class MessageConverterFactoryBean implements FactoryBean<MessageConverter> {

// 这个工厂本身可以被配置
private String type = "simple";

public void setType(String type) {
this.type = type;
}

// 2. 封装复杂的创建逻辑在 getObject() 方法中
@Override
public MessageConverter getObject() throws Exception {
System.out.println("FactoryBean 正在根据 type='" + type + "' 创建 MessageConverter...");
if ("simple".equalsIgnoreCase(type)) {
return new SimpleMessageConverter();
} else if ("complex".equalsIgnoreCase(type)) {
return new ComplexMessageConverter();
}
throw new IllegalArgumentException("未知的转换器类型: " + type);
}

@Override
public Class<?> getObjectType() {
return MessageConverter.class;
}
}

2. 配置与测试

XML 配置: src/main/resources/spring-instantiation.xml (修改)

1
2
3
<bean id="messageConverter" class="com.example.spring6.bean.MessageConverterFactoryBean">
<property name="type" value="complex"/>
</bean>

测试代码:

1
2
3
4
5
6
7
8
@Test
public void testFactoryBean() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-instantiation.xml");
// 当我们获取 id 为 "messageConverter" 的 Bean 时...
MessageConverter converter = context.getBean("messageConverter", MessageConverter.class);
// ...Spring 返回的不是 FactoryBean 本身,而是它 getObject() 方法的返回值!
System.out.println("获取到的 Bean 类型: " + converter.getClass().getSimpleName());
}

5.2.5. Spring Boot 对比:使用 @Configuration@Bean 方法

“痛点”场景:

我们已经看到了 XML 中各种工厂方法的威力,但 XML 配置是字符串形式的,类型不安全(写错类名或方法名在编译期无法发现),难以重构(IDE 无法方便地查找引用和重命名),而且配置和逻辑是分离的。在 Spring Boot 中,有没有一种既能实现所有这些复杂创建逻辑,又具备 Java 代码所有优点的现代方式呢?

解决方案:
当然有!这就是 Spring Boot 的核心配置机制:@Configuration 类和 @Bean 方法。它允许我们用纯 Java 代码来定义和配置 Bean。

  • 静态工厂:
    1
    <bean class="...Factory" factory-method="get"/>
  • 实例工厂:
    1
    2
    <bean id="myFactory" class="...Factory"/>
    <bean factory-bean="myFactory" factory-method="get"/>
  • FactoryBean:
    1
    <bean id="product" class="...ProductFactoryBean"/>

配置思路解读:
我们创建一个被 @Configuration 注解的类,这个类就相当于一个 XML 配置文件。类中所有被 @Bean 注解的方法,其返回值都会被 Spring 注册为一个 Bean,方法名默认就是 Bean 的 ID。

Java 配置: src/main/java/com/example/spring6boot/config/AppConfig.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.example.spring6boot.config;

import com.example.spring6.bean.*; // 引入我们之前定义的类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // 声明这是一个配置类,替代 XML 文件
public class AppConfig {

// 等价于【静态工厂方法】
@Bean
public LegacyApiClient apiClient() {
return LegacyApiClient.getInstance();
}

// 等价于【实例工厂方法】
@Bean
public ConnectionPoolManager poolManager(@Value("${pool.maxConnections:20}") int max) {
// 方法参数可以注入配置值或其它 Bean
ConnectionPoolManager manager = new ConnectionPoolManager();
manager.setMaxConnections(max); // 编程方式进行配置
return manager;
}
@Bean
public ConnectionPool connectionPool(ConnectionPoolManager manager) { // 自动注入上面定义的 manager
return manager.createPool();
}

// 等价于【FactoryBean】
// 我们可以用纯 Java 逻辑实现更复杂的创建过程
@Bean
public MessageConverter messageConverter(@Value("${converter.type:simple}") String type) {
System.out.println("@Bean 正在根据 type='" + type + "' 创建 MessageConverter...");
if ("simple".equalsIgnoreCase(type)) {
return new SimpleMessageConverter();
} else {
return new ComplexMessageConverter();
}
}
}

对比总结: Spring Boot 的 Java 配置方式 (@Configuration + @Bean) 完胜 XML 配置。它提供了完全的类型安全(所有东西都是 Java 代码,编译器会检查错误)、更好的可重构性(可以利用 IDE 的全部功能),并且能以编程方式实现任意复杂的 Bean 创建逻辑。这是现代 Spring 应用配置 Bean 的标准和推荐方式。


5.3. Bean 的生命周期

一个 Bean 在 Spring IoC 容器中,从被创建到最终被销毁,会经历一系列预定义的阶段,这就是 Bean 的生命周期。理解这个过程至关重要,因为它为我们提供了在特定时间点介入、执行自定义逻辑的机会。

“痛点”场景:

假设我们有一个 DatabaseManager Bean,它负责管理数据库连接。我们必须确保,在它所有必需的属性(如 url, username)被 Spring 注入之后,立刻调用 openConnection() 方法来建立连接;而在整个应用关闭、容器销毁这个 Bean 之前,必须调用 closeConnection() 方法来安全地释放资源。我们如何才能将这两个关键操作精确地安插在这两个时间点呢?

解决方案:
Spring 强大的生命周期回调机制,正是为了解决这类问题而设计的。它允许我们指定特定的方法,在 Bean 初始化和销毁时自动执行。

5.3.1. 核心五步:从实例化到销毁

我们可以将一个 Bean 的生命周期粗略地划分为五个核心阶段,使用时间线来展示最为清晰:

bean生命周期

Instantiation

Spring 容器通过构造方法创建 Bean 的实例。

Populate Properties

Spring 容器通过 DI 为 Bean 的属性注入值。

Initialization

执行开发者自定义的初始化逻辑,例如 openConnection()

In Use

Bean 处于可用状态,可以被应用程序中的其他对象调用。

Destruction

容器关闭时,执行自定义的销毁逻辑,例如 closeConnection()

实践验证

文件路径: src/main/java/com/example/spring6/bean/LifecycleBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.spring6.bean;

public class LifecycleBean {
private String name;

// 对应生命周期第 1 步
public LifecycleBean() {
System.out.println("1. 实例化 Bean (调用构造器)");
}

// 对应生命周期第 2 步
public void setName(String name) {
this.name = name;
System.out.println("2. Bean 属性赋值 (调用 setter)");
}

// 对应生命周期第 3 步 (自定义初始化方法)
public void init() {
System.out.println("3. 初始化 Bean (调用自定义 init 方法)");
}

// 对应生命周期第 5 步 (自定义销毁方法)
public void destroy() {
System.out.println("5. 销毁 Bean (调用自定义 destroy 方法)");
}
}

文件路径: src/main/resources/spring-lifecycle.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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 id="lifecycleBean"
class="com.example.spring6.bean.LifecycleBean"
init-method="init"
destroy-method="destroy">
<property name="name" value="TestBean"/>
</bean>

</beans>

文件路径: src/test/java/com/example/spring6/test/LifecycleTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.spring6.test;

import com.example.spring6.bean.LifecycleBean;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class LifecycleTest {
@Test
public void testLifecycleFiveSteps() {
// 要想触发销毁方法,必须使用 ClassPathXmlApplicationContext 并手动 close()
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-lifecycle.xml");

LifecycleBean bean = context.getBean("lifecycleBean", LifecycleBean.class);
System.out.println("4. 使用 Bean");

// 关键:手动关闭容器,以触发 Bean 的销毁阶段
context.close();
}
}
1
2
3
4
5
1. 实例化 Bean (调用构造器)
2. Bean 属性赋值 (调用 setter)
3. 初始化 Bean (调用自定义 init 方法)
4. 使用 Bean
5. 销毁 Bean (调用自定义 destroy 方法)

结论: 通过 init-methoddestroy-method,我们成功地将自定义逻辑挂载到了 Bean 的初始化和销毁阶段。


5.3.2. 关键扩展点:BeanPostProcessor

“痛点”场景:

init-method 很好用,但它只能对单个 Bean 生效。如果我们想对容器中所有(或某一批)的 Bean,在它们各自的 init 方法执行前后,都统一执行一段逻辑(比如打印日志、创建代理对象等),难道要修改每一个 Bean 的配置吗?这显然不现实。

解决方案:
这是一种典型的“横切关注点”,Spring 提供了其体系中最强大的扩展点之一:Bean 后置处理器 (BeanPostProcessor)

加入后置处理器后,生命周期演变为更精细的七个步骤:

  • 1.创建 Bean 实例。
  • 2.为 Bean 注入依赖。
  • 3.Bean后置处理器前置处理init-method 执行前的第一个扩展点。
  • 4.执行 Bean 自定义的 init-method
  • 5.Bean后置处理器后置处理init-method 执行后的第二个扩展点,常用于创建代理对象。
  • 6.Bean 处于可用状态。
  • 7.执行 Bean 自定义的 destroy-method

实践验证

文件路径: src/main/java/com/example/spring6/bean/LogBeanPostProcessor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.spring6.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class LogBeanPostProcessor implements BeanPostProcessor {
// 在每个 Bean 的【初始化方法之前】执行
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("-> postProcessBeforeInitialization: " + beanName);
return bean; // 必须返回 bean,否则后续流程会拿到 null
}

// 在每个 Bean 的【初始化方法之后】执行
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("-> postProcessAfterInitialization: " + beanName);
return bean;
}
}

文件路径: src/main/resources/spring-lifecycle.xml (修改)

1
<bean class="com.example.spring6.bean.LogBeanPostProcessor"/>

再次运行上一节的 testLifecycleFiveSteps() 测试。

1
2
3
4
5
6
7
1. 实例化 Bean (调用构造器)
2. Bean 属性赋值 (调用 setter)
-> postProcessBeforeInitialization: lifecycleBean
3. 初始化 Bean (调用自定义 init 方法)
-> postProcessAfterInitialization: lifecycleBean
4. 使用 Bean
5. 销毁 Bean (调用自定义 destroy 方法)

5.3.3. Spring Boot 对比:使用注解定义生命周期

通过在 <bean> 标签上设置 init-methoddestroy-method 属性来指定回调方法,并通过定义 <bean> 来注册后置处理器。

1
2
3
4
5
6
<bean id="lifecycleBean"
class="com.example.spring6.bean.LifecycleBean"
init-method="init"
destroy-method="destroy"/>

<bean class="com.example.spring6.bean.LogBeanPostProcessor"/>

对比总结: @PostConstruct@PreDestroyJSR-250 标准的一部分,这意味着您的代码不直接依赖于 Spring 的特定 API,具有更好的可移植性和通用性。这是在现代 Spring Boot 应用中处理生命周期回调的首选方式


6. [原理剖析] 深入 IoC 容器

摘要: 本章我们将探讨 Spring IoC 容器中两个非常重要的底层话题。首先是经典的面试难题——循环依赖,我们将从原理上剖析 Spring 是如何通过三级缓存解决它的。其次是作为 Spring 设计思想基石的工厂模式。

6.1. Bean 的循环依赖问题 (面试高频)

首先,我们需要理解什么是循环依赖,以及为什么它在 IoC 容器中是一个值得被探讨的问题。

6.1.1. "痛点"场景: 什么是循环依赖?

循环依赖(Circular Dependency),也称为循环引用,指的是两个或多个 Bean 之间相互持有对方的引用,形成一个闭环。最简单的场景就是 A 依赖 B,同时 B 又依赖 A。

A -> B -> A

一个生动的比喻:
想象一下“丈夫”(Husband)和“妻子”(Wife)两个类。Husband 对象有一个 Wife 类型的属性,Wife 对象也有一个 Husband 类型的属性。当 Spring 容器试图创建它们时,就会遇到一个难题:

  • 要创建 Husband 实例,必须先注入一个 Wife 实例。
  • 但要创建 Wife 实例,又必须先注入一个 Husband 实例。这就形成了一个看似无解的“死循环”。

准备代码:
我们将使用这个“夫妻”模型来贯穿本节的实验。

文件路径: src/main/java/com/example/spring6/bean/Husband.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
package com.example.spring6.bean;

import lombok.Data;
import lombok.ToString;

@Data
@ToString(exclude = "wife") // 注意,如果这里不排除wife的话会栈溢出的,这是独属于lombok的依赖循环问题
public class Husband {
private String name;
private Wife wife;
}

文件路径: src/main/java/com/example/spring6/bean/Wife.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
package com.example.spring6.bean;

import lombok.Data;
import lombok.ToString;

@Data
@ToString(exclude = "husband")
public class Wife {
private String name;
private Husband husband;
}

6.1.2. 实践验证:Spring 能否解决循环依赖?

Spring 能否解决循环依赖,取决于 Bean 的作用域和注入方式。让我们通过三个核心场景来一探究竟。

准备配置文件:
文件路径: src/main/resources/spring-dependency.xml (新增配置文件)

场景一: singleton + Setter 注入 (成功)

这是最常见的场景:两个 Bean 都是单例,并且通过 Setter 方法相互注入。

XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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 id="husband" class="com.example.spring6.bean.Husband">
<property name="name" value="张三"/>
<property name="wife" ref="wife"/>
</bean>

<bean id="wife" class="com.example.spring6.bean.Wife">
<property name="name" value="小红"/>
<property name="husband" ref="husband"/>
</bean>
</beans>

测试代码:

1
2
3
4
5
6
7
8
@Test
public void testSingletonSetterDI() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-dependency.xml");
Husband husband = context.getBean("husband", Husband.class);
Wife wife = context.getBean("wife", Wife.class);
System.out.println(husband);
System.out.println(wife);
}
1
2
Husband{name='张三', wifeName=小红}
Wife{name='小红', husbandName=张三}

结论: Spring 完美地解决了单例 Bean 通过 Setter 方式注入的循环依赖问题。

场景二: prototype + Setter 注入 (失败)

如果我们将两个 Bean 的作用域都改为 prototype,情况会如何?

XML 配置 (修改 scope):

1
2
3
4
5
6
7
8
9
<bean id="husband" class="com.example.spring6.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wife"/>
</bean>

<bean id="wife" class="com.example.spring6.bean.Wife" scope="prototype">
<property name="name" value="小红"/>
<property name="husband" ref="husband"/>
</bean>

结论: Spring 无法解决 prototype 作用域 Bean 的循环依赖问题,并会抛出 BeanCurrentlyInCreationException 异常。

场景三: singleton + 构造器注入 (失败)

我们回到 singleton 作用域,但改用构造器进行注入。

准备构造器注入的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// HusbandWithConstructor.java
public class HusbandWithConstructor {
private final String name;
private final WifeWithConstructor wife;
public HusbandWithConstructor(String name, WifeWithConstructor wife) {
this.name = name; this.wife = wife;
}
// ...
}
// WifeWithConstructor.java
public class WifeWithConstructor {
private final String name;
private final HusbandWithConstructor husband;
public WifeWithConstructor(String name, HusbandWithConstructor husband) {
this.name = name; this.husband = husband;
}
// ...
}

XML 配置:

1
2
3
4
5
6
7
8
9
<bean id="husband" class="com.example.spring6.bean.HusbandWithConstructor">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="wife" ref="wife"/>
</bean>

<bean id="wife" class="com.example.spring6.bean.WifeWithConstructor">
<constructor-arg name="name" value="小红"/>
<constructor-arg name="husband" ref="husband"/>
</bean>

结论: Spring 同样无法解决构造器注入方式的循环依赖问题,即使 Bean 是单例的。


6.1.3. 原理浅析:Spring 如何通过“三级缓存”解开死结?

为什么只有“singleton + Setter 注入”的组合能成功?这背后的功臣,就是 Spring IoC 容器内部精巧的“三级缓存”机制。

面试官深度剖析
今天 下午 3:15

刚才的实验结果很有趣。你能量化一下,为什么 Spring 能解决 singleton 和 Setter 的循环依赖吗?

求职者

核心原因在于 Spring 将 Bean 的创建过程分为了两步:1. 实例化2. 属性填充。对于单例 Bean,Spring 可以在完成第一步实例化之后,不等第二步属性填充完成,就将这个半成品的对象提前暴露出去。

很好。那 “提前曝光” 是通过什么机制实现的呢?能具体谈谈吗?

求职者

它是通过一个位于 DefaultSingletonBeanRegistry 类中的三级缓存来实现的。

求职者

一级缓存 : 存放已完成初始化的单例 Bean,是最终的成品区。

求职者

二级缓存 : 存放提前曝光的、未完成属性填充的半成品单例 Bean。

求职者

三级缓存 : 存放能生产半成品 Bean 的工厂 (ObjectFactory)。

能描述一下创建 husband 和 wife 的完整流程吗?

求职者

当然。

求职者
  1. getBean(“husband”),三级缓存中都没有,开始创建。
求职者
  1. new Husband() 实例化一个半成品 husband 对象。
求职者
  1. Spring 并不立即填充其 wife 属性,而是将一个能获取这个半成品 husband 的工厂放入三级缓存。
求职者
  1. 开始填充 husband 的属性,发现它需要 wife。
求职者
  1. getBean(“wife”),三级缓存中都没有,开始创建。
求职者
  1. new Wife() 实例化一个半成品 wife 对象,并同样将其工厂放入三级缓存。
求职者
  1. 开始填充 wife 的属性,发现它需要 husband。
求 postoji
  1. 再次 getBean(“husband”),此时 Spring 从三级缓存中找到了 husband 的工厂,通过工厂拿到半成品 husband 对象,并将其放入二级缓存。
求职者
  1. wife 成功获取到半成品 husband 的引用,完成属性填充和初始化,成为一个成品。随后,wife 被放入一级缓存。
求职者
  1. husband 也成功获取到成品 wife 的引用,完成自己的属性填充和初始化,也成为成品,并被放入一级缓存。循环依赖解决。

非常清晰!那最后请解释一下,为什么构造器注入和 prototype 作用域就不行呢?

求职者

构造器注入 的问题在于,它的实例化和属性填充是在同一步 (new Husband(wife)) 中完成的。在 new 的那一刻就必须拿到 wife 的实例,无法提前曝光一个半成品,所以陷入了死循环。

求职者

prototype 作用域 的问题在于,Spring 容器不对 prototype Bean 进行缓存。每次请求都是全新的,也就没有了可以存放半成品的缓存机制,自然也无法解决循环依赖。


6.2. [背景知识] 工厂设计模式

6.2.1. "痛点"场景: 为什么我们需要工厂?

在面向对象编程的初期,我们创建对象的方式通常非常直接:

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public void fight() {
// 客户端代码直接依赖具体的 Tank 类
Tank tank = new Tank();
tank.attack();

// 如果要换成战斗机,就需要修改客户端代码
Fighter fighter = new Fighter();
fighter.attack();
}
}

这段代码存在一个严重的问题:客户端 (Client) 与具体的产品 (Tank, Fighter) 强耦合

  • 缺乏弹性: 如果将来我们新增一种武器 Dagger,就必须修改 Client 类的代码。
  • 违反开闭原则 (OCP): 我们的系统对“扩展”(增加新武器)是关闭的,因为扩展需要“修改”现有代码。

解决方案:
引入一个“工厂”角色,专门负责创建对象。客户端不再关心对象是如何被创建的,只管向工厂索要即可。这就实现了创建过程与使用过程的分离


6.2.2. 模式概览: 简单工厂 vs 工厂方法

简单工厂模式

这是最基础的工厂模式,它将所有产品的创建逻辑集中在一个工厂类中。

1. 准备代码
文件路径: src/main/java/com/example/spring6/factory/Weapon.java (及实现类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.factory;

// 抽象产品角色
public abstract class Weapon {
public abstract void attack();
}

// 具体产品角色
class Tank extends Weapon {
public void attack() { System.out.println("坦克开炮!"); }
}
class Fighter extends Weapon {
public void attack() { System.out.println("战斗机投弹!"); }
}
class Dagger extends Weapon {
public void attack() { System.out.println("匕首攻击!"); }
}

文件路径: src/main/java/com/example/spring6/factory/WeaponFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.spring6.factory;

// 工厂类角色
public class WeaponFactory {
/**
* 根据武器类型,通过一个静态方法生产武器
* @param weaponType 武器类型
* @return 具体的武器实例
*/
public static Weapon get(String weaponType) {
if ("TANK".equals(weaponType)) {
return new Tank();
} else if ("FIGHTER".equals(weaponType)) {
return new Fighter();
} else if ("DAGGER".equals(weaponType)) {
return new Dagger();
} else {
throw new RuntimeException("不支持的武器类型");
}
}
}

2. 客户端调用
文件路径: src/main/java/com/example/spring6/factory/Client.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.spring6.factory;

public class Client {
public static void main(String[] args) {
// 客户端不再关心 Tank 和 Fighter 是如何 new 出来的
Weapon tank = WeaponFactory.get("TANK");
tank.attack();

Weapon fighter = WeaponFactory.get("FIGHTER");
fighter.attack();
}
}

优点: 实现了创建和使用的分离。
缺点: 违反了开闭原则。每当新增一种武器,我们都必须修改 WeaponFactory 类的 get 方法


工厂方法模式

为了解决简单工厂的 OCP 问题,工厂方法模式将工厂也进行了抽象。

核心思想: 不再由一个全能工厂来创建所有产品,而是为每一种产品都提供一个专门的工厂。

1. 准备代码
我们继续使用上面的 Weapon 产品类。

文件路径: src/main/java/com/example/spring6/factory/WeaponFactory.java (修改为接口)

1
2
3
4
5
6
package com.example.spring6.factory;

// 抽象工厂角色
public interface WeaponFactory {
Weapon get();
}

文件路径: src/main/java/com/example/spring6/factory/TankFactory.java (等具体工厂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.factory;

// 具体工厂角色,专门生产 Tank
class TankFactory implements WeaponFactory {
@Override
public Weapon get() {
return new Tank();
}
}

// 具体工厂角色,专门生产 Fighter
class FighterFactory implements WeaponFactory {
@Override
public Weapon get() {
return new Fighter();
}
}

2. 客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
// 想生产坦克,就使用坦克的工厂
WeaponFactory tankFactory = new TankFactory();
Weapon tank = tankFactory.get();
tank.attack();

// 想生产战斗机,就使用战斗机的工厂
WeaponFactory fighterFactory = new FighterFactory();
Weapon fighter = fighterFactory.get();
fighter.attack();
}
}

优点: 完美遵循了开闭原则。如果现在需要新增 Dagger 武器,我们只需新增一个 DaggerFactory 即可,完全不需要修改任何现有代码。
缺点: 当产品种类非常多时,会导致工厂类的数量急剧增加。


6.2.3. Spring 中的体现: BeanFactoryFactoryBean 的区别

理解了工厂模式后,我们就可以来辨析 Spring 中两个最容易混淆的核心概念了。

面试官深度辨析
今天 下午 4:30

我们经常听说 Spring 是一个大工厂,也常提到 BeanFactoryFactoryBean。它们之间有什么关系和区别?

求职者

这是一个经典问题。虽然名字很像,但它们是两个完全不同维度的概念。

求职者

BeanFactory 是工厂。它是 Spring IoC 容器的顶级接口,是 Spring 框架的“心脏”,是管理所有 Bean 的“总工厂”。它的职责是定义如何获取和管理 Bean,是 Spring IoC 功能的基石。我们通常使用的 ApplicationContext 就是它的一个功能更强大的子接口。

求职者

FactoryBean 是一个 Bean。它是一个可以被总工厂 (BeanFactory) 管理的、比较特殊的 Bean。特殊之处在于,它本身的作用不是给自己用,而是作为一个“小型、可插拔的零件工厂”,去生产另一个 Bean。

那么从容器中获取 Bean 时,这两者有什么不同?

求职者

这是最关键的区别。假设我们有一个 idmyCarEngineFactoryBean

求职者

当我们调用 context.getBean("myCarEngine") 时,我们得到的不是这个 FactoryBean 本身,而是它内部 getObject() 方法返回的那个“发动机”对象。

求- 职者

如果我们确实需要 FactoryBean 这个“机床”本身,需要使用一个 & 符号,像这样调用:context.getBean("&myCarEngine")

求职者

总结来说,BeanFactory 是管理者,FactoryBean 是被管理者中的一个特殊工匠。


重要信息: 后续内容还有重要的注解开发和面向切面AOP的核心,并不是笔记这里不讲了,是我认为他更适合在Spring-boot章节来讲,我们对比了这么多SpringBoot相关的知识点,所以我们与其学习Spring的基础用法,不如直接过渡到Spring-boot中,相信我,在学习了以上内容之后,过度到Spring-boot是最好契机!