7.5.2 容器中Bean的作用域

7.5.2 容器中Bean的作用域

当通过Spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。 Spring支持如下6种作用域

作用域 描述
singleton 单例模式,在整个Spring IoC容器中, singleton作用域的Bean将只生成一个实例。
prototype 每次通过容器的getBean()方法获取prototype作用域的Bean时,都将产生一个新的Bean实例
request 对于一次HTTP请求,request作用域的Bean将只生成一个实例,这意味着,在同一次HTTP请求内,程序每次请求该Bean,得到的总是同一个实例。只有在web应用中使用Spring时,该作用域才真正有效。
session 对于一次HTTP会话,session作用域的Bean将只生成一个实例,这意味着,在同一次HTTP会话内,程序每次请求该Bean,得到的总是同一个实例。只有在Web应用中使用Spring时,该作用域才真正有效.
application 对应整个Web应用,该Bean只生成一个实例。这意味着,在整个Web应用内,程序每次请求该Bean时,得到的总是同一个实例。只有在web应用中使用Spring时,该作用域才真正有效。
websocket 在整个WebSocket的通信过程中,该Bean只生成一个实例。只有在web应用中使用Spring时,该作用域才真正有效。

比较常用的是singletonprototype两种作用域,

  • 对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;
  • 如果一个Bean被设置成prototype作用域,程序每次请求该idBean, Spring都会新建一个Bean实例,然后返回给程序。在这种情况下, Spring容器仅仅使用new关键字创建Bean实例,一旦创建成功,容器就不再跟踪实例,也不会维护Bean实例的状态。

如果不指定Bean的作用域, Spring默认使用singleton作用域。Java在创建Java实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此, prototype作用域的Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,就可以重复使用。因此,应该尽量避免将Bean设置成prototype作用域。

通过scope属性执行Bean的作用域

Spring配置文件通过scope属性指定Bean的作用域,该属性可以接受singletonprototyperequestsessionglobalSession五个值,分别代表上面介绍的5个作用域。

程序示例

1
2
3
4
5
6
7
8
9
10
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\scope
└─src\
├─beans.xml
├─lee\
│ └─BeanTest.java
└─org\
└─crazyit\
└─app\
└─service\
└─Person.java

下面的配置文件中配置singleton作用域的Beanprototype作用域Bean各有一个。

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置一个singleton作用域Bean实例 -->
<bean id="p1" class="org.crazyit.app.service.Person" />
<!-- 配置一个prototype Bean实例 -->
<bean id="p2" class="org.crazyit.app.service.Person"
scope="prototype" />
<bean id="date" class="java.util.Date" />
</beans>

从上面的代码中可以看到,

  • 配置p1对象时没有指定scope属性,则它默认是一个singleton作用域的Bean;
  • p2则指定了scope="prototype"属性,这表明它是一个prototype作用域的Bean
  • 除此之外,上面配置文件还配置了一个iddatesingleton作用域的Bean

BeanTest.java

主程序通过如下代码来测试两个Bean的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package lee;

import org.springframework.context.*;
import org.springframework.context.support.*;

public class BeanTest
{
public static void main(String[] args) throws Exception
{
// 以类加载路径下的beans.xml文件创建Spring容器
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml"); // ①
// 判断两次请求singleton作用域的Bean实例是否相等
System.out.println(ctx.getBean("p1") == ctx.getBean("p1"));

// 判断两次请求prototype作用域的Bean实例是否相等
System.out.println(ctx.getBean("p2") == ctx.getBean("p2"));
System.out.println(ctx.getBean("date"));
Thread.sleep(1000);
System.out.println(ctx.getBean("date"));
}
}

程序执行结果如下:

1
2
3
4
true
false
Mon Aug 26 16:14:21 CST 2019
Mon Aug 26 16:14:21 CST 2019

从上面的运行结果可以看出,
对于singleton作用域的Bean,每次请求该idBean,都将返回同个共享实例,因而两次获取的Bean实例完全相同;
但对prototype作用域的Bean,每次请求该idBean都将产生新的实例,因此两次请求获得的Bean实例不相同。
上面程序的最后还分两次获取,并输出Spring容器中iddateBean代表的时间,虽然程序获取、输出两个date的时间相差一秒,但由于iddateBean是一个singleton Bean,该Bean会随着容器的初始化而初始化——也就是在①号代码处, date Bean已经被创建出来了,因此无论程序何时访问、输出date Bean所代表的时间,永远输出①号代码的执行时间。

通过singleton属性指定Bean的作用域

早期指定Bean的作用域也可通过singleton属性指定,该属性只接受两个属性值:truefalse,分别代表singletonprototype作用域。使用singleton属性则无法指定其他三个作用域,实际上Spring 2.x不推荐使用singleton属性指定Bean的作用域, singleton属性是Spring 1.2.x的方式

request和session作用域

对于request作用域,查看如下Bean定义:

1
<bean id="loginAction" class="org.crazyit.app.struts.LoginAction" scope="request"/>

针对每次HTTP请求,Spring容器会创建一个全新的idloginActionBean实例,且该loginAction Bean实例仅在当前HttpRequest内有效。因此,如果程序需要,完全可以自由更改Bean实例的内部状态;其他请求所获得的loginAction Bean实例无法感觉到这种内部状态的改变。当处理请求结束时, request作用域的Bean实例将被销毁。
requestsession作用域的Bean只对Web应用才真正有效。实际上通常只会将Web应用的控制器Bean指定成request作用域
session作用域与request作用域完全类似,区别在于:request作用域的Bean对于每次HTTP请求有效,而session作用域的Bean则对于每次Http Session有效。
requestsession作用域只在Web应用中才有效,并且必须在web应用中增加额外配置才会生效。为了让requestsession两个作用域生效,必须将HTTP请求对象绑定到为该请求提供服务的线程上,这使得具有requestsession作用域的Bean实例能够在后面的调用链中被访问到

如何使得request作用域生效

Web应用的web.xml文件中增加如下Listener配置,该Listener负责使request作用域生效:

1
2
3
4
5
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>

一旦在web.xml中增加了如上所示配置,程序就可以在Spring配置文件中使用requestsession作用域了。下面的配置文件配置了一个实现类为PersonBean实例,其作用域是request。配置文件代码如下。

程序示例2

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 指定使用request作用域 -->
<bean id="p" class="org.crazyit.app.service.Person"
scope="request" />
</beans>

这样Spring容器会为每次HTTP请求生成一个Person实例,当该请求响应结束时,该实例也随之消失。

Spring MVC默认支持request作用域

如果Web应用直接使用Spring MVC作为MVC框架,即用SpringDispatcherServletDispatcherPortlet来拦截所有用户请求,则无须这些额外的配置,因为SpringDispatcherServletDispatcherPortlet已经处理了所有和请求有关的状态处理。
接下来本示例使用一个简单的JSP脚本来测试该request作用域,该JSP脚本两次向Spring容器请求获取idpBean-当用户请求访问该页面时,由于在同一个页面内,因此可以看到Spring容器两次返回的是同一个Bean。该JSP脚本如下。

test.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%@ page contentType="text/html; charset=GBK" language="java"
errorPage=""%>
<%@ page import="org.springframework.web.context.*"%>
<%@ page import="org.springframework.web.context.support.*"%>
<%@ page import="org.crazyit.app.service.*"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Spring Bean的作用域</title>
</head>
<body>
<%
// 获取Web应用初始化的Spring容器
WebApplicationContext ctx = WebApplicationContextUtils
.getWebApplicationContext(application);
// 两次获取容器中id为p的Bean
Person p1 = (Person) ctx.getBean("p");
Person p2 = (Person) ctx.getBean("p");
out.println((p1 == p2) + "<br/>");
out.println(p1);
%>
</body>
</html>

使用浏览器请求该页面,将可以看到浏览器中输出如下内容:

1
2
true
org.crazyit.app.service.Person@5b82005

如果读者再次刷该页面,将可以看到该页面依然输出true,但程序访问、输出的Person Bean不再是前一次请求得到的Bean
关于HttpRequestHttpSession的作用范围,请参看第2章中关于Web编程的介绍
Spring5不仅可以为Bean指定已经存在的6个作用域,还支持自定义作用域。关于自定义作用域的内容,请参看Spring官方文档等资料。