【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

释放双眼,带上耳机,听听看~!
00:00
00:00

scope 属性说明

在spring中,在xml中定义bean时,scope属性是用来声明bean的作用域的。对于这个属性,你也许已经很熟悉了,singletonprototype信手捏来,甚至还能说出requestsessionglobal session,scope不就只有这么几个值吗。

emmm,话不要说太满,容易打脸。常见的各类博客中,一般只会介绍上面说到的几种可能值,但翻一翻官方的说明,你就会发现,事情并没有这么简单。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

这是官方文档中的介绍,scope属性一共有六种可能值,惊不惊喜,意不意外。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

下面,就让我们来一一看看各个值代表的意义。

singleton

singleton是scope属性的默认值,当我们把bean的scope属性设置为singleton时,代表将对该bean使用单例模式,单例想必大家都熟悉,也就是说每次使用该bean的id从容器中获取该bean的时候,都将会返回同一个bean实例。但这里的单例跟设计模式里的单例还有一些小区别。

设计模式中的单例是通过硬编码,给某个类仅创建一个静态对象,并且只暴露一个接口来获取这个对象实例,因此,设计模式中的单例是相对ClassLoader而言的,同一个类加载器下只会有一个实例。

下面就是经典的使用double-check实现的懒加载代码:

  1. public class Singleton{
  2. private static volatile Singleton FRANK;
  3. public static Singleton getInstance(){
  4. if (FRANK == null){
  5. synchronized(this){
  6. if (FRANK == null) FRANK = new Singleton();
  7. }
  8. }
  9. return FRANK;
  10. }
  11. }

但是在Spring中,singleton单例指的是每次从同一个IOC容器中返回同一个bean对象,单例的有效范围是IOC容器,而不是ClassLoader。IOC容器会将这个bean实例缓存起来,以供后续使用。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

下面做一个小实验验证一下:

先写一个测试类:

  1. public class TestScope {
  2. @Test
  3. public void testSingleton(){
  4. ApplicationContext context = new ClassPathXmlApplicationContext(\"test-bean.xml\");
  5. TestBean bean = (TestBean) context.getBean(\"testBean\");
  6. Assert.assertEquals(bean.getNum() , 0);
  7. bean.add();
  8. Assert.assertEquals(bean.getNum() , 1);
  9. TestBean bean1 = (TestBean) context.getBean(\"testBean\");
  10. Assert.assertEquals(bean1.getNum() , 1);
  11. bean1.add();
  12. Assert.assertEquals(bean1.getNum() , 2);
  13. ApplicationContext context1 = new ClassPathXmlApplicationContext(\"test-bean.xml\");
  14. TestBean bean2 = (TestBean) context1.getBean(\"testBean\");
  15. Assert.assertEquals(bean2.getNum() , 0);
  16. bean2.add();
  17. Assert.assertEquals(bean2.getNum() , 1);
  18. }
  19. }
  1. public class TestBean {
  2. private int num;
  3. public int getNum() {
  4. return num;
  5. }
  6. public void setNum(int num) {
  7. this.num = num;
  8. }
  9. public void add(){
  10. num++;
  11. }
  12. }

这是相应的配置文件test-bean.xml

  1. <?xml version=\"1.0\" encoding=\"UTF-8\"?>
  2. <!DOCTYPE beans PUBLIC \"-//SPRING//DTD BEAN 2.0//EN\" \"http://www.springframework.org/dtd/spring-beans-2.0.dtd\">
  3. <beans>
  4. <bean id=\"testBean\" class=\"com.frank.spring.bean.scope.TestBean\" scope=\"singleton\"/>
  5. </beans>

testBeanscopesingleton,而变量beanbean1所指向的实例都是从同一个IOC容器中获取的,所以获取的是同一个bean实例,因此分别对beanbean1调用add方法后,num的值就会变成2。而bean2是从另一个IOC容器中获取的,所以它是一个新的实例,num的值便成了初始值0,调用add方法后,num的值变成了1。这样也验证了上面所说的singleton单例含义,指的是每一个IOC容器中仅存在一个实例。

prototype

接下来是另一个常用的scope:prototype。与singleton相反,设置为prototype的bean,每次调用容器的getBean方法或注入到另一个bean中时,都会返回一个新的实例。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

与其他的scope类型不同的是,Spring并不会管理设置为prototype的bean的整个生命周期,获取相关bean时,容器会实例化,或者装配相关的prototype-bean实例,然后返回给客户端,但不会保存prototype-bean的实例。所以,尽管所有的bean对象都会调用配置的初始化方法,但是prototype-bean并不会调用其配置的destroy方法。所以清理工作必须由客户端进行。所以,Spring容器对prototype-bean 的管理在一定程度上类似于 new 操作,对象创建后的事情将全部由客户端处理。

仍旧用一个小栗子来进行测试:

我们将上面的xml文件进行修改:

  1. <?xml version=\"1.0\" encoding=\"UTF-8\"?>
  2. <!DOCTYPE beans PUBLIC \"-//SPRING//DTD BEAN 2.0//EN\" \"http://www.springframework.org/dtd/spring-beans-2.0.dtd\">
  3. <beans>
  4. <bean id=\"testBean\" class=\"com.frank.spring.bean.scope.TestBean\" scope=\"prototype\"/>
  5. </beans>
  1. @Test
  2. public void testPrototype(){
  3. ApplicationContext context = new ClassPathXmlApplicationContext(\"test-bean.xml\");
  4. TestBean bean = (TestBean) context.getBean(\"testBean\");
  5. Assert.assertEquals(bean.getNum() , 0);
  6. bean.add();
  7. Assert.assertEquals(bean.getNum() , 1);
  8. TestBean bean1 = (TestBean) context.getBean(\"testBean\");
  9. Assert.assertEquals(bean1.getNum() , 0);
  10. bean1.add();
  11. Assert.assertEquals(bean1.getNum() , 1);
  12. }

这里两次从同一个IOC容器中获取testBean,得到了两个不同的bean实例,这就是prototype的作用。

接着,我们配置一个初始化方法和销毁方法,来测试一下:

给TestBean类加两个方法:

  1. public class TestBean {
  2. private int num;
  3. public void init(){
  4. System.out.println(\"init TestBean\");
  5. }
  6. public void destroy(){
  7. System.out.println(\"destroy TestBean\");
  8. }
  9. public int getNum() {
  10. return num;
  11. }
  12. public void setNum(int num) {
  13. this.num = num;
  14. }
  15. public void add(){
  16. num++;
  17. }
  18. }

然后在配置文件里设置它的初始化方法和销毁方法:

  1. <beans>
  2. <bean id=\"testBean\" class=\"com.frank.spring.bean.scope.TestBean\" scope=\"prototype\" init-method=\"init\" destroy-method=\"destroy\"/>
  3. </beans>

还是用之前的测试方法:

  1. @Test
  2. public void testPrototype(){
  3. ApplicationContext context = new ClassPathXmlApplicationContext(\"test-bean.xml\");
  4. TestBean bean = (TestBean) context.getBean(\"testBean\");
  5. Assert.assertEquals(bean.getNum() , 0);
  6. bean.add();
  7. Assert.assertEquals(bean.getNum() , 1);
  8. TestBean bean1 = (TestBean) context.getBean(\"testBean\");
  9. Assert.assertEquals(bean1.getNum() , 0);
  10. bean1.add();
  11. Assert.assertEquals(bean1.getNum() , 1);
  12. }

输出如下:

  1. init TestBean
  2. init TestBean

可以看到,仅仅输出了初始化方法init中的内容,而没有输出销毁方法destroy中的内容,所以,对于prototype-bean而言,在xml中配置destroy-method属性是没有意义的,容器在创建这个bean实例后就抛弃它了,如果它持有的资源需要释放,则需要客户端进行手动释放才行。这大概就是亲生和领养的区别吧。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

另外,如果将一个prototype-bean注入到一个singleton-bean中,那么每次从容器中获取的singleton-bean对应prototype-bean都是同一个,因为依赖注入仅会进行一次。

Request && Session && Application && WebSocket Scopes

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

requestsession 这两个你也许有所耳闻,但是 applicationwebsocket 是什么鬼?竟然还有这样的神仙scope??莫方,让我们来一探究竟。

这几个类型的scope都只能在web环境下使用,如果使用 ClassPathXmlApplicationContext 来加载使用了该属性的bean,那么就会抛出异常。就像这样:

  1. java.lang.IllegalStateException: No Scope registered for scope name \'request\'

下面让我们依次来看看这几个值的作用。

request

如果将scope属性设置为 request 代表该bean的作用域为单个请求,请求结束,则bean将被销毁,第二次请求将会创建一个新的bean实例,让我们来验证一下。方便起见,创建一个springboot应用,然后创建一个配置类并指定其扫描的xml:

  1. @Configuration
  2. @ImportResource(locations = {\"classpath:application-bean.xml\"})
  3. public class WebConfiguration {
  4. }

以下是xml中的内容:

  1. <?xml version=\"1.0\" encoding=\"UTF-8\"?>
  2. <beans xmlns=\"http://www.springframework.org/schema/beans\"
  3. xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:aop=\"http://www.springframework.org/schema/aop\"
  4. xsi:schemaLocation=\"http://www.springframework.org/schema/beans
  5. 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\">
  6. <bean id=\"testBean\" class=\"com.frank.springboothello.model.TestBean\" scope=\"request\" >
  7. <aop:scoped-proxy/>
  8. </bean>
  9. </beans>

下面是controller的内容:

  1. @RestController
  2. public class HelloController {
  3. @Autowired
  4. private TestBean testBean;
  5. @Autowired
  6. private TestBean testBean1;
  7. @GetMapping(\"/testBean\")
  8. public void testBean(){
  9. System.out.println(\"==========request start==========\");
  10. System.out.println(testBean.getNum());
  11. testBean.add();
  12. System.out.println(testBean.getNum());
  13. System.out.println(testBean1.getNum());
  14. testBean1.add();
  15. System.out.println(testBean1.getNum());
  16. System.out.println(\"==========request end==========\");
  17. }
  18. }

这里还是使用之前的TestBean,也许细心的你会发现,这里有一个乱入的家伙:

  1. <aop:scoped-proxy/>

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

这是个什么东西???

这里其实是声明对该bean使用代理模式,这样做的话,容器在注入该bean的时候,将会使用CGLib动态代理为它创建一个代理对象,该对象拥有与原Bean相同的public接口并暴露,代理对象每次调用时,会从相应作用域范围内(这里是request)获取真正的TestBean对象。

那么,为什么要这样做呢?

因为被注入的bean(testBean)和目标bean(HelloController)的生命周期不一样,而同一个容器内的bean注入只会发生一次,你想想,HelloControllersingleton的,只会实例化一次,如果不使用代理对象,就意味着我们只能将同一个request-bean注入到这个singleton-bean中,那之后的每次访问,都将调用同一个testBean实例,这不是我们想要的结果。我们希望HelloController是容器范围内单例的,同时想要一个作用域为 Http RequesttestBean实例,这时候,代理对象就扮演着不可或缺的角色了。

另外,值得一提的是,如果我们对一个scopeprototype的bean使用<aop:scoped-proxy/>的话,那么每次调用该bean的方法都会创建一个新的实例,关于这一点,大家可以自行验证。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

代理方式默认是CGLib,并且只有public方法会被代理,private方法是不会被代理的。如果我们想要使用基于JDK的代理来创建代理对象,那么只需要将aop标签中的proxy-target-class属性设置为false即可,就像这样:

  1. <aop:scoped-proxy proxy-target-class=\"false\"/>

但有个条件,那就是这个bean必须要实现某个接口。

我们再来跑一下代码验证一下,启动!

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

接下来访问几次http://127.0.0.1:8080/testBean,输出如下:

  1. ==========request start==========
  2. 0
  3. 1
  4. 1
  5. 2
  6. ==========request end==========
  7. ==========request start==========
  8. 0
  9. 1
  10. 1
  11. 2
  12. ==========request end==========

嗯,一切都在掌控范围之内。

session

request类似,但它的生命周期更长一些,是在同一次会话范围内有效,也就是说如果不关闭浏览器,不管刷新多少次,都会访问同一个bean。

我们将上面的xml稍作改动:

  1. <bean id=\"testBean\" class=\"com.frank.springboothello.model.TestBean\" scope=\"session\" >
  2. <aop:scoped-proxy/>
  3. </bean>

再也运行一下,然后在页面刷新几次:

  1. ==========request start==========
  2. 0
  3. 1
  4. 1
  5. 2
  6. ==========request end==========
  7. ==========request start==========
  8. 2
  9. 3
  10. 3
  11. 4
  12. ==========request end==========
  13. ==========request start==========
  14. 4
  15. 5
  16. 5
  17. 6
  18. ==========request end==========

可以看到,num的值一直的增加,可见我们访问的是同一个bean实例。

然后,我们使用另一个浏览器继续访问该页面:

  1. ==========request start==========
  2. 0
  3. 1
  4. 1
  5. 2
  6. ==========request end==========
  7. ==========request start==========
  8. 2
  9. 3
  10. 3
  11. 4
  12. ==========request end==========

发现num又从0开始计数了。这样就验证了我们对session作用域的想法。

application

application的作用域比session又要更广一些,session作用域是针对一个 Http Session,而application作用域,则是针对一个 ServletContext ,有点类似 singleton,但是singleton代表的是每个IOC容器中仅有一个实例,而同一个web应用中,是可能会有多个IOC容器的,但一个Web应用只会有一个 ServletContext,所以 application 才是web应用中货真价实的单例模式。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

来测试一下,继续修改上面的xml文件:

  1. <bean id=\"testBean\" class=\"com.frank.springboothello.model.TestBean\" scope=\"application\" >
  2. <aop:scoped-proxy/>
  3. </bean>

然后再次启动后,疯狂访问。

  1. ==========request start==========
  2. 0
  3. 1
  4. 1
  5. 2
  6. ==========request end==========
  7. ==========request start==========
  8. 2
  9. 3
  10. 3
  11. 4
  12. ==========request end==========
  13. ==========request start==========
  14. 4
  15. 5
  16. 5
  17. 6
  18. ==========request end==========

换个浏览器继续访问:

  1. ==========request start==========
  2. 6
  3. 7
  4. 7
  5. 8
  6. ==========request end==========
  7. ==========request start==========
  8. 8
  9. 9
  10. 9
  11. 10
  12. ==========request end==========

嗯,验证完毕。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

websocket

websocket 的作用范围是 WebSocket ,即在整个 WebSocket 中有效。

emmmm,说实话,这个验证起来有点麻烦,摸索了半天没有找到正确姿势,所以。。。。如果有知道如何验证这一点的小伙伴欢迎留言补充。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

global session

也许你会发现,很多博客中说的 global session 怎么不见了??

这你就不知道了吧,因为在最新版本(5.2.0.BUILD-SNAPSHOT)中global session早就被移除了。

所以以后再有人问你,scope属性有哪几种可能值,分别代表什么含义的时候,就可以理直气壮的把这篇文章甩他脸上了。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

总结

关于 scope 的介绍到此就告一段落了,来做一个小结:

  1. singleton:单例模式,每次获取都返回同一个实例,相对于同一个IOC容器而言。
  2. prototype:原型模式,每次获取返回不同实例,创建后的生命周期不再由IOC容器管理。
  3. request:作用域为同一个 Http Request
  4. session:作用域为同一个 Http Session
  5. application:作用域为同一个WEB容器,可以看做Web应用中的单例模式。
  6. websocket:作用域为同一个WebSocket应用。

希望这篇文章能对你有帮助,如果觉得还不错的话,记得分享给身边的小伙伴哦。

让我们红尘作伴,活得潇潇洒洒。

【Spring源码解读】bean标签中的属性(一)你可能还不够了解的 scope 属性

站长资讯

#4 MAC帧格式 Part I

2020-11-9 3:39:06

站长资讯

#5 Python面向对象(四)

2020-11-9 3:39:08

0 条回复 A文章作者 M管理员
欢迎您,新朋友,感谢参与互动!
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
私信列表
搜索