在spring中,在xml中定义bean
时,scope
属性是用来声明bean
的做用域的。对于这个属性,你也许已经很熟悉了,singleton
和prototype
信手捏来,甚至还能说出request
、session
、global session
,scope不就只有这么几个值吗。java
emmm,话不要说太满,容易打脸。常见的各种博客中,通常只会介绍上面说到的几种可能值,但翻一翻官方的说明,你就会发现,事情并无这么简单。web
这是官方文档中的介绍,scope属性一共有六种可能值,惊不惊喜,意不意外。spring
下面,就让咱们来一一看看各个值表明的意义。设计模式
singleton
是scope属性的默认值,当咱们把bean的scope属性设置为singleton
时,表明将对该bean使用单例模式,单例想必你们都熟悉,也就是说每次使用该bean的id从容器中获取该bean的时候,都将会返回同一个bean实例。但这里的单例跟设计模式里的单例还有一些小区别。浏览器
设计模式中的单例是经过硬编码,给某个类仅建立一个静态对象,而且只暴露一个接口来获取这个对象实例,所以,设计模式中的单例是相对ClassLoader
而言的,同一个类加载器下只会有一个实例。缓存
下面就是经典的使用double-check
实现的懒加载代码:springboot
public class Singleton{ private static volatile Singleton FRANK; public static Singleton getInstance(){ if (FRANK == null){ synchronized(this){ if (FRANK == null) FRANK = new Singleton(); } } return FRANK; } }
可是在Spring中,singleton单例
指的是每次从同一个IOC容器中返回同一个bean对象,单例的有效范围是IOC容器,而不是ClassLoader
。IOC容器会将这个bean实例缓存起来,以供后续使用。websocket
下面作一个小实验验证一下:session
先写一个测试类:app
public class TestScope { @Test public void testSingleton(){ ApplicationContext context = new ClassPathXmlApplicationContext("test-bean.xml"); TestBean bean = (TestBean) context.getBean("testBean"); Assert.assertEquals(bean.getNum() , 0); bean.add(); Assert.assertEquals(bean.getNum() , 1); TestBean bean1 = (TestBean) context.getBean("testBean"); Assert.assertEquals(bean1.getNum() , 1); bean1.add(); Assert.assertEquals(bean1.getNum() , 2); ApplicationContext context1 = new ClassPathXmlApplicationContext("test-bean.xml"); TestBean bean2 = (TestBean) context1.getBean("testBean"); Assert.assertEquals(bean2.getNum() , 0); bean2.add(); Assert.assertEquals(bean2.getNum() , 1); } }
public class TestBean { private int num; public int getNum() { return num; } public void setNum(int num) { this.num = num; } public void add(){ num++; } }
这是相应的配置文件test-bean.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <beans> <bean id="testBean" class="com.frank.spring.bean.scope.TestBean" scope="singleton"/> </beans>
testBean
的scope
为singleton
,而变量bean
和bean1
所指向的实例都是从同一个IOC容器中获取的,因此获取的是同一个bean实例,所以分别对bean
和bean1
调用add方法后,num的值就会变成2。而bean2
是从另外一个IOC容器中获取的,因此它是一个新的实例,num
的值便成了初始值0,调用add
方法后,num的值变成了1。这样也验证了上面所说的singleton
单例含义,指的是每个IOC容器中仅存在一个实例。
接下来是另外一个经常使用的scope:prototype
。与singleton
相反,设置为prototype
的bean,每次调用容器的getBean
方法或注入到另外一个bean中时,都会返回一个新的实例。
与其余的scope
类型不一样的是,Spring并不会管理设置为prototype
的bean的整个生命周期,获取相关bean时,容器会实例化,或者装配相关的prototype-bean
实例,而后返回给客户端,但不会保存prototype-bean
的实例。因此,尽管全部的bean对象都会调用配置的初始化方法,可是prototype-bean
并不会调用其配置的destroy方法。因此清理工做必须由客户端进行。因此,Spring容器对prototype-bean
的管理在必定程度上相似于 new
操做,对象建立后的事情将所有由客户端处理。
仍旧用一个小栗子来进行测试:
咱们将上面的xml文件进行修改:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <beans> <bean id="testBean" class="com.frank.spring.bean.scope.TestBean" scope="prototype"/> </beans>
@Test public void testPrototype(){ ApplicationContext context = new ClassPathXmlApplicationContext("test-bean.xml"); TestBean bean = (TestBean) context.getBean("testBean"); Assert.assertEquals(bean.getNum() , 0); bean.add(); Assert.assertEquals(bean.getNum() , 1); TestBean bean1 = (TestBean) context.getBean("testBean"); Assert.assertEquals(bean1.getNum() , 0); bean1.add(); Assert.assertEquals(bean1.getNum() , 1); }
这里两次从同一个IOC容器中获取testBean
,获得了两个不一样的bean实例,这就是prototype
的做用。
接着,咱们配置一个初始化方法和销毁方法,来测试一下:
给TestBean类加两个方法:
public class TestBean { private int num; public void init(){ System.out.println("init TestBean"); } public void destroy(){ System.out.println("destroy TestBean"); } public int getNum() { return num; } public void setNum(int num) { this.num = num; } public void add(){ num++; } }
而后在配置文件里设置它的初始化方法和销毁方法:
<beans> <bean id="testBean" class="com.frank.spring.bean.scope.TestBean" scope="prototype" init-method="init" destroy-method="destroy"/> </beans>
仍是用以前的测试方法:
@Test public void testPrototype(){ ApplicationContext context = new ClassPathXmlApplicationContext("test-bean.xml"); TestBean bean = (TestBean) context.getBean("testBean"); Assert.assertEquals(bean.getNum() , 0); bean.add(); Assert.assertEquals(bean.getNum() , 1); TestBean bean1 = (TestBean) context.getBean("testBean"); Assert.assertEquals(bean1.getNum() , 0); bean1.add(); Assert.assertEquals(bean1.getNum() , 1); }
输出以下:
init TestBean init TestBean
能够看到,仅仅输出了初始化方法init
中的内容,而没有输出销毁方法destroy
中的内容,因此,对于prototype-bean
而言,在xml中配置destroy-method
属性是没有意义的,容器在建立这个bean实例后就抛弃它了,若是它持有的资源须要释放,则须要客户端进行手动释放才行。这大概就是亲生和领养的区别吧。
另外,若是将一个prototype-bean
注入到一个singleton-bean
中,那么每次从容器中获取的singleton-bean
对应prototype-bean
都是同一个,由于依赖注入仅会进行一次。
request
和 session
这两个你也许有所耳闻,可是 application
和 websocket
是什么鬼?居然还有这样的神仙scope??莫方,让咱们来一探究竟。
这几个类型的scope都只能在web环境下使用,若是使用 ClassPathXmlApplicationContext
来加载使用了该属性的bean,那么就会抛出异常。就像这样:
java.lang.IllegalStateException: No Scope registered for scope name 'request'
下面让咱们依次来看看这几个值的做用。
若是将scope属性设置为 request
表明该bean的做用域为单个请求,请求结束,则bean将被销毁,第二次请求将会建立一个新的bean实例,让咱们来验证一下。方便起见,建立一个springboot应用,而后建立一个配置类并指定其扫描的xml:
@Configuration @ImportResource(locations = {"classpath:application-bean.xml"}) public class WebConfiguration { }
如下是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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="testBean" class="com.frank.springboothello.model.TestBean" scope="request" > <aop:scoped-proxy/> </bean> </beans>
下面是controller的内容:
@RestController public class HelloController { @Autowired private TestBean testBean; @Autowired private TestBean testBean1; @GetMapping("/testBean") public void testBean(){ System.out.println("==========request start=========="); System.out.println(testBean.getNum()); testBean.add(); System.out.println(testBean.getNum()); System.out.println(testBean1.getNum()); testBean1.add(); System.out.println(testBean1.getNum()); System.out.println("==========request end=========="); } }
这里仍是使用以前的TestBean,也许细心的你会发现,这里有一个乱入的家伙:
<aop:scoped-proxy/>
这是个什么东西???
这里实际上是声明对该bean使用代理模式,这样作的话,容器在注入该bean的时候,将会使用CGLib动态代理
为它建立一个代理对象,该对象拥有与原Bean相同的public接口并暴露,代理对象每次调用时,会从相应做用域范围内(这里是request
)获取真正的TestBean
对象。
那么,为何要这样作呢?
由于被注入的bean(testBean
)和目标bean(HelloController
)的生命周期不同,而同一个容器内的bean注入只会发生一次,你想一想,HelloController
是singleton
的,只会实例化一次,若是不使用代理对象,就意味着咱们只能将同一个request-bean
注入到这个singleton-bean
中,那以后的每次访问,都将调用同一个testBean
实例,这不是咱们想要的结果。咱们但愿HelloController
是容器范围内单例的,同时想要一个做用域为 Http Request
的testBean
实例,这时候,代理对象就扮演着不可或缺的角色了。
另外,值得一提的是,若是咱们对一个scope
为prototype
的bean使用<aop:scoped-proxy/>
的话,那么每次调用该bean的方法都会建立一个新的实例,关于这一点,你们能够自行验证。
代理方式默认是CGLib
,而且只有public
方法会被代理,private
方法是不会被代理的。若是咱们想要使用基于JDK
的代理来建立代理对象,那么只须要将aop标签中的proxy-target-class
属性设置为false便可,就像这样:
<aop:scoped-proxy proxy-target-class="false"/>
但有个条件,那就是这个bean必需要实现某个接口。
咱们再来跑一下代码验证一下,启动!
接下来访问几回http://127.0.0.1:8080/testBean
,输出以下:
==========request start========== 0 1 1 2 ==========request end========== ==========request start========== 0 1 1 2 ==========request end==========
嗯,一切都在掌控范围以内。
跟request
相似,但它的生命周期更长一些,是在同一次会话范围内有效,也就是说若是不关闭浏览器,无论刷新多少次,都会访问同一个bean。
咱们将上面的xml稍做改动:
<bean id="testBean" class="com.frank.springboothello.model.TestBean" scope="session" > <aop:scoped-proxy/> </bean>
再也运行一下,而后在页面刷新几回:
==========request start========== 0 1 1 2 ==========request end========== ==========request start========== 2 3 3 4 ==========request end========== ==========request start========== 4 5 5 6 ==========request end==========
能够看到,num的值一直的增长,可见咱们访问的是同一个bean实例。
而后,咱们使用另外一个浏览器继续访问该页面:
==========request start========== 0 1 1 2 ==========request end========== ==========request start========== 2 3 3 4 ==========request end==========
发现num又从0开始计数了。这样就验证了咱们对session
做用域的想法。
application
的做用域比session
又要更广一些,session
做用域是针对一个 Http Session
,而application
做用域,则是针对一个 ServletContext
,有点相似 singleton
,可是singleton
表明的是每一个IOC容器中仅有一个实例,而同一个web应用中,是可能会有多个IOC容器的,但一个Web应用只会有一个 ServletContext
,因此 application
才是web应用中货真价实的单例模式。
来测试一下,继续修改上面的xml文件:
<bean id="testBean" class="com.frank.springboothello.model.TestBean" scope="application" > <aop:scoped-proxy/> </bean>
而后再次启动后,疯狂访问。
==========request start========== 0 1 1 2 ==========request end========== ==========request start========== 2 3 3 4 ==========request end========== ==========request start========== 4 5 5 6 ==========request end==========
换个浏览器继续访问:
==========request start========== 6 7 7 8 ==========request end========== ==========request start========== 8 9 9 10 ==========request end==========
嗯,验证完毕。
websocket
的做用范围是 WebSocket
,即在整个 WebSocket
中有效。
emmmm,说实话,这个验证起来有点麻烦,摸索了半天没有找到正确姿式,因此。。。。若是有知道如何验证这一点的小伙伴欢迎留言补充。
也许你会发现,不少博客中说的 global session
怎么不见了??
这你就不知道了吧,由于在最新版本(5.2.0.BUILD-SNAPSHOT)中global session
早就被移除了。
因此之后再有人问你,scope属性有哪几种可能值,分别表明什么含义的时候,就能够义正词严的把这篇文章甩他脸上了。
关于 scope 的介绍到此就告一段落了,来作一个小结:
Http Request
。Http Session
。WebSocket
应用。但愿这篇文章能对你有帮助,若是以为还不错的话,记得分享给身边的小伙伴哦。
让咱们红尘做伴,活得潇潇洒洒。