# Spring教程 - 4 IoC容器 - 基于XML配置(2)
继续讲解基于XML 的 IoC 容器配置。
# 4.5 bean的作用域
# 1 作用域简介
Spring 中 bean 默认是单例的,我们还可以通过修改 bean 的作用域范围。
常用的作用域有两个,如下:
作用域 | 含义 | 创建对象的时机 |
---|---|---|
singleton(默认) | 单例模式,在整个 IoC 容器中只有一个实例 | 在 IoC 容器初始化的时候创建 |
prototype | 在 IoC 容器中会创建多个实例 | 每次获取 bean 的时候就会创建一个新的实例 |
在 Web 项目中,还有另外几个作用域,只能在 Web 环境下使用,但是不常用,了解一下:
作用域 | 含义 |
---|---|
request | 每次 HTTP 请求会创建一个新的 Bean,在同一次请求中共享,请求结束后销毁 |
session | 与 HTTP Session 生命周期一致,同一个用户 Session 中共享 Bean,不同 Session 独立。 |
application | 与 ServletContext 生命周期一致,整个 Web 应用共享一个 Bean,类似于 singleton,但作用范围是整个 Web 应用。singleton 是单个 IoC 容器中只有一个实例,但是一个项目允许存在多个 IoC 容器。 |
关于session作用域
在以前非前后端分离的项目中,页面是由服务器渲染的,整个 Session 的判断流程如下:
服务器收到请求后,服务器的Web容器(Tomcat/Jetty/Undertow等)会自动生成 Session ID,并通过 Set-Cookie 发给客户端;
浏览器在收到服务器响应头里的
Set-Cookie: JSESSIONID=xxx
时,会自动保存到本地 Cookie 存储;之后访问同域名时,浏览器会自动把这个 Cookie 附加在请求头里;
服务器的Web容器会自动解析请求头的 Cookie,找到 JSESSIONID,并去 Session 管理器里查找对应的 Session 对象,判断是否是同一个 Session。
这个操作流程是不需要我们介入的,是浏览器和 Web 容器自己完成的。
但是现在一般的项目都是前后端分离的项目,会存在跨域问题(什么是跨域? (opens new window)),出于安全考虑,浏览器默认不会跨域携带cookie,Cookie + Session 的方式就需要额外的配置了:
前端必须允许跨域携带 Cookie:
fetch
/axios
要加withCredentials: true
。后端 CORS 配置必须允许携带 Cookie:
Access-Control-Allow-Credentials: true # 必须允许携带 Cookie Access-Control-Allow-Origin: http://前端域名 # 允许哪些前端可以跨域访问
1
2
但是因为前后端普遍存在跨域问题,所以很多项目选择不用传统的 Session,而是采用 JWT
或者 自定义Token
的方式,主要思路就是:
登录时,后端验证成功 → 生成一个 Token(字符串) 返回给前端;
前端浏览器把 Token 存储在本地 localStorage 或 sessionStorage 中;
后面发起请求时,前端在请求头里带上:
Authorization: Bearer <token>
1后端验证 Token,有效就认为是同一个会话。
对于这种处理方式,Spring bean作用域的 session
作用域配置就会失效,此时我们就不要使用 session
作用域了,建议使用 singleton
单例模式就好了,或者需求变态+你牛逼,你可以自定义 Spring 的 bean 作用域。
# 2 作用域演示
在前面配置 bean 的时候,都没有 bean 的作用域,所以 bean 就是单例的。
举个栗子:
<bean id="student" class="com.foooor.hellospring.pojo.Student" scope="singleton">
</bean>
<!-- 不配置scope就是singleton -->
<bean id="student" class="com.foooor.hellospring.pojo.Student">
</bean>
2
3
4
5
6
通过代码获取 bean:
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Student student1 = context.getBean("student", Student.class);
System.out.println(student1);
Student student2 = context.getBean("student", Student.class);
System.out.println(student2);
System.out.println(student1 == student2); // true
2
3
4
5
6
7
- 在上面的代码中获取了两次 bean,但是打印两个 bean,发现他们的地址是一样的,所以其实是同一个对象。
修改bean的作用域,scope
修改为 prototype
:
<bean id="student" class="com.foooor.hellospring.pojo.Student" scope="prototype">
</bean>
2
再次通过代码获取bean :
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Student student1 = context.getBean("student", Student.class);
Student student2 = context.getBean("student", Student.class);
System.out.println(student1 == student2); // false
2
3
4
- 你会发现每次获取bean,都是不同的。
我们在实际的开发中,一般用单例的较多。除非有特殊的需求,才使用 prototype
,此时每次都是新的实例。
# 4.6 bean的生命周期
bean 的声明周期,就是 bean 从创建到销毁的过程。
在 Spring Ioc 容器中,bean 的生命周期如下:
- 调用类的无参构造方法创建对象;
- 通过依赖注入,调用bean的setter,设置属性;
- 调用 BeanPostProcessor 的前置处理;
- 调用bean指定的初始化方法;
- 调用 BeanPostProcessor 的后置处理;
- bean就绪,可以使用了!
- 销毁bean对象,调用指定的销毁方法;
下面就演示一下上面的调用过程。
准备bean类,还是 Student.java
package com.foooor.hellospring.pojo;
public class Student {
private String name;
public Student() {
System.out.println("1. 调用 Student 无参构造方法");
}
public void initMethod() {
System.out.println("4. 调用 Student initMethod 方法");
}
public void destroyMethod() {
System.out.println("7. 调用 Student destroyMethod 方法");
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("2. 调用 Student setName 方法");
this.name = name;
}
}
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
- 在上面的类中,有两个自定义的方法
initMethod
和destroyMethod
,待会我们配置bean的时候指定调用这两个方法。在实际开发的时候,我们可以通过指定初始化和销毁的方法,可以让bean在创建的时候和销毁的时候执行一些操作,例如初始化资源或释放资源。
配置bean:
<bean id="student" class="com.foooor.hellospring.pojo.Student" init-method="initMethod" destroy-method="destroyMethod">
<property name="name" value="张三" />
</bean>
2
3
- 在上面通过
init-method
指定调用 bean 中定义的初始化方法,可以在创建对象后,执行一些初始化的操作;同样通过destroy-method
属性指定了销毁的方法,可以在销毁bean的时候执行一些操作。方法名是自定义的。
上面在描述生命周期过程中有一个 BeanPostProcessor
,这个 BeanPostProcessor
是 Spring 提供的一个扩展接口,允许我们在 Spring 容器实例化 bean时,在依赖注入之后,调用初始化方法( init-method
)的前后,对 bean 进行额外的处理。
我们需要在项目中,创建一个类,实现 BeanPostProcessor
接口:
package com.foooor.hellospring.config;
import com.foooor.hellospring.pojo.Student;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;
public class MyBeanPostProcessor implements BeanPostProcessor {
@Nullable
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("3. 调用BeanPostProcessor的前置处理,beanName:" + beanName);
// 这里判断是Student实例,可以修改bean的属性
if (bean instanceof Student) {
// ((Student) bean).setName("李四");
}
return bean;
}
@Nullable
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("5. 调用BeanPostProcessor的后置处理,beanName:" + beanName);
return bean;
}
}
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
- 在类中需要重写前置和后置处理两个方法,这个
BeanPostProcessor
会在每个 bean 创建的时候都会执行。 - 我们可以在方法中判断当前是哪个 bean,从而对 bean 进行一些修改。
创建完 BeanPostProcessor
,我们需要将其添加到 IoC 容器中,所以需要在 bean.xml
中进行配置一下:
<!-- 配置BeanPostProcessor -->
<bean id="myBeanPostProcessor" class="com.foooor.hellospring.config.MyBeanPostProcessor" />
2
下面就编写一个测试方法,来测试一下 bean 的生命周期:
package com.foooor.hellospring;
import com.foooor.hellospring.pojo.Student;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class StudentTest {
@Test
public void testStudent() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Student student = context.getBean("student", Student.class);
context.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 在上面测试的时候,需要使用
ClassPathXmlApplicationContext
才有close()
方法 ,此时会关闭 IoC 容器,关闭之前会调用 bean 的销毁方法。
执行结果如下:
1. 调用 Student 无参构造方法
2. 调用 Student setName 方法
3. 调用BeanPostProcessor的前置处理,beanName:student
4. 调用 Student initMethod 方法
5. 调用BeanPostProcessor的后置处理,beanName:student
张三
2025-08-30 13:32:02 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@19976a65, started on Fri Aug 30 13:32:02 CST 2025
7. 调用 Student destroyMethod 方法
2025-08-30 13:32:02 [main] DEBUG o.s.b.f.s.DisposableBeanAdapter - Custom destroy method 'destroyMethod' on bean with name 'student' completed
2
3
4
5
6
7
8
9
# 4.7 引入其他bean.xml
当 bean 很多的时候,使用单个 bean.xml
会显得臃肿难维护,我们可以将配置文件 bean.xml拆分成多个。
例如在 SpringMVC 的框架中,一般整个项目会按照如下的结构来划分层级:
我们可以将每一层放到一个 bean.xml 中,当然你也可以按照模块来划分。
比如你有以下几个配置文件:
applicationContext.xml
(主配置文件)dao-beans.xml
(DAO 层 Bean)service-beans.xml
(Service 层 Bean)controller-beans.xml
(Controller 层 Bean)
可以在主配置文件中引入其它文件即可:
<!-- applicationContext.xml -->
<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">
<!-- 导入其他配置文件 -->
<import resource="dao-beans.xml"/>
<import resource="service-beans.xml"/>
<import resource="controller-beans.xml"/>
</beans>
2
3
4
5
6
7
8
9
10
11
12
13
这样在加载 applicationContext.xml
时,Spring 会自动把其他配置也加载进来。
我们也在代码中同时加载多个 XML:
举个栗子:
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"dao-beans.xml", "service-beans.xml", "controller-beans.xml"});
2
这样就可以一次性加载多个配置文件。
# 4.8 引入外部属性文件
有时候为了让配置文件结构更清晰,会将一些配置抽取到单独的文件中。
最常用的就是将数据库连接信息放到 *.properties
文件中,然后在 bean.xml 中引入配置文件。
举个栗子:
我们现在就实现使用 Spring 获取阿里巴巴 druid
数据源,然后获取数据库数据,在 bean.xml 中引入 *.properties
文件中的配置。
# 1 引入依赖
首先在项目的 pom.xml 中引入 MySQL 数据源和 druid
连接池的依赖:
<!-- 引入mysql驱动依赖,用于连接mysql数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 引入druid依赖,提供数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.27</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
# 2 创建外部属性文件
在 resources
目录下创建 jdbc.properties
配置文件,在配置文件中配置数据库相关的连接信息:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
jdbc.username=root
jdbc.password=123456
2
3
4
- 这里要准备一下数据库,以及数据库中的表和数据。
# 3 配置bean
在 bean.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"
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
https://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="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
</beans>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 首先要引入外部属性文件,启用属性占位符。
- 然后配置数据源类,类
com.alibaba.druid.pool.DruidDataSource
是 druid 提供的数据源类,使用 spring 创建该类的实例,用于数据库的连接操作,使用${}
从配置文件中读取对应的配置。 - 另外,XML 头部
<beans>
标签中需要添加context
的相关约束。
# 4 测试
编写测试类测试一下,获取数据源 bean 实例,然后获取数据库连接,就可以执行 SQL 获取数据了。
如下:
package com.foooor.hellospring;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DataSourceTest {
@Test
public void testDataSource() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
DataSource dataSource = context.getBean(DataSource.class);
// 2. 获取连接
try (Connection conn = dataSource.getConnection()) {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM tb_user");
while (rs.next()) {
System.out.println(rs.getString("username"));
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
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
- 数据库中是准备了表
tb_user
,并且有 username 字段,这里你可以在数据库中创建相应的表和字段。 - 然后通过上面的测试代码可以获取到
tb_user
表中所有的数据,并打印 username 字段的值。
# 4.9 FactoryBean
在前面我们在 bean.xml 中配置什么bean,Spring 就帮我们生成什么 bean。
而 FactoryBean 是一个工厂 bean,配置它不会生成它的 bean,而是用来生成它要生产的对象。
一般用来整合第三方的框架的时候才使用。
下面演示一下。
# 1 创建FactoryBean
例如我创建一个 StudentFactoryBean,实现 FactoryBean
接口:
package com.foooor.hellospring.factory;
import com.foooor.hellospring.pojo.Student;
import org.springframework.beans.factory.FactoryBean;
public class StudentFactoryBean implements FactoryBean<Student> {
@Override
public Student getObject() throws Exception {
return new Student();
}
@Override
public Class<?> getObjectType() {
return Student.class;
}
@Override
public boolean isSingleton() {
return true; // 配置Student是否为单例
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 通过
getObject()
方法返回 FactoryBean 要创建的对象; - 可是通过
isSingleton()
配置创建的对象是否是单例的。
# 2 配置FactoryBean
在 xml 中配置 FactoryBean,和普通的 bean 是一样的。
<!-- 配置FactoryBean -->
<bean id="studentFactoryBean" class="com.foooor.hellospring.factory.StudentFactoryBean"/>
2
# 3 获取bean
这样就可以通过 FactoryBean 来获取创建的对象了:
ApplicationContext context = new ClassPathXmlApplicationContext("factory-bean.xml");
Student student1 = context.getBean("studentFactoryBean", Student.class);
Student student2 = context.getBean("studentFactoryBean", Student.class);
System.out.println(student1 == student2);
2
3
4
- 需要注意:在上面是通过
studentFactoryBean
获取的 Student 对象。 - 因为配置的是单例的,所以多次获取到的 Student 对象是同一个实例。
# 4.10 基于XML的自动装配
自动装备就是 bean 中的属性自动注入,在 xml 中不需要针对每个属性进行单独的配置。
举个栗子:
我们在 Controller 中去调用 Service,在 Service 中去调用 Dao,这样的结构来实现自动装配。
# 1 不使用Spring的实现
如果没有 Spring,那么实现的代码大概如下:
UserController.java
package com.foooor.hellospring.controller;
import com.foooor.hellospring.service.UserService;
public class UserController {
/**
* 获取用户信息
*/
public void getUser() {
System.out.println("controller 获取用户信息");
UserService userService = new UserService();
userService.getUser();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
UserService.java
package com.foooor.hellospring.service;
import com.foooor.hellospring.dao.UserDao;
public class UserService {
/**
* 获取用户
*/
public void getUser() {
System.out.println("service 获取用户信息");
UserDao userDao = new UserDao();
userDao.getUser();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
UserDao.java
package com.foooor.hellospring.dao;
public class UserDao {
/**
* 获取用户
*/
public void getUser() {
System.out.println("dao 获取用户信息");
}
}
2
3
4
5
6
7
8
9
10
- 我们需要在调用对象方法之前,先自己通过 new 创建对象实例。
# 2 使用Spring实现
而如果使用 Spring 来实现的话,只需要定义属性和 setter 方法即可,Spring 会帮我们完成注入。
这里有一个问题需要解释一下,在实际的 Spring 项目中,一般采用 Controller → Service(接口 + 实现类) → DAO(接口 + 实现类)
的分层架构,并通过接口 + 实现类的方式组织代码,而不是直接使用 Service 和 Dao 的实现类。
一个是设计原则的问题,通过面向接口编程,降低代码耦合,Controller 调用 Service 接口,而不关心具体的实现,如果换成了别的实现类,不需要修改 Controller,这样便于扩展。另外,Spring AOP 默认使用 JDK 动态代理
,而 JDK 动态代理只能基于接口实现,当然如果你不使用接口,Spring 会自动使用 CGLIB 来实现动态代理,所以你不使用接口也是可以的。
基于上面的原因,Service 和 Dao 都是使用 接口 + 实现类 的方式来定义的。
那么定义类如下:
UserController.java
package com.foooor.hellospring.controller;
import com.foooor.hellospring.service.IUserService;
public class UserController {
private IUserService userService;
public void setUserService(IUserService userService) {
this.userService = userService;
}
/**
* 获取用户信息
*/
public void getUser() {
System.out.println("controller 获取用户信息");
userService.getUser();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- userService 使用接口类型。
IUserService.java
package com.foooor.hellospring.service;
public interface IUserService {
/**
* 获取用户信息
*/
void getUser();
}
2
3
4
5
6
7
8
UserServiceImpl.java
package com.foooor.hellospring.service.impl;
import com.foooor.hellospring.dao.IUserDao;
import com.foooor.hellospring.service.IUserService;
public class UserServiceImpl implements IUserService {
private IUserDao userDao;
public void setUserDao(IUserDao userDao) {
this.userDao = userDao;
}
/**
* 获取用户
*/
public void getUser() {
System.out.println("service 获取用户信息");
userDao.getUser();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 实现 IUserService 接口。
IUserDao.java
package com.foooor.hellospring.dao;
public interface IUserDao {
/**
* 获取用户
*/
void getUser();
}
2
3
4
5
6
7
8
UserDaoImpl.java
package com.foooor.hellospring.dao.impl;
import com.foooor.hellospring.dao.IUserDao;
public class UserDaoImpl implements IUserDao {
/**
* 获取用户
*/
public void getUser() {
System.out.println("dao 获取用户信息");
}
}
2
3
4
5
6
7
8
9
10
11
- 实现 IUserDao 接口。
配置 bean.xml :
<!-- 配置UserController -->
<bean id="userController" class="com.foooor.hellospring.controller.UserController" autowire="byType">
</bean>
<!-- 配置UserServiceImpl -->
<bean id="userServiceImpl" class="com.foooor.hellospring.service.impl.UserServiceImpl" autowire="byType">
</bean>
<!-- 配置UserDaoImpl -->
<bean id="userDaoImpl" class="com.foooor.hellospring.dao.impl.UserDaoImpl" autowire="byType">
</bean>
2
3
4
5
6
7
8
9
10
11
- 在 xml 中配置了三个bean,同时添加
autowire
表示自动装配,byType
表示是通过类型自动装配。这样三个 bean 中不用再通过<property>
来注入 bean,会自动通过类型自动装配。
autowire
的常用取值如下:
byType
:根据IoC容器中的类型,为属性自动赋值。如果找不到对应类型的 bean,则该属性不装配,为null;如果同一个类型有多个bean,则会报错:NoUniqueBeanDefinitionExcepton
。byName
:根据属性名,在 IoC 容器中寻找 id 与之相同的 bean 进行赋值。如果找不到对应类型的 bean,则该属性不装配,为null。
测试:
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserController userController = context.getBean(UserController.class);
userController.getUser();
2
3
执行结果如下:
controller 获取用户信息
service 获取用户信息
dao 获取用户信息
2
3