Skip to content

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. 服务器收到请求后,当服务器端的代码**第一次调用 **request.getSession() 时,Web容器(例如Tomcat)会创建Session对象并生成 Session ID,通过 Set-Cookie 发给客户端;
  2. 浏览器在收到服务器响应头里的 Set-Cookie: JSESSIONID=xxx 时,会自动保存到本地 Cookie 存储;
  3. 之后访问同域名时,浏览器会自动把这个 Cookie 附加在请求头里;
  4. 服务器的Web容器会自动解析请求头的 Cookie,找到 JSESSIONID,并去 Session 管理器里查找对应的 Session 对象,判断是否是同一个 Session。

这个操作流程是不需要我们介入的,是浏览器和 Web 容器自己完成的。


但是现在一般的项目都是前后端分离的项目,会存在跨域问题(什么是跨域?),出于安全考虑,浏览器默认不会跨域携带cookie,Cookie + Session 的方式就需要额外的配置了:

  • 前端必须允许跨域携带 Cookie:fetch/axios 要加 withCredentials: true

  • 后端 CORS 配置必须允许携带 Cookie:

    properties
    Access-Control-Allow-Credentials: true         # 必须允许携带 Cookie
    Access-Control-Allow-Origin: http://前端域名    # 允许哪些前端可以跨域访问

但是因为前后端普遍存在跨域问题,所以很多项目选择不用传统的 Session,而是采用 JWT 或者 自定义Token 的方式,主要思路就是:

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

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

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

    properties
    Authorization: Bearer <token>
  4. 后端验证 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 的生命周期如下:

  1. 调用类的无参构造方法创建对象;
  2. 通过依赖注入,调用bean的setter,设置属性;
  3. 调用 BeanPostProcessor 的前置处理;
  4. 调用bean指定的初始化方法;
  5. 调用 BeanPostProcessor 的后置处理;
  6. bean就绪,可以使用了!
  7. 销毁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;
    }
}
  • 在上面的类中,有两个自定义的方法 initMethoddestroyMethod ,待会我们配置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' completed

4.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 获取用户信息