# 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 的判断流程如下:

  1. 服务器收到请求后,服务器的Web容器(Tomcat/Jetty/Undertow等)会自动生成 Session ID,并通过 Set-Cookie 发给客户端;

  2. 浏览器在收到服务器响应头里的 Set-Cookie: JSESSIONID=xxx 时,会自动保存到本地 Cookie 存储;

  3. 之后访问同域名时,浏览器会自动把这个 Cookie 附加在请求头里;

  4. 服务器的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 的方式,主要思路就是:

  1. 登录时,后端验证成功 → 生成一个 Token(字符串) 返回给前端;

  2. 前端浏览器把 Token 存储在本地 localStorage 或 sessionStorage 中;

  3. 后面发起请求时,前端在请求头里带上:

    Authorization: Bearer <token>
    
    1
  4. 后端验证 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>
1
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
1
2
3
4
5
6
7
  • 在上面的代码中获取了两次 bean,但是打印两个 bean,发现他们的地址是一样的,所以其实是同一个对象。

修改bean的作用域,scope 修改为 prototype

<bean id="student" class="com.foooor.hellospring.pojo.Student" scope="prototype">
</bean>
1
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
1
2
3
4
  • 你会发现每次获取bean,都是不同的。

我们在实际的开发中,一般用单例的较多。除非有特殊的需求,才使用 prototype ,此时每次都是新的实例。

# 4.6 bean的生命周期

bean 的声明周期,就是 bean 从创建到销毁的过程。

在 Spring Ioc 容器中,bean 的生命周期如下:

  1. 调用类的无参构造方法创建对象;
  2. 通过依赖注入,调用bean的setter,设置属性;
  3. 调用 BeanPostProcessor 的前置处理;
  4. 调用bean指定的初始化方法;
  5. 调用 BeanPostProcessor 的后置处理;
  6. bean就绪,可以使用了!
  7. 销毁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;
    }
}
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
  • 在上面的类中,有两个自定义的方法 initMethoddestroyMethod ,待会我们配置bean的时候指定调用这两个方法。在实际开发的时候,我们可以通过指定初始化和销毁的方法,可以让bean在创建的时候和销毁的时候执行一些操作,例如初始化资源或释放资源。

配置bean:

<bean id="student" class="com.foooor.hellospring.pojo.Student" init-method="initMethod" destroy-method="destroyMethod">
    <property name="name" value="张三" />
</bean>
1
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;
    }

}
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
  • 在类中需要重写前置和后置处理两个方法,这个 BeanPostProcessor 会在每个 bean 创建的时候都会执行。
  • 我们可以在方法中判断当前是哪个 bean,从而对 bean 进行一些修改。

创建完 BeanPostProcessor ,我们需要将其添加到 IoC 容器中,所以需要在 bean.xml 中进行配置一下:

<!-- 配置BeanPostProcessor -->
<bean id="myBeanPostProcessor" class="com.foooor.hellospring.config.MyBeanPostProcessor" />
1
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();
    }
}
1
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
1
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>
1
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"});
1
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>
1
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
1
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>
1
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);
        }
    }

}
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
  • 数据库中是准备了表 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是否为单例
    }

}
1
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"/>
1
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);
1
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();
    }
}
1
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();
    }
}
1
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 获取用户信息");
    }
}
1
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();
    }
}
1
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();
}
1
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();
    }
}
1
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();
}
1
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 获取用户信息");
    }
}
1
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>
1
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();
1
2
3

执行结果如下:

controller 获取用户信息
service 获取用户信息
dao 获取用户信息
1
2
3