在 Spring 中, 最主要的操作就是存储 Bean 和 从容器中拿取 Bean, 因此 Bean 对象有关的东西就显得很重要了, 比如作用域
一. 什么是作用域
作用域应该都不陌生, 作用域在前面的学习中, 无论是数据结构还是SE 等地方, 都有涉及过作用域的问题, **简单来说作用域就是起作用的范围, **前面的所学中, 有如下几种作用域 :
- 类级别作用域 : 在类的声明中定义的变量, 被称为局部变量或静态变量, 它们的作用域是整个类
- 方法级别作用域 : 在大括号范围内定义的变量, 被称为局部变量, 他们的作用域只在该代码块内有效. 一般在方法、循环体、条件语句等代码块内定义.
- 方法级别作用域 : 在方法中定义的变量, 也是局部变量, 它们的作用域只在该方法内部有效
- 形式参数作用域 : 在方法签名中定义的参数, 也是局部变量, 他的作用域只在方法中有效.
需要注意的是, 变量的作用域是不能嵌套的, 即内部的作用域的变量不能访问外部作用域的变量, 但外部作用域的变量可以被内部作用域访问到
二. Bean 的作用域问题
在 Spring 框架中, Bean 对象的作用域指的是 Spring 容器管理的 Bean 对象的生命周期和可见性范围
为什么要去了解 Bean 的作用域 ? 一方面除了上面所说的 Bean 对象在 Spring 框架中的重要性, 另一方面更是想要用好 Bean 对象的关键, 因为作用域的问题, 如果不理解, 可能会让你的代码中 Bean 对象出现在不该出现的地方, 赋值给不该给的对象等问题
比如下面这段代码 :
- 先创建一个 People 类
- 将 People 注入到 Spring 容器中
- 模拟 A、B 两个用户去使用 People 这个 Bean 对象
- 输出看看 A B 用户在使用 Bean 时有什么变化
输出后发现, B 去读取的时候居然读取到了 A 修改之后的内容, 这就出现问题了, 如果在后续的项目中, 你修改了这个对象, 而另一个人也需要用这个对象, 他改了你再去用拿到的就是错误得数据, 影响后续项目的开发.
哪这是什么原因呢 ?
出现以上问题, 其实是因为 Bean 默认情况下是单例的, 也就是大家在使用的都是同一个对象, 对于单例模式而言我们知道, 单例模式是可以提高性能的, 在 Spring 中 Bean 的作用域同样是默认为单例模式的
三. Bean 的六种作用域
哪上面这个问题怎么解决呢 ? Bean 又有哪些作用域申明呢 ? 对于作用域, 又是在什么时候生效的 ?
1. 单例作用域(singleton)
对于单例作用域, 写过单例模式来说应该都不会太陌生, 对于 Spring 中 Bean 的单例模式是差不多的
定义: 该作用域下的 Bean 在 IoC 容器中只存在一个实例, 获取 Bean (ApplicationContontext.getBean) 以及装配 Bean 对象(@Autowired) 都是同一个对象
在 Spring 中通常无状态的 Bean 使用该作用域, 无状态表示 Bean 对象的属性状态不需要更新, 就像上面一样, 这个对象的内容它是不可变的, 谁去获取都是同一个对象, 就可以用单例模式
最后需要注意的是, Spring 中的 Bean 对象创建时默认都是单例模式的, 如果需要更改则需要重新指定作用域
2. 原型作用域(prototype)
定义 : 每次对该作用域下的 Bean 的请求都会去创建一个新的实例, 获取 Bean (ApplicationContontext.getBean) 以及装配 Bean 对象(@Autowired) 都是新的对象实例
和单例模式恰好相反, 原型作用域通常用于有状态的 Bean 对象. 比如上面未解决的问题, 我们期望的是 A 去修改以后获取的是 A 修改以后的值, 而 B 去读取的时候, 读取的是原来的值, 这样 A B 都取操作容器里的 Bean 对象是就不会互相干扰了. 这样就需要在注入 Bean 的时候修改它的作用域为原型作用域
修改作用域使用的是 @Scope 注解, Spring 在注解里面提供的 ConfigurableBeanFactory 方法去设置作用域.
可以看到, 重新设置 Bean 对象的作用域以后, 当 A B 去获取 Bean 对象时, 都是一份新的实例, 各自使用自己的, 互不干扰.
3. 请求作用域(request)
对于请求作用域, 很容易联想到 HTTP 请求中的 Request, 这和 HTTP 请求也是有一定关系的
定义 : 每次 HTTP 请求都会创建新的 Bean 实例, 和原型作用域类似.
通常用在一次 HTTP 请求和响应都是共享一份 Bean 的, 这样一说还有点抽象. 具体而言就是和一次 HTTP 请求的声明周期是有关的. 当客户端发送一个 HTTP 请求时, Spring 会创建一个上下文对象也就是 Request Context , 在这个上下文中所有的请求作用域的 Bean 实例都会存活, 只要请求结束或者超时, 这些 Bean 就会销毁.
所以通常情况下, 请求作用域适用于 Web 应用程序的处理流中需要共享的数据的情况, 当请求一结束, 数据就消失了.
需要注意的是, 这个请求作用域在 Spring 中不存在, 只限定于 Spring MVC 中使用 !
4. 会话作用域(session)
对于 session 也不陌生, 在 Web 项目中, 是非常常见的
定义 : 每次 HTTP 会话(session) 都会创建一个新的 Bean 实例, 类似于原型模式.
通常适用于需要与用户会话相关的场景, 具体来说, 比如当用户登陆系统后, 系统会为它建立一个会话,在这个会话期间, 所有会话作用域下的 Bean 实例都会存活, 当会话结束或超时, 这些 Bean 对象就会被销毁. 又例如一个在线购物程序, 用户可能需要在多个页面之间添加商品到购物车中并进行结算, 同时还需要信息登录验证等操作, 这时候可以将用户的购物车、登陆信息等状态存储在会话作用域的 Bean实例中, 以便在不同的页面之间共享和保持这些状态和数据
需要注意的时, 会话作用域同样是只限定于 Spring MVC 中使用 !
5. 全局作用域(application)
定义 : 在整个 Web 应用程序中, 只创建一个 Bean 实例, 并且该实例会一直存在于应用程序的整个生命周期, 也就是说, 在初始加载应用程序时, Spring 容器会创建该 Bean 实例, 并将其放入容器中供所有的请求使用, 直至程序结束.
通常用于 Web 项目的上下文信息, 比如记录一个应用的共享信息. 由于全局作用域在 Web 应用中只创建一个 Bean 实例, 并且供其他所有请求使用, 也可以存一些 全局的配置信息和状态(例如系统日志记录器、缓存管理), 共享的资源和对象(比如数据库连接池、线程池等), 提供全局服务和功能(邮件发送,、短信通知、定时任务调度服务)
需要注意的是, 全局作用域同样只现定于 Spring MVC 中使用 !
6. WebSocket作用域(websocket)
定义 : 在 WebSocket 连接的整个生命周期中共享同一个 Bean 实例. WebSocket 的每次会话中, 保存了一个 Map 结构的头信息, 将用来包裹客户端的消息头. 在第一次初始化后, 直到 WebSocket 结束都是同一个 Bean
由于 WebSocket 会话作用域的 Bean 是在每个 WebSocket 连接时创建的, 并且会一直保留到连接关闭. 这种作用域的 Bean 实例通常用于处理跨越多个 WebSocket 消息的数据或状态, 比如聊天室的用户信息、在线游戏中房间状态等
需要注意的是, WebSocket 作用域只限定于 Spring WebSocket 中使用 !
四. 设置 Bean 的作用域
使用 @Scope 注解则可以声明作用域.
- 直接设置值
- 使用枚举设置
Spring 中提供了 ConfigurableBeanFactory 里面列举了一些作用域(由于我这是 Spring 项目, 没有添加 Spring MVC 因此没有其他的作用域可以选择)
五. Bean 的生命周期
1. Spring 的执行流程
在去了解 Bean 的声明周期之前, 需要先去了解 Spring 的执行流程
Spring 的执行流程(Bean 的执行流程) - > 启动 Spring 容器 - >配置文件的加载 - > 实例化 Bean ( 分配内存空间 ) - > 将 Bean 注入到 Spring 容器中( 存操作 ) - > 装配 Bean 对象( 取操作, 将 Bean 装配到指定类 )
2. 生命周期
- 实例化 Bean ( 为 Bean 分配内存空间 , 实例化 ≠ 初始化 )
- 设置属性 ( Bean 的注入 )
Bean 的初始化
- 实现各种 Aware 通知, 例如 : BeanNameAware、BeanFactoryAware、ApplicationContextAware 的接⼝⽅法
- 执⾏初始化前置⽅法, 如 : BeanPostProcessor
- 执⾏初始化⽅法,依赖注⼊操作之后被执⾏( 设置了指定方法才执行, 不设置不执行 )
- 注解方式 : @PostConstruct
- xml 方式 : 指定的 init-method ⽅法
- 执⾏ BeanPostProcessor 初始化后置⽅法
- 使用 Bean
- 销毁 Bean , 例如 : @PreDestroy、DisposableBean 接口方法 、destroy-method.
下面, 通过一组代码实例来演示 Bean 的生命周期过程
实例化 : 通过 BeanDefinition 对象创建 Bean 实例, Spring 根据配置文件或者注解的方式获取 Bean 的元数据信息, 并将这些信息封装为 BeanDefinition 对象. 当 Spring 容器启动时,会根据这些 BeanDefinition 对象创建相应的 Bean 实例
- 注解方式
- 配置文件方式
此处并非是去设置属性, 在这是申明是通过配置文件的形式来将 Bean 对象实例, 得先创建这个实例才能存到 Spring 中
- 设置属性: 注册 Bean 实例的方式主要有两种
- 在配置文件中使用 标签进行声明
这里的属性设置, 是指将 Bean 实例化后, 将这些对象注入到容器之中的过程
- 使用注解 @Bean 声明 Bean
此处由于没有使用 @Bean 注解, 因此没有去实现, 可以自己试试
3. Bean 的初始化
- 实现各种通知 :
去实现 BeanNameAware 接口, 并重写setBeanName 方法, 即可重写执行通知
- 执行初始化前置方法
实现 InitalizingBean 接口, 重写 afterPropertiesSet 方法, 即可重写前置初始化方法
- 执行初始化方法
- 注解的方式
使用 @PostConstruct 注解可以完成初始化
- XML 方式
- 执行后置化方法
实现 BeanPostProcessor 接口, 重写里面的 postProcessBeforeInitialization 方法, 这是自定义的前置方法
重写 postProcessAfterInitialization 这是自定义的后置方法, 需要注意的是, 如果需要启动这个自定义的后置初始化方法, 前提是容器中有两个及以上的 Bean 对象, 从而 Spring 在管理的时候才会进行区分
比如此时, 我的整个 Spring 容器中只有我此刻注入的 PeopleController2 这个类
此刻去执行会发现, 没有输出后置初始化中的打印信息, 就是因为我的容器中此时只有一个 Bean 对象可以管理, 当我们去添加一个 Bean 对象时, 此处通过注解添加一个 ServiceStudent 到容器中, 再次去执行后才会启动了后置初始化方法
这里由于我在后置初始化都得方法中并没有去设置执行前后逻辑, 因此它是连在一起打印的, 在重写这个接口的时候, 可以根据自己的逻辑实现前置初始化和后置初始化, 可以不用去实现 InitalizingBean 接口.
- 使用 Bean
创建 Spring 容器并获得 Bean 对象调用其中的 fun 方法验证是否注入成功
- **销毁 Bean **
- 注解的方式
- XML 方式
- 演示代码
PS1 : 可以看到的不一样的是, 在创建 Spring 容器的时候使用的是 ClassPathXmlApplicationContext , 此处是因为 ApplicationContext 来创建的上下文对象没有 destroy() 方法, 此处为了演示调用这个方法 因此选择的是 ClassPathXmlApplicationContext.
PS2 : 上面由于Bean 的声明周期问题, 如果注入多个会初始化多次, 因此此处只注入了申明的 PeopleCenter2 这个类, 只有一个 Bean 对象在容器中, 从而没有启动后置初始化方法( BeanPostProcessor 中重写的前置和后置方法 )
总结 : 从上面的代码演示中可以看出整个 Bean 的生命周期, 了解好 Bean 对象的声明周期, 可以加深对 Spring 中 Bean 对象的使用, 从而写出更符合要求的代码!