Spring框架——Bean的Scope(作用域)易错陷阱解析
阅读先修:
阅读本篇博文之前,您必须熟悉基本的有关Spring框架核心的IOC容器相关知识,以及基本的Bean配置方式。如若对该部分知识点仍然不太熟悉,可参考Spring官方文档有关于Scope的介绍:
什么是Bean的Scope
我们知道,BeanFactory除了作为一个轻量级的IOC容器,能够帮我们方便的管理Bean以及Bean之间的依赖关系,它其实还有着一些其它的功能,那就是管理Bean的Scope,或者说管理这些Bean的生命周期。
什么是Scope?亦或者说,什么是Bean的生命周期?Scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,简单点来说,即容器在对象进入其相应的Scope之前,生成并装配这些对象,此时,这些对象就“出生”了;在该对象不再处于这些Scope的限定之后,容器通常会销毁这些对象,此时,对象就“凋亡”了。
在Spring1.0时代,Spring容器最初只提供了两种Scope,也是我们最常用的两种:singleton和prototype。到目前的Spring 5.0,又引入了另外4种Score类型:request、session、application、和websocket。不过这四种类型有所限制,只能在Web应用中使用。也就是说,只有在支持Web应用的ApplicationContext中使用这4个Scope才是合理的。
我们可以通过使用<bean>的singleton或者scope属性来指定相应对象的scope,其中,scope属性只能XSD格式的文档声明中使用,类似于如下代码所演示的形式:
DTD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" singleton="false"/>
XSD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
本篇博文主要讲解基本的两个scope:singleton和prototype。
Singleton Scope(单例)
在Spring的IOC容器中,XML配置中的每个<bean>.......</bean>定义都会被转换成一个BeanDefinition对象,这个对象是用来告知IOC容器如何去初始化相对应的Bean实例对象,我们也可以把它看作是一个模板,容器会根据这个模板来构造对象。但是要根据这个模板构造多少个对象的实例,又该让这些对象能够“存活”多久,则由容器根据Bean定义的scope语义来决定。
标记为拥有singleton scope的对象定义,在Spring 的IOC容器中只存在一个实例,所有对该对象的引用将共享这个实例。换句话说,当你定义了一个singleton scope的Bean定义,IOC容器将会根据这个Bean定义有且仅实例化1个对象,并且,这个唯一的Bean实例会被储存到缓存当中,接下来,如果还有请求需要依赖该Bean,IOC容器将会把缓存当中的那个唯一的Bean实例返回出去。这样,在整个IOC容器中,该Bean对应的实例就只存在一个啦。
这个单一的Bean实例,所有对该对象的引用将共享这个实例。该实例从容器启动,并且因为第一次被请求而初始化之后,将一直存活到容器退出。也就是说,它与IOC容器“几乎”拥有相同的“寿命”。
下图是Spring官方文档对Singleton scope的演示图例:
我们可以从两个方面来看待singleton的Bean所具有的特性:
①对象的实例个数。singleton类型的Bean定义,在一个容器中只存在一个共享的实例对象,所有对该Bean的依赖都引用这一单一实例对象。也就是说,IOC容器只生产一个该对象,并把该对象存入缓存当中。每次有谁请求该对象,就把这唯一的一个对象从缓存当中交给它,如果同时有多个对象依赖这个singleton对象实例,则大家依赖的都是同一个singleton实例对象,因为这个对象在IOC容器只存在一个嘛,大家当然就只能一起“共用”啦。
②对象存活时间。singleton类型的Bean定义,从容器启动,到它第一次被请求而实例化开始,只要容器不销毁或者退出,该类型的Bean的单一实例就会一直存活。
通常情况下,如果我们不指定bean的scope,singleton便是容器的默认scope,所以下面三种配置方式达到的效果是一样的:
DTD或者XSD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" />
DTD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" singleton="true"/>
XSD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
假设我们有一个名为Children类型的Bean实例如下:
package com.jx079;
public class Children {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
我们在ApplicationContext中配置该Bean,虽然我们没有指定该Bean的Scope,但是Spring默认为Singleton模式:
编写如下测试方法:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
System.out.println(ctx.getBean("children"));
System.out.println(ctx.getBean("children"));
输出结果如下:
我们发现,多次从IOC容器中获取children对象,IOC容器给我们返回的都是同一个对象。这也就验证了Singleton scope的作用。
Prototype Scope(多例)
针对声明为拥有prototype scope的Bean定义,容器在接受到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方后,容器就不在拥有当前返回对象的引用,也就是说,容器不会像singleton那样,帮你把该对象存入缓存当中,而是交给请求方后就再也不管了。请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回给请求方一个新的对象实例后,就任由这个实例对象“自生自灭”了。
当然,prototype类型的Bean在每次被注入其他对象的时候,注入的都是新的对象。每次向IOC容器调用getBean()时返回的也是新的实例对象。
所以,对于那些请求方不能共享使用的对象类型,我们必须将其Bean定义的scope设置为prototype。这样,每个请求方可以得到自己对应的一个对象实例。通常,声明为prototype的scope的Bean定义类型,都是有一些状态的,比如保存每个顾客信息的对象。
当然,对于DAO(Data Access Object)对象来说,通常并不会把它配置成为prototype类型的对象,因为通常DAO对象并不具备会话状态。所以我们通常都把DAO对象设置为singleton类型就好了。
下面是Spring官方文档对于prototype的解释:
你可以使用下面几种形式来定义某个Bean的scope为prototype类型,效果是一样的:
DTD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" singleton="false"/>
XSD约束格式:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
正是因为IOC容器每次接受到请求后,都实例化一个新的pototype类型的对象返回给请求方,并不在管理该实例对象的生命周期了,因此,需要用户亲自去管理这些Bean的生命周期,以防发生内存泄漏。
同样是以Children做例子,我们在ApplicationContext中配置该Bean的scope为“prototype”,如下:
我们仍然使用相同的测试代码,看一看返回的结果是怎样的:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
System.out.println(ctx.getBean("children"));
System.out.println(ctx.getBean("children"));
我们发现,每次向IOC容器请求Children对象的时候,IOC容器每次都“现做”一个“新鲜”的全新实例对象给我们。
Singleton类型的Bean依赖Prototype类型的Bean(易错点,难点)
我们知道,拥有prototype类型scope的bean,在请求方每次向容器请求该类型对象的时候,容器都会返回一个全新的该对象实例。
因此,假设我们有下面这样的业务场景:
我们已经了解了Children这个Bean,假设我们现在有一个Parent类型的Bean,它依赖于Children类型的Bean,现在我们需要每次获取parent对象时,parent对象都是同一个对象,但他依赖的children对象必须每次都不一样。
有同学肯定会说,这不是我们刚学了Prototype scope吗?只要把children的scope设置为prototype就好了,这简直是so easy!
那我们就来试一试吧,新的Parent类如下:
public class Parent {
private Children children;
//引用Children类型的对象
public String sayHello() {
return "my children is:" + getChildren();
}
public Children getChildren() {
return children;
}
public void setChildren(Children children) {
this.children = children;
}
}
applicationContext的配置如下:
测试代码如下:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Parent parent1 = (Parent) ctx.getBean("parent");
Parent parent2 = (Parent) ctx.getBean("parent");
System.out.println("parent1:"+parent1);
System.out.println("parent2:"+parent2);
System.out.println("parent1 introduce his children:"+ parent1.sayHello());
System.out.println("parent2 introduce his children:"+ parent2.sayHello());
测试结果如下图:
我们发现,IOC容器给我们返回的Parent对象确实都是同一个,但是它所依赖的children对象貌似也都是同一个。。。并没有每次都给我们新的对象啊!
喵了个怪了!我们的Children实例明明设置的是“prototype”,怎么还每次都给我返回一样的Children对象???莫非是我们发现了IOC容器的一个新Bug?
哈哈,事情当然没有我们想的那么简单。好了,问题并不是处在Children的scope类型是否是prototype的,而是出在实例的q取得方式上。虽然我们给Children的scope设置的确实是为“prototype”,但当容器将一个Children的实例注入Parent后,Parent就一直持有这个Children对象的引用。虽然每次输出都调用了getChildren()方法并返回了Children的实例,但实际上每次返回的都是Parent持有的容器第一次注入的实例。这就是问题所在,换句话说,第一个实例注入后,Parent就再也没有向IOC容器请求新的Children实例了。所以,容器也不会为其重新注入新的Children类型的实例了。
说白了,在IOC容器中只存在一个Parent对象,这个Parent对象持有的Children对象在初始化的时候就已经确定了。随意我们每次请求Parent,都拿到的是同一个Parent,他的Children对象当然也是同一个了。除非我们把Parent对象的scope也设置成为“prototype”,这样,每次向IOC容器请求Parent对象的时候,IOC容器就必须重新生成Parent对象,连带着就会重新向IOC容器申请新的Children对象用于依赖注入。这样,每次拿到的Parent对象都是新的了,相对应的Children对象也是新的了。
我们的applicationContext配置如下:
测试代码不变,结果为:
此时,每次向IOC容器获取的parent都是新的了,相应的children也是新的。But...,我们似乎还是没有实现最初的要求,假设我们现在就要求每次获取Parent对象都是同一个,但是它依赖的Children对象却不一样,那该怎么实现呢?
其实,有两种方法能够实现我们的要求,分别是:①方法注入和②使用BeanFactoryAware接口
Method Injection(方法注入)
Spring容器提供了一种叫做方法注入的方式,可以帮助我们解决上述问题。我们需要做的很简单,只要让getChildren()方法声明符合规定的格式,并在配置文件中通知容器,当该方法被调用时,每次返回指定类型的对象即可。
在Spring的官方文档中,规定了方法声明需要符合的定义如下:
也就是说,该方法必须能够被子类实现或者覆写,因为容器会为我们要进行方法注入的对象使用Cglib技术动态的生成一个子类实现,从而替代当前的对象。我们的getChildren()方法定义如下:
已经满足了规定的要求,接下来我们需要做的就是配置该类了,配置内容如下:
此时,我们再运行一下我们的测试代码,测试结果如下:
Bingo!此时我们每次调用的Parent对象都是同一个了,而依赖的Children对象却并不一样。
通过<look-method>的name属性指定需要注入的方法名,bean属性指定需要注入的对象,当getChildren()再次被调用的时候,容器可以每次返回一个新的Children实例对象。
利用BeanFactoryAware接口
我们知道,即使不利用方法注入,我们只需要在实现getChildren()方法的时候,强制调用BeanFactory的getBean(“children”),就同样可以每次取得新的Children对象实例。现在我们唯一需要的,就是让Parent拥有一个BeanFactory的引用就好啦!
Spring框架提供了一个BeanFactoryAware接口,容器在实例化实现了该接口的bean定义的过程中,会自动将容器本身注入该Bean。这样,该Bean就持有了它所处的BeanFactory的引用。
BeanFactoryAware接口的定义如下:
我们只需要让Parent对象实现该接口从而在初始化的时候拿到BeanFactory的引用就好了,改造后的Parent代码如下:
public class Parent implements BeanFactoryAware{
private BeanFactory beanFactory;
//获取BeanFactory的引用
private Children children;
public String sayHello() {
return "my children is:" + beanFactory.getBean("children");//从容器中拿新的Children
}
public Children getChildren() {
return children;
}
public void setChildren(Children children) {
this.children = children;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory=beanFactory;
}
}
applicationContext配置如下:
运行测试代码的结果如下:
同样的也实现了我们的需求。
参考文献
【1】Spring官方文档 https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html
【2】《Spring揭秘》王福强 著 人民邮电出版社