《系列二》-- 2、bean 的作用域: Scope 有哪些
阅读之前要注意的东西:本文就是主打流水账式的源码阅读,主导的是一个参考,主要内容需要看官自己去源码中验证。全系列文章基于 spring 源码 5.x 版本。
写在开始前的话:
阅读spring 源码实在是一件庞大的工作,不说全部内容,单就最基本核心部分包含的东西就需要很长时间去消化了:
- beans
- core
- context
实际上我在博客里贴出来的还只是一部分内容,更多的内容,我放在了个人,fork自 spring 官方源码仓了; 而且对源码的学习,必须是要跟着实际代码层层递进的,不然只是干巴巴的文字味同嚼蜡。
这个仓设置的公共仓,可以直接拉取。
作用域 Scope 特性概述
Spring 容器支持以下6种 scopes
常规作用域
在常规的 spring IOC 场景下使用
- prototype: 原型模式
跟单例模式相反,每次使用beanName 向spring 容器申请bean的时候,必须重新new一个bean对象。
如下案例,向spring 容器中注入一个 "原型模式" 作用域的bean, 每次当需要注入 WorkerService 时,都会创建一个全新的bean 对象。
@Component
@Scope("prototype")
public class WorkerService {
public void handler(String arg) {
this.workerDo();
}
public void workerDo() {
// xx 业务逻辑
}
}
- singleton:单例模式<Spring默认的bean作用域>
当用户通过getBean(beanName) 向容器主动申请bean时、或者由 spring 容器自身进行依赖注入操作时;必须保证被当作依赖,注入的单例bean全局唯一。
如下案例:通过注解的方式向容器中注入一个单例bean, spring 容器在第一次创建 UserService 这个bean后会将其缓存下来。
后续通过同样的beanName 来获取这个bean的时候,Spring 容器会将之前缓存的bean引用返回,从而保证 UserService 的bean 全局唯一。
@Component
@Scope("singleton")
public class UserService {
@Autowired
private WorkerService workerService;
public void handler(String arg) {
workerService.handler(arg);
System.out.println("workerService实例对象内存地址:" + workerService);
}
}
web 场景作用域
-
request: 该作用域的bean,在每次HTTP 请求发生时被创建,它生命周期被,该HTTP请求的生命周期包含。(只有一个bean实例)
-
session: session作用域的bean,在session的生命周期内有效。(只有一个bean实例)
-
application: 该作用域的bean 在 ServletContext 的生命周期内有效。(只有一个bean实例)
-
webSocket: 在webSocket生命周期内有效。(只有一个bean实例)
实际上当下 Spring Web MVC 已经显现颓势,大势已经转向了前后端分离,所以 Web 场景的 bean 作用域的使用场景已经越来越少,本文也不再详细展开。
经典问题
单例作用域的 UserService 中注入,原型作用域的 WorkerService;必须保证从 spring 容器中多次获取 UserService 时,其中注入的 WorkerService 每次都必须不一样。
模拟场景
结合上边讲解单例和原型作用域时,给的伪代码,已知:
-
UserService 是单例作用域
- WorkerService 将作为依赖注入到 UserService 中
-
WorkerService 是原型作用域
看如下的程序1和程序2,如果我们从不同的地方,多次使用 UserService.handler(arg), 你猜猜打印出来的 WorkerService 对象地址会是怎样的?
程序1:
@Controller
public class BusinessOld {
@Autowired
private UserService userService;
public void execute(String arg) {
userService.handler("arg");
}
}
程序2:
@Controller
public class BusinessNew {
@Autowired
private UserService userService;
public void execute(String arg) {
userService.handler("arg");
}
}
实际上运行上述的两段代码,输出的 workerService 对象内存地址将会一样. 前边也讲过了spring 对单例bean的管理模式:
-
单例bean 在第一次被加载后,会被直接缓存在容器中,后续再向容器请求这个单例 bean时,会将之前加载好的bean 直接返回
-
再说直白点,在UserService 类的单例bean 在第一次被初始化时,其上注入的 WorkerService 类对象就已经注入成功了,由于在UserService 是通过单例的形式注入容器的。
所以它只会被初始化一次,所以 WorkerService 也只会被注入 UserService 中一次。自然就会打印一模一样的 WorkerService 地址了
所以,上述的 程序1 和 程序2 将会输出同样的 WorkerService 对象地址。
解决办法
方法一
借助前边讲过的 "lookup-method"实现
简单回归下这个标签的作用,lookup-method.name 属性配置一个方法名, lookup-method.bean 配置一个beanId;
当从容器中加载 id="lookUpTest" 的bean 时,就会通过 lookup-method.name 配置的方法,动态的返回一个 userBean。
这里我们就不用xml的方式书写案例了,基于上述的原理,我们可以简单改一下 UserService 类的代码:
这里在 UserService.handler() 方法上加了一个注解:@Lookup 这代表着,UserService.handler() 方法每次执行时都会重新向容器获取 WorkerService 类的bean。
因为其以原型模式作用域注入容器,故此可以保证每次都能获取到一个新new出来的 WorkerService 类对象。
@Component
@Scope("singleton")
public class UserService {
@Autowired
private WorkerService workerService;
@Lookup("workerService")
public void handler(String arg) {
workerService.handler(arg);
System.out.println("workerService实例对象内存地址:" + workerService);
}
}
方法二 实现接口 BeanFactoryAware
我们首先看看这个接口的结构:
- 很简单,这里只有一个简单的Setter 方法,从这里可以接收到的参数是 BeanFactory 对象,也就是spring容器工厂本身,意思就是说我把整个车床斗给你了,你还车不出来一个珠子?
既然都拿到工厂了,你要什么bean 还不是随你喜欢,还要啥自行车。下边直接看修改好的代码:
@Component
@Scope("singleton")
public class UserService implements BeanFactoryAware {
private BeanFactory beanFactory; // 接收Setter 注入的容器对象工厂
@Override
public void setBeanFactory(Beanfactory beanFactory) {
this.beanFactory = beanFactory;
}
// @Autowired
// private WorkerService workerService;
@Lookup("workerService")
public void handler(String arg) {
// workerService.handler(arg);
WorkerService workerService = beanFactory.getBean("workerService");
workerService.handler(arg);
System.out.println("workerService实例对象内存地址:" + workerService);
}
}
这里相当于我们会在运行时动态从 BeanFactory 中获取bean: "workerService"
由于我们在bean 定义时把它的作用域定义为了 "原型模式", 故此这里当然可以从容器中捞出单例的 WorkerService