Appearance
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 的判断流程如下:
- 服务器收到请求后,当服务器端的代码**第一次调用 **
request.getSession()时,Web容器(例如Tomcat)会创建Session对象并生成 Session ID,通过 Set-Cookie 发给客户端; - 浏览器在收到服务器响应头里的
Set-Cookie: JSESSIONID=xxx时,会自动保存到本地 Cookie 存储; - 之后访问同域名时,浏览器会自动把这个 Cookie 附加在请求头里;
- 服务器的Web容器会自动解析请求头的 Cookie,找到 JSESSIONID,并去 Session 管理器里查找对应的 Session 对象,判断是否是同一个 Session。
这个操作流程是不需要我们介入的,是浏览器和 Web 容器自己完成的。
但是现在一般的项目都是前后端分离的项目,会存在跨域问题(什么是跨域?),出于安全考虑,浏览器默认不会跨域携带cookie,Cookie + Session 的方式就需要额外的配置了:
前端必须允许跨域携带 Cookie:
fetch/axios要加withCredentials: true。后端 CORS 配置必须允许携带 Cookie:
propertiesAccess-Control-Allow-Credentials: true # 必须允许携带 Cookie Access-Control-Allow-Origin: http://前端域名 # 允许哪些前端可以跨域访问
但是因为前后端普遍存在跨域问题,所以很多项目选择不用传统的 Session,而是采用 JWT 或者 自定义Token 的方式,主要思路就是:
登录时,后端验证成功 → 生成一个 Token(字符串) 返回给前端;
前端浏览器把 Token 存储在本地 localStorage 或 sessionStorage 中;
后面发起请求时,前端在请求头里带上:
propertiesAuthorization: Bearer <token>后端验证 Token,有效就认为是同一个会话。
对于这种处理方式,Spring bean作用域的 session 作用域配置就会失效,此时我们就不要使用 session 作用域了,建议使用 singleton 单例模式就好了,或者需求变态+你牛逼,你可以自定义 Spring 的 bean 作用域。
2 作用域演示
在前面配置 bean 的时候,都没有 bean 的作用域,所以 bean 就是单例的。
举个栗子:
xml
<bean id="student" class="com.foooor.hellospring.pojo.Student" scope="singleton">
</bean>
<!-- 不配置scope就是singleton -->
<bean id="student" class="com.foooor.hellospring.pojo.Student">
</bean>通过代码获取 bean:
java
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- 在上面的代码中获取了两次 bean,但是打印两个 bean,发现他们的地址是一样的,所以其实是同一个对象。
修改bean的作用域,scope 修改为 prototype :
xml
<bean id="student" class="com.foooor.hellospring.pojo.Student" scope="prototype">
</bean>再次通过代码获取bean :
java
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- 你会发现每次获取bean,都是不同的。
我们在实际的开发中,一般用单例的较多。除非有特殊的需求,才使用 prototype ,此时每次都是新的实例。
4.6 bean的生命周期
bean 的声明周期,就是 bean 从创建到销毁的过程。
在 Spring Ioc 容器中,bean 的生命周期如下:
- 调用类的无参构造方法创建对象;
- 通过依赖注入,调用bean的setter,设置属性;
- 调用 BeanPostProcessor 的前置处理;
- 调用bean指定的初始化方法;
- 调用 BeanPostProcessor 的后置处理;
- bean就绪,可以使用了!
- 销毁bean对象,调用指定的销毁方法;
下面就演示一下上面的调用过程。
准备bean类,还是 Student.java
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;
}
}- 在上面的类中,有两个自定义的方法
initMethod和destroyMethod,待会我们配置bean的时候指定调用这两个方法。在实际开发的时候,我们可以通过指定初始化和销毁的方法,可以让bean在创建的时候和销毁的时候执行一些操作,例如初始化资源或释放资源。
配置bean:
xml
<bean id="student" class="com.foooor.hellospring.pojo.Student" init-method="initMethod" destroy-method="destroyMethod">
<property name="name" value="张三" />
</bean>- 在上面通过
init-method指定调用 bean 中定义的初始化方法,可以在创建对象后,执行一些初始化的操作;同样通过destroy-method属性指定了销毁的方法,可以在销毁bean的时候执行一些操作。方法名是自定义的。
上面在描述生命周期过程中有一个 BeanPostProcessor,这个 BeanPostProcessor 是 Spring 提供的一个扩展接口,允许我们在 Spring 容器实例化 bean时,在依赖注入之后,调用初始化方法( init-method)的前后,对 bean 进行额外的处理。
我们需要在项目中,创建一个类,实现 BeanPostProcessor 接口:
java
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;
}
}- 在类中需要重写前置和后置处理两个方法,这个
BeanPostProcessor会在每个 bean 创建的时候都会执行。 - 我们可以在方法中判断当前是哪个 bean,从而对 bean 进行一些修改。
创建完 BeanPostProcessor ,我们需要将其添加到 IoC 容器中,所以需要在 bean.xml 中进行配置一下:
xml
<!-- 配置BeanPostProcessor -->
<bean id="myBeanPostProcessor" class="com.foooor.hellospring.config.MyBeanPostProcessor" />下面就编写一个测试方法,来测试一下 bean 的生命周期:
java
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();
}
}- 在上面测试的时候,需要使用
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' completed4.7 引入其他bean.xml
当 bean 很多的时候,使用单个 bean.xml 会显得臃肿难维护,我们可以将配置文件 bean.xml拆分成多个。
例如在 SpringMVC 的框架中,一般整个项目会按照如下的结构来划分层级:

分层使代码结构更清晰,Controller负责响应用户请求,Service 处理处理业务逻辑,Dao负责操作数据库。
我们可以将每一层放到一个 bean.xml 中,当然你也可以按照模块来划分。
比如你有以下几个配置文件:
applicationContext.xml(主配置文件)dao-beans.xml(DAO 层 Bean)service-beans.xml(Service 层 Bean)controller-beans.xml(Controller 层 Bean)
可以在主配置文件中引入其它文件即可:
xml
<!-- 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>这样在加载 applicationContext.xml 时,Spring 会自动把其他配置也加载进来。
我们也在代码中同时加载多个 XML:
举个栗子:
java
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"dao-beans.xml", "service-beans.xml", "controller-beans.xml"});这样就可以一次性加载多个配置文件。
4.8 引入外部属性文件
有时候为了让配置文件结构更清晰,会将一些配置抽取到单独的文件中。
最常用的就是将数据库连接信息放到 *.properties 文件中,然后在 bean.xml 中引入配置文件。
举个栗子:
我们现在就实现使用 Spring 获取阿里巴巴 druid 数据源,然后获取数据库数据,在 bean.xml 中引入 *.properties 文件中的配置。
1 引入依赖
首先在项目的 pom.xml 中引入 MySQL 数据源和 druid 连接池的依赖:
xml
<!-- 引入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 创建外部属性文件
在 resources 目录下创建 jdbc.properties 配置文件,在配置文件中配置数据库相关的连接信息:
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- 这里要准备一下数据库,以及数据库中的表和数据。
3 配置bean
在 bean.xml 中进行一下配置:
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>- 首先要引入外部属性文件,启用属性占位符。
- 然后配置数据源类,类
com.alibaba.druid.pool.DruidDataSource是 druid 提供的数据源类,使用 spring 创建该类的实例,用于数据库的连接操作,使用${}从配置文件中读取对应的配置。 - 另外,XML 头部
<beans>标签中需要添加context的相关约束。
4 测试
编写测试类测试一下,获取数据源 bean 实例,然后获取数据库连接,就可以执行 SQL 获取数据了。
如下:
java
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);
}
}
}- 数据库中是准备了表
tb_user,并且有 username 字段,这里你可以在数据库中创建相应的表和字段。 - 然后通过上面的测试代码可以获取到
tb_user表中所有的数据,并打印 username 字段的值。
4.9 FactoryBean
在前面我们在 bean.xml 中配置什么bean,Spring 就帮我们生成什么 bean。
而 FactoryBean 是一个工厂 bean,配置它不会生成它的 bean,而是用来生成它要生产的对象。
一般用来整合第三方的框架的时候才使用。
下面演示一下。
1 创建FactoryBean
例如我创建一个 StudentFactoryBean,实现 FactoryBean 接口:
java
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是否为单例
}
}- 通过
getObject()方法返回 FactoryBean 要创建的对象; - 可是通过
isSingleton()配置创建的对象是否是单例的。
2 配置FactoryBean
在 xml 中配置 FactoryBean,和普通的 bean 是一样的。
xml
<!-- 配置FactoryBean -->
<bean id="studentFactoryBean" class="com.foooor.hellospring.factory.StudentFactoryBean"/>3 获取bean
这样就可以通过 FactoryBean 来获取创建的对象了:
java
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);- 需要注意:在上面是通过
studentFactoryBean获取的 Student 对象。 - 因为配置的是单例的,所以多次获取到的 Student 对象是同一个实例。
4.10 基于XML的自动装配
自动装备就是 bean 中的属性自动注入,在 xml 中不需要针对每个属性进行单独的配置。
举个栗子:
我们在 Controller 中去调用 Service,在 Service 中去调用 Dao,这样的结构来实现自动装配。

1 不使用Spring的实现
如果没有 Spring,那么实现的代码大概如下:
UserController.java
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();
}
}UserService.java
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();
}
}UserDao.java
java
package com.foooor.hellospring.dao;
public class UserDao {
/**
* 获取用户
*/
public void getUser() {
System.out.println("dao 获取用户信息");
}
}- 我们需要在调用对象方法之前,先自己通过 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
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();
}
}- userService 使用接口类型。
IUserService.java
java
package com.foooor.hellospring.service;
public interface IUserService {
/**
* 获取用户信息
*/
void getUser();
}UserServiceImpl.java
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();
}
}- 实现 IUserService 接口。
IUserDao.java
java
package com.foooor.hellospring.dao;
public interface IUserDao {
/**
* 获取用户
*/
void getUser();
}UserDaoImpl.java
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 获取用户信息");
}
}- 实现 IUserDao 接口。
配置 bean.xml :
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>- 在 xml 中配置了三个bean,同时添加
autowire表示自动装配,byType表示是通过类型自动装配。这样三个 bean 中不用再通过<property>来注入 bean,会自动通过类型自动装配。
autowire 的常用取值如下:
byType:根据IoC容器中的类型,为属性自动赋值。如果找不到对应类型的 bean,则该属性不装配,为null;如果同一个类型有多个bean,则会报错:NoUniqueBeanDefinitionExcepton。byName:根据属性名,在 IoC 容器中寻找 id 与之相同的 bean 进行赋值。如果找不到对应类型的 bean,则该属性不装配,为null。
测试:
java
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserController userController = context.getBean(UserController.class);
userController.getUser();执行结果如下:
controller 获取用户信息
service 获取用户信息
dao 获取用户信息