掘金 后端 ( ) • 2024-04-05 18:30

Spring Framework的主要功能是用来存储和读取Bean,Bean于Spring框架的地位可见一斑。本节的目的就是从作用域和生命周期的角度更加深入了解一下Bean对象。

1. Bean的作用域

首先我们来学习一下Bean的作用域,源码位置:bean_scope

1.1 作用域的定义

作用域一词在学习JavaSE的时候就有接触过,指的是源代码中定义变量的某个区域,与JavaSE中定义的作用域不同,Bean 的作用域是指Bean在Spring整个框架中的某种行为模式,比如Spring中默认的Ben作用域:singleton单例作用域,指的就是在整个Spring中只有一份。

Spring有6种作用域:

  1. singleton: 单例作用域
  2. prototype: 原型作用域(多例)
  3. request: 请求作用域
  4. session: 会话作用域
  5. application: 全局作用域
  6. websocket: HTTP WebSocket 作用域

后 4 种状态是Spring MVC 中的值, 在普通的 Spring 项目中只有前两种

【问题】默认作用域的不足

Spring中Bean的默认作用域是singleton单例作用域,在业务中可能遇到下面这种场景:

首先在名为model的包中定义User,用来传输User的数据:

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

创建一个bean包,并定义一个UserBean,用于初始化User并将User存入Spring容器中:

@Component
public class UserBean {

    @Bean(name = "user")
    public User getUserBean() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(20);
        return user;
    }
}

controller包中创建UserController1UserController2分别代表两个业务

程序员A在业务中需要临时创建临时的User对象并且需要进行修改,出于无心之举,创建的临时对象user2直接浅拷贝了Bean,于是修改了Bean的数据,然后将代码推送到仓库:

@Controller
public class UserController1 {
    @Autowired
    private User user;

    public void doController() {
        System.out.println("Do UserController1");
        System.out.println("从Spring中取出user:" + user);
        //创建的临时对象user2直接浅拷贝了Bean
        User user2 = user;
        user2.setName("lisi");
        user2.setAge(18);
        System.out.println("修改后的数据" + user2);
    }
}

程序员B拉取了新的代码,并且要将 UserController2 给其他业务部门交付,并展示,预期中User的数据是User{name='zhangsan', age=20}

@Controller
public class UserController2 {
    @Autowired
    private User user;

    public void  doController() {
        System.out.println("Do UserController2");
        System.out.println("从Spring中取出user:" + user);
    }
}

程序员B展示他的业务代码并一顿鼓吹他是如何拿到这么一个User{name='zhangsan', age=20}的数据:

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("bean-scope.xml");
    //UserController1业务
    UserController1 userController1 = context.getBean("userController1", UserController1.class);
    userController1.doController();
    System.out.println("===========================");
    //UserController2业务
    UserController2 userController2 = context.getBean("userController2", UserController2.class);
    userController2.doController();
}

但是他并不知道代码被修改,在展示的过程中直接傻眼了,运行结果如下:

Do UserController1
从Spring中取出user:User{name='zhangsan', age=20}
修改后的数据User{name='lisi', age=18}
===========================
Do UserController2
从Spring中取出user:User{name='lisi', age=18}

1.2 Bean的 6 种作用域

上述问题的解决方案就是设置 Bean 的作用域,在说明设置 Bean 的作用域的方法前,先来介绍下 Bean 的 6 种作用域的定义。

1) singleton作用域(单例)

官方说明:(Default)Scopes a single bean definition to a single object instance for each Spring IoC container.

描述: 该作用域下的Bean在IoC容器中只存在一个实例:singleton作用域是Spring中Bean的默认作用域,singleton作用域指的是单例作用域,这里的单例和常规设计模式中的单例又稍微一些不一样,常规设计模式中的单例模式指的是一个类只能对应唯一的实例对象;而 Bean 的单例作用域表示的是 Bean 对象在Spring中只有一份,它是全局共享的

场景: 通常无状态的Bean使用该作用域,无状态表示Bean的属性不需要更新。

2) prototype作用域 (原型)

官方说明: Scopes a single bean definition to any number of object instances.

描述: 每次该作用域下的获取Bean的请求都会创建新的实例:这种方式对比singleton作用域,会频繁创建实例,因此开销较大。

场景: 通常有状态的Bean使用该作用域,如前面所提到的例子,就可以用到该作用域。

3) request作用域

官方说明: Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTPrequest has its own instance of a bean created off the back of a single bean definition. Only valid in thecontext of a web-aware Spring ApplicationContext.

描述: 每次HTTP请求会创建新的Bean实例,它的生命周期和一个HTTP的生命周期相同。

场景: 一次HTTP的请求和响应的共享Bean,如在处理一个HTTP请求的过程中,可能有多个组件或步骤需要访问或修改同一份数据,此时就可以用上request作用域。

备注: 限定Spring MVC中使用

4) session作用域

官方说明: Scopes a single bean definition to the lifecycle of an HTTP Session. Only a web-aware Spring ApplicationContext.

描述: 在一个http session(会话)中,定义一个Bean实例,它的生命周期和会话的生命周期相同。

场景: 用户会话的共享Bean,常用于在用户的整个会话过程中共享数据,比如:记录一个用户的登录信息。

备注: 限定Spring MVC中使用

5) application作用域

官方说明: Scopes a single bean definition to the lifecycle of a ServletContext. Only valid in the context of a web-aware Spring ApplicationContext.

描述: 一个http servlet context共享的Bean,它们在服务器开始执行服务时创建,并持续存在直到服务器关闭。

场景: Web应用的上下文信息,比如:记录一个应用的共享信息

备注: 限定Spring MVC中使用

6) websocket作用域

官方说明: Scopes a single bean definition to the lifecycle of a WebSocket. Only valid in the context of web-aware Spring ApplicationContext

描述: 在一个HTTP WebSocket的生命周期中,定义一个Bean实例

对WebSocket的解释: HTTP协议是基于请求/响应模式的,在客户端发起请求,服务端返回响应后,连接就结束了。WebSocket可以看作是可以实现长连接的HTTP的升级,弥补了HTTP协议无法进行长连接的缺点,一次WebSocket的连接可以看作是Websocket会话。

场景: WebSocket的每次会话中,开始时初始化Bean,直到WebSocket结束都是同一个Bean。

备注: 限定Spring MVC中 使用

1.3 【方案】设置作用域

使用@Scope标签就可以用来声明 Bean 的作用域,如下面代码:

@Component
public class UserBean {

    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    @Bean(name = "user")
    public User getUserBean() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(20);
        return user;
    }
}

@Scope标签既可以修饰方法也可以修饰类,它有两种设置方式:

  1. 直接设置值:@Scope("prototype")
  2. 使用 ConfigurableBeanFactory(推荐):@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

其实SCOPE_PROTOTYPE就是 ConfigurableBeanFactory 的一个字段: image.png

在设置了prototype作用域后,终于能够如预期拿到结果:

Do UserController1
从Spring中取出user:User{name='zhangsan', age=20}
修改后的数据User{name='lisi', age=18}
===========================
Do UserController2
从Spring中取出user:User{name='zhangsan', age=20}

2 Spring主要执行流程

2.1 容器初始化

ApplicationContext context = 
        new ClassPathXmlApplicationContext("bean-lifecycle.xml");

2.2 Bean的实例化

读取xml配置文件中配置的所有 Bean 对象以及xml配置文件所配置的扫描路径下添加六大注解的Bean,通过反射实例化Bean(包括分配内存空间和调用构造方法)

<!--  xml中直接配置的Bean  -->
<bean id="userXml" class="com.chenshu.lifecycle.model.UserXml"/>
<!--  xml中配置的扫描路径  -->
<content:component-scan base-package="com.chenshu.lifecycle"/>

2.3 Bean的依赖注入

在依赖注入前,对象仍然是一个原生的状态,并没有进行依赖注入,值得一提的依赖注入这一步并不一定是在2. Bean的实例+初始化之后,如果是构造方法注入的话,会在2. Bean的实例+初始化的初始化的过程中注入所需依赖。

扩展:如果Bean所需要依赖注入的对象没有完全初始化好,会先去完全初始化好(也就是依赖注入)该对象,再注入该Bean

2.4 存入Spring容器中

Spring容器会将这个已经完全初始化好的Bean存入IoC容器中。IoC容器通常是一个Map结构的实现,它使用Bean的名称或ID作为键,Bean的实例作为值。

2.5 使用Bean

应用程序可以通过依赖注入或依赖查找机制来获取bean的引用,并调用其方法。

2.6 销毁Spring容器

Spring容器关闭时,会触发bean的销毁过程。

3. Bean的生命周期

Bean的生命周期分为以下5大部分:

  1. 实例化 Bean(为Bean分配内存空间)
  2. 设置属性(Bean的依赖注入,如@Autowired
  3. Bean 初始化
    • 执行各种通知(xxxAware的接口方法)
    • 执行初始化方法(各种初始化方法执行顺序如下)
      • @PostConstruct定义的初始化方法
      • 判断是否为InitializingBean(调用afterPropertiesSet)
      • xml中定义的init-method方法
  4. 使用Bean
  5. 销毁Bean
    • 销毁Bean前的各种方法,如@PreDestroyDisposableBean接口方法,destroy-method.

Untitled Diagram.drawio-2.png

3.1 代码验证

User类,为了演示依赖注入的时机:

@Component
public class User implements BeanPostProcessor {
}

BeanLifeComponent类,演示了生命周期:

@Component
public class BeanLifeComponent implements BeanNameAware, InitializingBean {
    private User user;
    //构造方法
    public BeanLifeComponent() {
        System.out.println("执行构造方法");
    }
    //setter依赖注入
    @Autowired
    public void setUser(User user) {
        this.user = user;
        System.out.println("依赖注入");
    }
    //BeanNameAware的通知方法
    @Override
    public void setBeanName(String name) {
        System.out.println("执行了BeanName通知方法,name="+name);
    }
    //初始化方法
    @PostConstruct
    public void initByAnnotation() {
        System.out.println("执行@PostConstrut修饰的init方法");
    }
    //初始化方法
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("执行InitializingBean接口的afterPropertiesSet方法");
    }
    //初始化方法
    public void initByXml() {
        System.out.println("执行xml中定义的init方法");
    }
    //使用Bean
    public void use() {
        System.out.println("使用use方法");
    }
    //销毁前调用的方法
    @PreDestroy
    public void destroy() {
        System.out.println("执行@PreDestroy修饰的destroy方法");
    }
}

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:content="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">

    <bean id="beanLifeComponent" 
          class="com.chenshu.lifecycle.beanlife.BeanLifeComponent"
          init-method="initByXml"/>

    <content:component-scan base-package="com.chenshu.lifecycle"/>
</beans>

运行结果:

执行构造方法
依赖注入
执行了BeanName通知方法,name=beanLifeComponent
执行@PostConstrut修饰的init方法
执行InitializingBean接口的afterPropertiesSet方法
执行xml中定义的init方法
使用use方法
执行@PreDestroy修饰的destroy方法