前言
用过Spring的都知道在我们定义完BeanDefinition后,Spring会根据我们的BeanDefinition去创建实例。但是你有没有想过,Spring根据BeanDefinition创建的实例是单例还是每次都创建一个新的实例呢?假如我们现在需要保证一个单例该怎么办?如果我们要保证每次获取的实例都是新的又该怎么办?关于上面这些问题,我们可以通过设置BeanDefinition中的scope属性来解决。
Bean的作用域
可能在大多数时候我们并没有关注Bean的作用域,在默认情况下Spring创建的Bean的作用域为singleton
,你可以简单的理解为单例。如果不是很明白直接看下面的示例:
public class App1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(App1.class);
context.refresh();
User mac1 = context.getBean("mac", User.class);
User mac2 = context.getBean("mac", User.class);
System.out.println(mac1 == mac2);
User jack1 = context.getBean("jack", User.class);
User jack2 = context.getBean("jack", User.class);
System.out.println(jack1 == jack2);
}
@Bean
@Scope(scopeName = "singleton")
public User mac(){
User user = new User();
user.setName("mac");
user.setAge(18);
return user;
}
@Bean
@Scope(scopeName = "prototype")
public User jack(){
User user = new User();
user.setName("jack");
user.setAge(18);
return user;
}
}
@Getter
@Setter
class User{
private String name;
private Integer age;
}
上面我们定义了两个Bean它们的名称分别为mac
和jack
,对于这两个Bean的Scope我们分别设置成singleton
和prototype
。运行程序打印结果为true
和false
,从结果可以看出不同的scope会对Bean产生不一样的影响。在Spring中默认提供了两个Scope分别为singleton
和prototype
,如果在Web环境下还增加了4中默认scope。
-
singleton
:Spring默认Scope。它表示的是在同一个容器中只会存在一个实例,它会在Spring第一次创建后缓存起来,后续再次从容器中获取时将返回缓存的对象,这种scope也是目前使用最广泛的。 -
prototype
:该Scope表示每次从容器中获取实例都会创建一个新的实例。 -
request
:Bean的范围控制在一次http请求周期内,该scope是web环境提供的。 -
session
:Bean的范围控制在http会话周期内,该scope是web环境提供的。 -
application
:Bean的范围控制在ServletContext周期内,该scope是web环境提供的。 -
websocket
:Bean的范围控制在websocket周期内,该scope是web环境提供的。
如何自定义Scope
上面我们简单的介绍了Spring中默认提供的一些scope,但实际开发中Spring提供的作用域可能并不能满足我们的需求,这时候我们就可以通过自定义scope来实现我们自己的需求。
注册scope
在Spring中大多数我们自定义的东西都需要注册到Spring中才能使用,在自定义scope时同样需要如此操作。如果还有印象的话,你可能还记得在我们之前讲《Spring进阶-BeanFactory》
中有提及过,ConfigurableBeanFactory
提供了scope相关的接口。
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
void registerScope(String scopeName, Scope scope);
String[] getRegisteredScopeNames();
Scope getRegisteredScope(String scopeName);
}
该接口提供了注册、获取scope名称、根据scope名称获取scope相关方法。而registerScope
方法的实现只有一处,它就是位于AbstractBeanFactory
中,其源码如下所示:
public void registerScope(String scopeName, Scope scope) {
Assert.notNull(scopeName, "Scope identifier must not be null");
Assert.notNull(scope, "Scope must not be null");
if (SCOPE_SINGLETON.equals(scopeName) || SCOPE_PROTOTYPE.equals(scopeName)) {
throw new IllegalArgumentException("Cannot replace existing scopes 'singleton' and 'prototype'");
}
Scope previous = this.scopes.put(scopeName, scope);
if (previous != null && previous != scope) {
if (logger.isDebugEnabled()) {
logger.debug("Replacing scope '" + scopeName + "' from [" + previous + "] to [" + scope + "]");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Registering scope '" + scopeName + "' with implementation [" + scope + "]");
}
}
}
从实现逻辑来看还是比较简单的,在内部使用一个Map
类型的变量scopes
存储Scope实例。同时从源码可以看出,对于singleton
和prototype
这两个scope我们是不可以注册的。
Scope接口
上面了解了如何注册Scope,接下来就是如何自定义一个Scope。在Spring中提供了一个接口Scope,实现这个接口的方法就可以自定义一个Scope。
public interface Scope {
//从该Scope中获取实例,如果获取不到则通过objectFactory创建。这个方法通常来说是需要我们实现的
Object get(String name, ObjectFactory<?> objectFactory);
//从该Scope中删除实例
Object remove(String name);
//注册实例销毁逻辑
void registerDestructionCallback(String name, Runnable callback);
//用于解析相应上下文中的数据,例如在request域中可以用来返回request中的属性
Object resolveContextualObject(String key);
//作用域中的会员标志,例如session作用域中就是sessionId
String getConversationId();
}
SimpleThreadScope
通过前面两点我们知道了如何自定义一个Scope。现在我们来自己实现一个简单的Scope,用这个例子来说明。在Spring中默认提供了一个SimpleThreadScope
作用域,但是默认情况下该Scope并为注册到容器中,这里我直接使用该类来说明。
public class SimpleThreadScope implements Scope {
private static final Log logger = LogFactory.getLog(SimpleThreadScope.class);
private final ThreadLocal<Map<String, Object>> threadScope =
new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = this.threadScope.get();
// NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details,
// see https://github.com/spring-projects/spring-framework/issues/25801.
Object scopedObject = scope.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
scope.put(name, scopedObject);
}
return scopedObject;
}
@Override
@Nullable
public Object remove(String name) {
Map<String, Object> scope = this.threadScope.get();
return scope.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
logger.warn("SimpleThreadScope does not support destruction callbacks. " +
"Consider using RequestScope in a web environment.");
}
@Override
@Nullable
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
从代码的实现可以看出,SimpleThreadScope
是一个通过ThreadLocal
实现的Scope的。简单的说就是对于同一个线程从容器中获取的Bean实例都会是相同的,而不同的线程获取的Bean实例都是不同的。这个结论我们在后面的实例代码中会给出。接下来我们就是将这个Scope注册到Spring中,然后使用就可以了。
-
java示例代码
public class App2 {
private static final String THREAD_LOCAL_SCOPE = "thread_local_scope";
public static void main(String[] args) {
//创建IOC容器
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
//注册作用域
factory.registerScope(THREAD_LOCAL_SCOPE,new SimpleThreadScope());
//读取配置
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions("spring-scope-1.xml");
//同一个线程获取的是相同的
Student mainStudent1 = factory.getBean("student", Student.class);
Student mainStudent2 = factory.getBean("student", Student.class);
System.out.println("mainStudent1 == mainStudent2 => " + (mainStudent1 == mainStudent2));
//不同线程获取的是不同
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Student t1Student1 = factory.getBean("student", Student.class);
System.out.println("mainStudent1 == t1Student1 => " + (mainStudent1 == t1Student1));
System.out.println(mainStudent1);
System.out.println(t1Student1);
}
});
t1.setName("t1");
t1.start();
}
}
@Getter
@Setter
@ToString
class Student{
private String name;
private String classNo;
public Student() {
this.name = Thread.currentThread().getName();
this.classNo = "测试班级";
}
}
-
spring-scope-1.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="student" class="com.buydeem.share.scope.Student" scope="thread_local_scope"/>
</beans>
-
运行结果
mainStudent1 == mainStudent2 => true
mainStudent1 == t1Student1 => false
Student(name=main, classNo=测试班级)
Student(name=t1, classNo=测试班级)
上面的示例很简单,我们在XML中定义了一个Student,同时我们将它的作用域设置为thread_local_scope
,这个作用域就是我们自己定义的Scope。在Java示例代码中,我们创建完IOC容器后,然后将自定义的scope注册到容器中。从运行结果可以看出,对于同一个线程(main
)而言,它们从容器中获取的实例都是同一个。而对于不同线程(t1和main
)它们获取的实例最后比较发现是不一样。
scope对依赖注入的影响
在前面讲《Spring进阶-依赖注入》
一文中没有提到这一点,这里正好讲到了Scope所以一起来说明一下。如果两个Bean的作用域相同,相同作用域之间的相互依赖基本上没有问题。但如果两个Bean的作用域不相同,如果不做特殊处理那么很可能会出现一些怪异的问题。在依赖注入时,Bean只会在实例化时注入它的依赖Bean。这样会导致的一个问题是,如果作用域短的Bean被注入到作用域长的Bean中则会产生一些奇怪的问题。可能语言表达的难以理解,直接上代码。
-
Java示例代码
public class App3 {
public static void main(String[] args) {
//创建IOC容器
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
//读取配置
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions("spring-scope-2.xml");
PersonService ps1 = factory.getBean("personService", PersonService.class);
for (int i = 0; i < 3; i++) {
System.out.println(ps1.getPerson());
}
for (int i = 0; i < 3; i++) {
Person person = factory.getBean("person", Person.class);
System.out.println(person);
}
}
}
@Getter
@Setter
@ToString
class Person{
private static final AtomicInteger COUNTER = new AtomicInteger();
private static final String NAME_TEMPLATE = "user_%d";
private String name;
public Person() {
this.name = String.format(NAME_TEMPLATE,COUNTER.getAndIncrement());
}
}
@Getter
@Setter
class PersonService{
private Person person;
}
-
spring-scope-2.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.buydeem.share.scope.Person" scope="prototype"/>
<bean id="personService" class="com.buydeem.share.scope.PersonService" scope="singleton">
<property name="person" ref="person"/>
</bean>
</beans>
上面的示例很简单,在XML配置文件中我们配置了两个Bean分别为person
和personService
,对于这两个Bean我们设置它们的scope分别为prototype
和singleton
。我们将作用域短的person
注入到作用域长的personSerivce
中,现在我想问的是最后程序的运行结果是什么?
Person(name=user_0)
Person(name=user_0)
Person(name=user_0)
Person(name=user_1)
Person(name=user_2)
Person(name=user_3)
从运行结果我们可以看出,通过PersonService
实例获取的Person
实例都是同一个,而直接从IOC容器中获取的并不是同一个实例。这就是Scope对依赖注入的影响。这个问题的产生原因就是依赖注入只会进行一次,所以我们通过PersonService
获取的就是第一次注入的对象,这就导致了我们即使设置了scope为prototype
它也不会生效。对于这个问题如何解决,Spring官方给出了下面几种解决方案。
BeanFactoryAware和ApplicationContextAware
这种方式比较简单,就是通过实现BeanFactoryAware
或者ApplicationContextAware
能获取到IOC容器,而依赖注入项我们直接从IOC容器中获取。不过这种方式Spring官方并不是特别推荐。
Lookup Method Injection
该方式是通过查找方法注入依赖,内部原理就是通过CGLIB来生成子类,然后重写方法来获取依赖项。我们修改代码如下:
@Getter
class PersonService{
private Person person;
public Person getPerson(){
return createPerson();
}
protected Person createPerson(){
return null;
}
}
配置文件修改如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.buydeem.share.scope.Person" scope="prototype"/>
<bean id="personService" class="com.buydeem.share.scope.PersonService" scope="singleton">
<lookup-method name="createPerson" bean="person"/>
</bean>
</beans>
再次运行代码,运行结果如下:
Person(name=user_0)
Person(name=user_1)
Person(name=user_2)
Person(name=user_3)
Person(name=user_4)
Person(name=user_5)
从运行结果可以看出,即使是通过PersonService
实例获取Person
实例,它每次返回的都不是同一个对象。
scoped-proxy
对于上面的第二种方式可能你不太理解,我们换用这种方式。我们可以直接在person
定义中增加如下配置:
<aop:scoped-proxy proxy-target-class="true"/>
整个配置文件修改如下:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="person" class="com.buydeem.share.scope.Person" scope="prototype">
<aop:scoped-proxy proxy-target-class="true"/>
</bean>
<bean id="personService" class="com.buydeem.share.scope.PersonService" scope="singleton">
<property name="person" ref="person"/>
</bean>
</beans>
对于PersonService
方法我们还是保持与之前定义的一致,再次运行代码可以发现,这种方式同样能解决问题。这种方式与Lookup Method Injection
有点类似,都是通过动态代理来生成代理类。你可以这么理解,在依赖注入的时候注入的并不是一个Person
类,而是一个Person
的一个代理类。其实你通过代码打印出Person
实例的类,从类名可以看出它们其实是一个代理类。而Java的动态代理我们都知道有两种方式来实现,一个是JDK提供的通过接口的方式,另一种则是CGLIB库提供的生成子类的方式。而proxy-target-class
则可以控制是哪种方式。
小结
关于Spring的Scope相关知识目前只介绍到这里,当然还有其他很多点没有说到。后续如果有涉及到该部分内容,我们后续再聊。
❝
其实不是不想说了,是因为周五我要下班了! — 来自某一个菜鸟程序员
❞
原文始发于微信公众号(一只菜鸟程序员):Spring进阶-Bean的作用域
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72967.html