您现在的位置是:首页 > 文章详情

Spring源码解析-Scopes之Singleton Scope(单例)和Prototype Scope(多例)

日期:2019-02-25点击:286

这是继这篇文章后对scope的扩展Spring源码解析-Scopes之Request、Session、Application

Bean的作用域(scopes)

对于Spring里面的Bean的作用域(scope),我们在熟悉不过了,在Spring里面,平时开发中最经常遇到的作用域就是单例(singleton)和多例(prototype)了,但是除了单例和多例两个作用域,其实Spring还有其他的作用域,但是这些基本在开发中没用到过的,Spring完整的作用域有:

singleton:单例,spring默认的创建bean是个单例,在每个IOC容器里面只有唯一的实例;

prototype:多例,spring创建多例时,都会实例化一个新的实例,但是这些实例只有且仅有一个bean的定义对象;

request:将单个bean定义范围限定为单个HTTP请求的生命周期。每个HTTP请求都有自己的bean实例,它是在单个bean定义的后面创建的。仅在Web感知Spring的上下文中有效ApplicationContext;

session:将单个bean定义范围限定为HTTP的生命周期Session。仅在Web感知Spring的上下文中有效ApplicationContext。

application:将单个bean定义到一个生命周期的范围ServletContext。仅在Web感知Spring的上下文中有效ApplicationContext。

websocket:将单个bean定义到一个生命周期的范围WebSocket。仅在Web感知Spring的上下文中有效ApplicationContext。

当然,除了spring定义的这些,我们自己也可以自定义scope。通过实现org.springframework.beans.factory.config.Scope这个接口来自定义我们的scope就可以了。

Singleton Scope(单例)

在spring的IOC容器中,一个单例bean有且仅有一个实例对象。也就是说,当你指定一个bean的定义为单例的话,spring IOC容器只会通过特定的bean定义创建唯一一个bean的实例对象,而这些被创建起来的单例会被缓存起来,在接下来的应用和请求中,都会返回缓存里的实例对象,spring关于单例的概念和我们平时在设计模式里面看到单例模式,还是有区别的,平时我们在设计模式书里面看到单例,往往是在每个类加载器上一个单例只会被实例化一次,即有且仅有一个实例对象,但是spring的单例更加侧着的是每个容器里只有一个实例化对象,所以这就要求容器的唯一性才能确保单例。 如何去指定一个bean是单例呢?spring提供了两种方式去实现,第一种,xml配置方式,通过xml来定义bean并指定scope;第二种,通过注解的方式,spring提供了注解@Scope来指定bean的作用域。

  1. xml实现方式:
<bean id="testService" class="com.demo.DefaultTestService"/> <bean id="testService" class="com.demo.DefaultTestService" scope="singleton"/> 

scope不指定默认就是单例,所以上面两种写法都是可以的。

  1. 注解实现方式
@Scope(value = "singleton") @Service public class DefaultTestService { } 

通过以上两种方式创建出来的就是单例了。在spring中,我们大部分的类都是单例,在单例中,我们应该尽量不使用有状态的成员变量,单例的成员变量是共享的,这就会导致多线程问题,所以,我们的spring的controller和Struts的action这两种的实现是有差异的,因为spring的controller是单例的所以很少会用到类成员变量,而Struts跟前端交互的参数基本都是类成员变量,这是因为Struts的action是多例才能这样做。所以当我们在使用spring创建bean的时候,我们要慎用有状态的类成员变量,如果要使用必须同步操作,不然就会引起多线程共享变量的问题了。当然你也可以创建多例来解决多线程问题。 我们新建两个service类,这2个service类同时依赖了1个dao类,这个dao类有个变量count,初始化为0。下面看代码:

 @Service public class DefaultTestService { @Autowired private TestDao testDao; public Integer getCount() { return testDao.getCount(); } public void IncCount() { testDao.setCount(testDao.getCount()+1); } public TestDao getTestDao() { return testDao; } public void setTestDao(TestDao testDao) { this.testDao = testDao; } } @Service public class DefaultTestService1 { @Autowired private TestDao testDao; public Integer getCount() { return testDao.getCount(); } public TestDao getTestDao() { return testDao; } public void setTestDao(TestDao testDao) { this.testDao = testDao; } } @Component public class TestDao { private Integer count =0; public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } } 

我们新建了上面三个类,然后通过不同的service去修改dao里面的变量,测试代码如下:

public class DemoTest extends BaseTest { protected static final Logger logger = LoggerFactory.getLogger(DemoTest.class); @Autowired private DefaultTestService defaultTestService; @Autowired private DefaultTestService1 defaultTestService1; @Test public void testGet() { defaultTestService.IncCount(); logger.info("其他service对count加1后,别的service去获取到的结果:"+defaultTestService1.getCount()); logger.info("defaultTestService:"+defaultTestService.getTestDao()); logger.info("defaultTestService1:"+defaultTestService1.getTestDao()); } } 

执行以上代码结果如下:

2018-12-17 14:07:07.816 [main] DemoTest - 其他service对count加1后,别的service去获取到的结果:1 2018-12-17 14:07:07.816 [main] DemoTest - defaultTestService:com.fcbox.pangu.manage.web.TestDao@46678e49 2018-12-17 14:07:07.816 [main] DemoTest - defaultTestService1:com.fcbox.pangu.manage.web.TestDao@46678e49 

从结果可以得出,我们在一个service里面去修改dao里的变量count,在另外的service里面获取到了新修改后的值,通过打印可以知道,其实注入到service里的dao是同个实例,所以修改实例里面的变量是全局可见的,这也是为什么我们不能在单例里面使用有状态类成员的原因。当然了,这个例子并不是个严谨的多线程例子,只是为了简单说明单例在不同的调用类里面是同个实例罢了。同理,其实如果我们在单例里面如果使用到了成员变量,对成员变量的操作必须进行同步,如果你有看spring的源码,你会发现spring里面也有很多对成员变量访问进行了同步操作。

Prototype Scope(多例)

正如上面的例子,多例bean,在依赖注入的时候,都会从新建一个实例然后注入进去,多例的实现跟单例一样,只不过关键字从singleton变成了prototype,这样就定义好一个多例bean了。与其他的scope不同,Spring并不管理多例bean的完整生命周期。当一个多例bean被实例化后,这个实例就交由给客户端了,容器并不会记录这个实例。对上面的例子,如果我们把“dao”设置成多例,我们来看看打印出来的结果,结果如下:

2018-12-17 14:14:21.518 [main] DemoTest - 其他service对count加1后,别的service去获取到的结果:0 2018-12-17 14:14:21.518 [main] DemoTest - defaultTestService:com.fcbox.pangu.manage.web.TestDao@46678e49 2018-12-17 14:14:21.518 [main] DemoTest - defaultTestService1:com.fcbox.pangu.manage.web.TestDao@748e9b20 

所以,当一个bean被设置成多例,每次依赖注入的时候,就会去创建新的实例,并注入到具体目标bean里面。可以看出,在DefaultTestService 对TestDao里面的变量修改,不会影响别的service,那是因为,不同的service依赖注入的TestDao是不同的实例,从结果打印就可以清楚的看出两个实例其实是不一样的。

单例bean里依赖多例bean

当一个单例的bean依赖一个多例的bean的时候,这个多例的bean只会在依赖注入的时候被实例化一次,实例化完后,在接下来就不会变了,也就是说只会被实例化一次,如果你想在运行时重复的获取一个多例的话,是不能通过这种依赖注入的方式去执行的,因为多例的实例化只会发生一次,接下来就不会再发生改变了。要想程序每次运行获取的实例都是不一样的,我们会在介绍其他scope里面会介绍,是通过代理的方式来实现的。

DefaultSingletonBeanRegistry简介

DefaultSingletonBeanRegistry可以简单的理解为单例的注册表,所有单例创建完后都会缓存在这个类里面,这个类里有一系列的单例创建,注册等操作。当创建bean的时候如果是单例都会用到这个类里面的方法。看了下里面,有很多cashe map对象如下:

单例的创建完都会保存到这些map中,看下具体的代码:

protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT)); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } 

spring源码里面很多对类成员变量的修改都会进行同步,所以我们的单例要是有成员变量就需要进行同步才行,不然会引起多线程问题的。这个代码其实就是将单例bean的实例对象和bean名字put到map里面保存起来,下次要是有需要注入该bean的话,会先去cache获取,获取不到才会去新建一个单例,新建完后也会放到缓存里面去,具体的单例的创建实例化过程下面讲。

单例和多例的实例化过程

首先,不管是单例还是多例的实例化,都是通过统一的bean的创建流程创建的,具体来看下他们的创建过程,所有的bean的创建都会去调用getBean方法,如下:

 @Override public Object getBean(String name) throws BeansException { return doGetBean(name, null, null, false); } 

doGetBean就会创建bean,继续看下它里面的实现,这个函数比较长不一一讲解,只讲解主流程,在这里面首先会先去缓存里面获取单例,其中的代码如下

Object sharedInstance = getSingleton(beanName); if (sharedInstance != null && args == null) { if (logger.isDebugEnabled()) { if (isSingletonCurrentlyInCreation(beanName)) { logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); } else { logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); } } bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else{ //创建单例 } 

首先,会去spring的容器里面根据名字查找单例,如果获取到单例实例对象,就直接返回该实例,如果获取不到,则进入到else分支,就是创建单例,创建单例之前,首先会把目标bean里面的所有依赖bean(并非@Autowired注解的bean,而是xml用到depend-on标签,或者是参数值引用的bean)全都创建完才创建目标bean,部分代码如下:

//获取目标bean的definition final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); checkMergedBeanDefinition(mbd, beanName, args); // 实例化依赖bean String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { if (isDependent(beanName, dep)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } registerDependentBean(dep, beanName); getBean(dep); } } 

从源码可以看出,依赖bean会在目标bean之前被创建出来。创建完依赖bean之后,就开始创建目标bean,首先他会先判断bean的定义是否是单例,如果是,会先去缓存查找是否已经创建了单例,如果创建了直接返回,否则创建单例并返回;如果不是则创建多例,创建单例的部分代码如下:

if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { try { // return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } 

如果是多例则会进入到多例的创建流程,部分代码如下:

else if (mbd.isPrototype()) { // It's a prototype -> create a new instance. Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } 

单例和多例的创建的不同之处是,单例创建完后spring会记录他的实例状态并保存到缓存里面,下次创建会优先取缓存里的实例,而多例的创建则是直接创建实例化对象,并不会记录他的实例。 从上面的代码可以看到有句代码始终都会出现,不管是从缓存里面获取的单例还是创建的单例又或者是创建的多例,创建的实例对象并不是直接返回的,而是通过这个方法执行后作为结果返回,这句代码bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);到底有什么作用呢?众所周知,spring里面有一种叫做bean工厂的,就是实现了接口FactoryBean的类,他的实例并不是真正的bean,他是一特定类的创建的工厂类,所以工厂的bean实例化完后并不能直接返回,bean工厂的具体的实例化对象是通过接口FactoryBean里面的方法getObject来实现的,所以如果是工厂类则需要特殊处理,这也是正是这句代码的作用,我们来看下它的实现:

protected Object getObjectForBeanInstance( Object beanInstance, String name, String beanName, RootBeanDefinition mbd) { // Don't let calling code try to dereference the factory if the bean isn't a factory. if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) { throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass()); } // Now we have the bean instance, which may be a normal bean or a FactoryBean. // If it's a FactoryBean, we use it to create a bean instance, unless the // caller actually wants a reference to the factory. if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { return beanInstance; } Object object = null; if (mbd == null) { object = getCachedObjectForFactoryBean(beanName); } if (object == null) { // Return bean instance from factory. FactoryBean<?> factory = (FactoryBean<?>) beanInstance; // Caches object obtained from FactoryBean if it is a singleton. if (mbd == null && containsBeanDefinition(beanName)) { mbd = getMergedLocalBeanDefinition(beanName); } boolean synthetic = (mbd != null && mbd.isSynthetic()); object = getObjectFromFactoryBean(factory, beanName, !synthetic); } return object; } 

从代码里我们可以看出,要是如果是个普通的bean则就会直接返回实例对象,如果是个工厂bean,则会去执行获取工厂里的实例化对象。

总结

不管是单例还是多例的创建,这只不过是spring初始化bean的一个步骤,spring从准备bean的定义在到根据这些定义实例化bean,并为bean依赖注入各种属性,通过对spring的深入剖析,对spring的一个非常重要的概念IOC容器已经大体能理解。不管是applicationContext的加载还有bean definition的创建,还是到依赖注入和单例多例的创建都是spring的IOC容器里的一些重要的概念和流程。下一篇会对其他的scope进行研究。

原文链接:https://my.oschina.net/wang5v/blog/3014387
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章