Spring Bean作用域&FactoryBean
在配置文件中定义 Bean 时,用户不但可以配置 Bean 的属性值及相互之间的依赖关系,还可以定义 Bean 的作用域。作用域将对 Bean 的生命周期和创建方式产生影响。
类型 | 说明 |
singleton | 在 Spring Ioc 容器中仅存在一个 Bean 实例,Bean 以单实例的方式存在 |
prototype |
每次从容器中调用 Bean 时,都返回一个新的实例,即每次调用 getBean() 时,相当于执行 new XxxBean()操作 |
request | 每次 HTTP 请求都会创建一个新的 Bean。该作用域仅适用于WebApplicationContext环境 |
session |
同一个 HTTP Session 共享一个Bean,不同的 HTTP Session 使用不同的Bean。该作用域仅适用于 |
globalSession |
同一个全局 Session 共享一个 Bean,一般用于 Portlet 应用环境。该作用域仅适用于 WebApplicationContext环境 |
在低版本的 Spring 中,仅支持两个 Bean 作用域,所以采用 singleton="truelfalse" 的配置方式。Spring 为了向后兼容,依然支持这种配置方式。不过,Spring 推荐采用新的配置方式:scope="<作用域类型>"。
除了以上5种预定义的 Bean 作用域外,Spring 还允许用户自定义Bean的作用域。可以先通过org.springframework.beans.factory.config.Scope 接口定义新的作用域,再通过org.springframework.beans.factory.config.CustomScopeConfigurer 这个 BeanFactoryPostProcessor 注册自定义的 Bean 作用域。在一般的应用中,Spring 所提供的作用域已经能够满足应用的要求,用户很少需要自定义新的 Bean 作用域。感兴趣的读者可以自行阅读 Scope 接口的 Javadoc 文档。
1.singleton作用域
单例模式是重要的设计模式之一。在传统的应用开发中,需要手工为每个单实例类编写特定代码,在这种情况下,类的业务逻辑代码和模式代码紧密耦合在一起。Spring 以容器的方式提供天然的单例模式功能,任何 POJO 无须编写特殊的代码,仅通过配置就可以享用单例模式的“大餐”。
一般情况下,无状态或者状态不可变的类适合使用单例模式,不过 Spring 对此实现了超越。在传统开发中,由于DAO 类持有 Connection 这个非线程安全的变量,因此往往未采用单例模式。而在 Spring 环境下,对于所有的DAO 类都可以采用单例模式,因为 Spring 利用 AOP 和 LocalThread 功能,对非线程安全的变量(或称状态)进行了特殊处理,使这些非线程安全的类变成了线程安全的类。
因为 Spring 的这一超越,所以在实际应用中,大部分 Bean 都能以单实例的方式运行,这也是为什么 Spring 将 Bean 的默认作用域定为 singleton 的原因。
singleton 的 Bean 在同一 Spring IOC 容器中只有一个实例,请看下面的例子:
<bean id="car" class="com.smart.scope.Car" scope="singleton"/>① <bean id="boss1" class="com.smart.scope.Boss" p:car-ref="car"/>② <bean id="boss2" class="com.smart.scope.Boss" p:car-ref="car"/>③ <bean id="boss3" class="com.smart.scope.Boss" p:car-ref="car"/>④
①处的 car Bean 声明为 singleton(因为默认是singleton,所以无须显式指定),在容器中有3个其他的 Bean 引用了 car Bean,如②、③、④所示。在容器内部,boss1、boss2 和 boss3 的 car 属性都指向同一个 Bean,如下图所示
不但在配置文件中通过配置注入的 car 引用相同的 car Bean,任何通过容器的 getBean("car") 方法返回的实例也指向同一个Bean。
在默认情况下,Spring 的 ApplicationContext 容器在启动时,自动实例化所有 singleton 的 Bean 并缓存于容器中。虽然启动时会花费一些时间,但它带来两个好处:首先,对 Bean 提前进行实例化操作会及早发现一些潜在的配置问题;其次,Bean 以缓存的方式保存,当运行时用到该 Bean 时就无须再实例化了,提高了运行的效率。如果用户不希望在容器启动时提前实例化 singleton 的 Bean,则可以通过 lazy-init 属性进行控制。
<bean id="boss1" class="com.smart.scope.Boss" p:car-ref="car" lazy-init="true"/>
lazy-init="true" 的 Bean 在某些情况下依然会提前实例化:如果该 Bean 被其他需要提前实例化的 Bean 所引用,那么 Spring 将忽略延迟实例化的设置。
2.prototype作用域
采用 scope="prototype" 指定非单例作用域的 Bean,请看下面的配置:
<bean id="car" class="com.smart.scope.Car" scope="prototype"/>① <bean id="boss1" class="com.smart.scope.Boss" p:car-ref="car"/>② <bean id="boss2" class="com.smart.scope.Boss" p:car-ref="car"/>③ <bean id="boss3" class="com.smart.scope.Boss" p:car-ref="car"/>④
通过以上配置,boss1、boss2、boss3 所引用的都是一个新的 car 实例,每次通过容器的 getBean("car") 方法返回的也是一个新的 car 实例,如下图所示。
在默认情况下,Spring 容器在启动时不实例化 prototype 的 Bean。此外,Spring 容器将 prototype 的 Bean 交给调用者后,就不再管理它的生命周期。
3.与Web应用环境相关的Bean作用域
如果用户使用 Spring 的 WebApplicationContext,则可使用另外3种Bean的作用域:request、session 和 globalSessiono。不过在使用这些作用域之前,首先必须在 web 容器中进行一些额外的配置。
1)在Web容器中进行额外配置
在低版本的 Web 容器中(Servlet 2.3之前),用户可以使用 HTTP 请求过滤器进行配置。
<web—app> ... <filter> <fi1ter—name>requestContextFilter</fi1ter—name> <filter—class>org.springframework.web.filter.RequestContextFiIter</filter—cIass> </filter> <filter—mapping> <filter—name>requestContextFi1ter</filter—name> <!--①对所有的URL进行过滤拦截--> <url-pattern>/*</url-pattern> </filter—mapping> ... </web—app>
在高版本的 web 容器中,则可以利用 HTTP 请求监听器进行配置。
<web—app> ... <listener> <listener—class> org.springframework.web.context.request.RequestContextListener </listener—class> </listener> ... </web—app>
细心的读者可能会有一个疑问:在介绍 WebApplicationContext 初始化时,已经通过 ContextLoaderListener(或ContextLoaderServlet)将 Web 容器与 Spring 容器进行了整合,为什么在这里又要引入一个额外的 RequestContextListener 以支持 Bean 的另外3个作用域呢?通过分析两个监听器的源码,一切疑问就真相大白了,如下图所示。
在整合 Spring 容器时使用 ContextLoaderListener,它实现了 ServletContextListener 监听器接口,ServletContextListener 只负责监听 web 容器启动和关闭的事件。而 RequestContextListener 实现了ServletRequestListener 监听器接口,该监听器监听 HTTP 请求事件,web 服务器接收的每一次请求都会通知该监听器。
Spring 容器启动和关闭操作由 web 容器的启动和关闭事件触发,但如果 Spring 容器中的 Bean 需要 request、session 和 globalSession作 用域的支持,Spring 容器本身就必须获得 web 容器的 HTTP 请求事件,以 HTTP 请求事件“驱动” Bean 作用域的控制逻辑。也就是说,通过配置 RequestContextListener,Spring 容器和 Web 容器的结合更加密切,Spring 容器对 web 容器中的“风吹草动”都能够察觉,因而就可以实施 web 相应 Bean 作用域的控制了。
当然,Spring 完全可以提供一个既实现 ServletContextListener 又实现 ServletContextListener 接口的监听器,这样我们仅需配置一次就可以了。探究 Spring 将二者分开的原因,可能出于两个方面的考虑:第一,考虑版本兼容的问题,毕竟针对 web 应用的 Bean 作用域是从2.0开始提供的;第二:这3种新增的 Bean 作用域的适用场合并不多,用户往往并不真的需要这些新增的 Bean 作用域。
2)request作用域
顾名思义,request 作用域的 Bean 对应一个 HTTP 请求和生命周期。考虑下面的配置:
<bean id="car" class="com.smart.scope.Car" scope="request"/>
这样,每次 HTTP 请求调用 car Bean 时,Spring 容器就会创建一个新的 car Bean,请求处理完毕后,就会销毁这个 Bean。
3)session作用域
假设将以上 Car 的作用域调整为 session 类型,如下:
<bean id="car" class="com.smart.scope.Car" scope="session"/>
这样配置后,car Bean 的作用域横跨整个 HTTP Session,Session 中的所有 HTTP 请求都共享同一个 car Bean。当 HTTP 结束后,实例才被销毁。
4)globalSession作用域
下面的配置片段将 car 的作用域设置为 globalSession:
<bean id="car" class="com.smart.scope.Car" scope="globalSession"/>
globalSession 作用域类似于 session 作用域,不过仅在 Portlet 的 web 应用中使用。Portlet 规范定义了全局 Session 的概念,它被组成 Portlet web 应用的所有子 Portlet 共享。如果不在 Portlet web 应用环境下,那么 globalSession 作用域等价于 session 作用域。
4.作用域依赖问题
假设将 web 相关作用域的 Bean 注入 singleton 或 prototype 的 Bean 中,我们当然希望它能够按照预定的方式工作,即引用者应该从指定的域中取得它的引用。但如果没有进行一些额外的配置,那么我们将得到一个失望的结果。在这种情况下,需要 Spring AOP “出手相救”。
非 Web 相关作用域引用 Web 相关作用域的 Bean
car Bean 是 request 作用域,它被 singleton 作用域的 boss Bean 引用。为了使 boss 能够从适当作用域中获取 car Bean 的引用,需要使用 Spring AOP 的语法为 car Bean 配置一个代理类,如②所示。为了能够在配置文件中使用 AOP 的配置标签,需要在文档声明头中定义 aop 命名空间。
当 boss Bean 在 Web 环境下调用 car Bean 时,Spring AOP 将启用动态代理智能地判断 boss Bean 位于哪个 HTTP 请求线程中,并从对应的 HTTP 请求线程域中获取对应的 car Bean。我们通过下图此进行剖析。
boss Bean 的作用域是 singleton,也就是说,在 Spring 容器中始终只有一个实例,而 car Bean 的作用域为 request,所以每个调用到 car Bean 的 HTTP 请求都会创建一个 car Bean。Spring 通过动态代理技术,能够让 boss Bean 引用到对应 HTTP 请求的 car Bean。
反过来,在配置文件中添加 <aop:scoped-proxy/> 后,注入 boss Bean 中的 car Bean 已经不是原来的 car Bean,而是 car Bean 的动态代理对象。这个动态代理是 Car 类的子类(或实现类,假设 Car 是接口),Spring 在动态代理子类中加入一段逻辑,以判断当前的 boss 需要取得哪个 HTTP 请求相关的 car Bean。
提示:动态代理所添加的逻辑其实也很简单,即判断当前 boss 位于哪个线程中,然后根据这个线程找到对应的 HttpRequest,再从 HttpRequest 域中获取对应的 car。因为 Web 容器的特性,一般情况下,一个 HTTP 请求对应一个独立的线程。
Java 语言只能对接口提供自动代理,所以,如果需要对类提供代理,则需要在类路径中加入 CGLib 的类库,这时 Spring 将使用 CGLib 为类生成动态代理的子类。
FactoryBean
一般情况下,Spring 通过反射机制利用 <bean> 的 class 属性指定实现类实例化 Bean。在某些情况下,实例化 Bean 的过程比较复杂,如果按照传统的方式,则需要在 <bean> 中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会获得一个简单的方案。Spring 为此提供了一个 org.springframework.beans.factory.FactoryBean 工厂类接口,用户可以通过实现该工厂类接口定制实例化 Bean 的逻辑。
FactoryBean 接口对于 Spring 框架来说占有重要的地位,Spring 自身就提供了70多个 FactoryBean 的实现类。它们隐藏了实例化一些复杂 Bean 的细节,给上层应用带来了便利。从 Spring 3.0 开始,FactoryBean 开始支持泛型,即接口声明改为 FactoryBean<T> 的形式。在该接口中共定义了3个接口方法。
1)T getObject():返回由 FactoryBean 创建的 Bean 实例。如果 isSingleton() 返回 true,则该实例会放到 Spring 容器的单实例缓存池中。
2)boolean isSingleton():确定由 FactoryBean 创建的 Bean 的作用域是 singleton 还是 prototype。
3)Class<?> getObjectType():返回 FactoryBean 创建 Bean 的类型。
当配置文件中 <bean> 的 class 属性配置的实现类是 FactoryBean 时,通过 getBean() 方法返回的不是 FactoryBean 本身,而是 FactoryBean#getObject() 方法所返回的对象,相当于 FactoryBean#getObject() 代理了 getBean() 方法。
在前面的例子中,在配置 car 时,car 的每个属性分别对应一个 <property> 元素标签。假设我们认为这种方式不够简洁,而希望通过逗号分隔的方式一次性为 Car 的所有属性指定配置值,那么可以通过编写一个 FactoryBean 来达到目的。
public class CarFactoryBean implements FactoryBean<Car> { private String carInfo; public String getCarInfo() { return carInfo; } //①接受逗号分隔的属性设置信息 public void setCarInfo(String carInfo) { this.carInfo = carInfo; } //②实例化 car Bean public Car getObject() throws Exception { Car car = new Car(); String[] infos = carInfo.split(","); car.setBrand(infos[0]); car.setMaxSpeed(Integer.parseInt(infos[1])); car.setPrice(Double.parseDouble(infos[2])); return car; } //③返回 Car 的类型 public Class<Car> getObjectType() { return Car.class; } //④标识通过该FactoryBean返回Bean是singleton public boolean isSingleton() { return false; } }
有了这个 CarFactoryBean 后,就可以在配置文件中使用以下自定义的配置方式配置Car Bean。
<bean id="car1" class="com.smart.fb.CarFactoryBean" p:carlnfo="红旗CA72,200,20000.00"/>
当调用 getBean("car") 时,Spring 通过反射机制发现 CarFactoryBean 实现了 FactoryBean 的接口,这时 Spring容器就调用接口方法 CarFactoryBean#getObject() 返回工厂类创建的对象。如果用户希望获取 CarFactoryBean 的实例,则需要在使用 getBean(beanName) 方法时显式地在 beanName 前加上“&”前缀,即 getBean("&car")。