Spring容器中的组件默认是单例的,在Spring启动时就会实例化并初始化这些对象,将其放到Spring容器中,以后,每次获取对象时,直接从Spring容器中获取,而再也不建立对象。若是每次从Spring容器中获取对象时,都要建立一个新的实例对象,该如何处理呢?此时就须要使用@Scope注解设置组件的做用域。java
项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotationgit
@Scope注解可以设置组件的做用域,咱们先来看@Scope注解类的源码,以下所示。github
package org.springframework.context.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Scope { @AliasFor("scopeName") String value() default ""; /** * Specifies the name of the scope to use for the annotated component/bean. * <p>Defaults to an empty string ({@code ""}) which implies * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}. * @since 4.2 * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE * @see ConfigurableBeanFactory#SCOPE_SINGLETON * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION * @see #value */ @AliasFor("value") String scopeName() default ""; ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; }
从源码中能够看出,在@Scope注解中能够设置以下值。web
ConfigurableBeanFactory#SCOPE_PROTOTYPE ConfigurableBeanFactory#SCOPE_SINGLETON org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
很明显,在@Scope注解中能够设置的值包括ConfigurableBeanFactory接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext类中SCOPE_REQUEST和SCOPE_SESSION。这些都是什么鬼?别急,咱们来一个个查看。spring
首先,咱们进入到ConfigurableBeanFactory接口中,发如今ConfigurableBeanFactory类中存在两个常量的定义,以下所示。安全
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry { String SCOPE_SINGLETON = "singleton"; String SCOPE_PROTOTYPE = "prototype"; /*****************此处省略N多行代码*******************/ }
没错,SCOPE_SINGLETON就是singleton,SCOPE_PROTOTYPE就是prototype。微信
那么,WebApplicationContext类中SCOPE_REQUEST和SCOPE_SESSION又是什么鬼呢?就是说,当咱们使用了Web容器来运行Spring应用时,在@Scope注解中能够设置WebApplicationContext类中SCOPE_REQUEST和SCOPE_SESSION的值,而SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session。session
综上,在@Scope注解中的取值以下所示。多线程
其中,request和session做用域是须要Web环境支持的,这两个值基本上使用不到,若是咱们使用Web容器来运行Spring应用时,若是须要将组件的实例对象的做用域设置为request和session,咱们一般会使用request.setAttribute("key",object)和session.setAttribute("key", object)的形式来将对象实例设置到request和session中,一般不会使用@Scope注解来进行设置。并发
首先,咱们在io.mykit.spring.plugins.register.config包下建立PersonConfig2配置类,在PersonConfig2配置类中实例化一个Person对象,并将其放置在Spring容器中,以下所示。
package io.mykit.spring.plugins.register.config; import io.mykit.spring.bean.Person; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author binghe * @version 1.0.0 * @description 测试@Scope注解设置的做用域 */ @Configuration public class PersonConfig2 { @Bean("person") public Person person(){ return new Person("binghe002", 18); } }
接下来,在SpringBeanTest类中建立testAnnotationConfig2()测试方法,在testAnnotationConfig2()方法中,建立ApplicationContext对象,建立完毕后,从Spring容器中按照id获取两个Person对象,并打印两个对象是不是同一个对象,代码以下所示。
@Test public void testAnnotationConfig2(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); //从Spring容器中获取到的对象默认是单实例的 Object person1 = context.getBean("person"); Object person2 = context.getBean("person"); System.out.println(person1 == person2); }
因为对象在Spring容器中默认是单实例的,因此,Spring容器在启动时就会将实例对象加载到Spring容器中,以后,每次从Spring容器中获取实例对象,直接将对象返回,而没必要在建立新对象实例,因此,此时testAnnotationConfig2()方法会输出true。以下所示。
这也验证了咱们的结论:对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,以后,每次从Spring容器中获取实例对象,直接将对象返回,而没必要在建立新对象实例。
修改Spring容器中组件的做用域,咱们须要借助于@Scope注解,此时,咱们将PersonConfig2类中Person对象的做用域修改为prototype,以下所示。
@Configuration public class PersonConfig2 { @Scope("prototype") @Bean("person") public Person person(){ return new Person("binghe002", 18); } }
其实,使用@Scope设置做用域就等同于在XML文件中为bean设置scope做用域,以下所示。
此时,咱们再次运行SpringBeanTest类的testAnnotationConfig2()方法,此时,从Spring容器中获取到的person1对象和person2对象仍是同一个对象吗?
经过输出结果能够看出,此时,输出的person1对象和person2对象已经不是同一个对象了。
接下来,咱们验证下在单实例做用域下,Spring是在何时建立对象的呢?
首先,咱们将PersonConfig2类中的Person对象的做用域修改为单实例,并在返回Person对象以前打印相关的信息,以下所示。
@Configuration public class PersonConfig2 { @Scope @Bean("person") public Person person(){ System.out.println("给容器中添加Person...."); return new Person("binghe002", 18); } }
接下来,咱们在SpringBeanTest类中建立testAnnotationConfig3()方法,在testAnnotationConfig3()方法中,咱们只建立Spring容器,以下所示。
@Test public void testAnnotationConfig3(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); }
此时,咱们运行SpringBeanTest类中的testAnnotationConfig3()方法,输出的结果信息以下所示。
从输出的结果信息能够看出,Spring容器在建立的时候,就将@Scope注解标注为singleton的组件进行了实例化,并加载到Spring容器中。
接下来,咱们运行SpringBeanTest类中的testAnnotationConfig2(),结果信息以下所示。
说明,Spring容器在启动时,将单实例组件实例化以后,加载到Spring容器中,之后每次从容器中获取组件实例对象,直接返回相应的对象,而没必要在建立新对象。
若是咱们将对象的做用域修改为多实例,那何时建立对象呢?
此时,咱们将PersonConfig2类的Person对象的做用域修改为多实例,以下所示。
@Configuration public class PersonConfig2 { @Scope("prototype") @Bean("person") public Person person(){ System.out.println("给容器中添加Person...."); return new Person("binghe002", 18); } }
咱们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,输出的结果信息以下所示。
能够看到,终端并无输出任何信息,说明在建立Spring容器时,并不会实例化和加载多实例对象,那多实例对象是何时实例化的呢?接下来,咱们在SpringBeanTest类中的testAnnotationConfig3()方法中添加一行获取Person对象的代码,以下所示。
@Test public void testAnnotationConfig3(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); Object person1 = context.getBean("person"); }
此时,咱们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,结果信息以下所示。
从结果信息中,能够看出,当向Spring容器中获取Person实例对象时,Spring容器实例化了Person对象,并将其加载到Spring容器中。
那么,问题来了,此时Spring容器是否只实例化一个Person对象呢?咱们在SpringBeanTest类中的testAnnotationConfig3()方法中再添加一行获取Person对象的代码,以下所示。
@Test public void testAnnotationConfig3(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); Object person1 = context.getBean("person"); Object person2 = context.getBean("person"); }
此时,咱们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,结果信息以下所示。
从输出结果能够看出,当对象的Scope做用域为多实例时,每次向Spring容器获取对象时,都会建立一个新的对象并返回。此时,获取到的person1和person2就不是同一个对象了,咱们也能够打印结果信息来进行验证,此时在SpringBeanTest类中的testAnnotationConfig3()方法中打印两个对象是否相等,以下所示。
@Test public void testAnnotationConfig3(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); Object person1 = context.getBean("person"); Object person2 = context.getBean("person"); System.out.println(person1 == person2); }
此时,咱们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,结果信息以下所示。
能够看到,当对象是多实例时,每次从Spring容器中获取对象时,都会建立新的实例对象,而且每一个实例对象都不相等。
单例bean是整个应用共享的,因此须要考虑到线程安全问题,以前在玩springmvc的时候,springmvc中controller默认是单例的,有些开发者在controller中建立了一些变量,那么这些变量实际上就变成共享的了,controller可能会被不少线程同时访问,这些线程并发去修改controller中的共享变量,可能会出现数据错乱的问题;因此使用的时候须要特别注意。
多例bean每次获取的时候都会从新建立,若是这个bean比较复杂,建立时间比较长,会影响系统的性能,这个地方须要注意。
若是Spring内置的几种sope都没法知足咱们的需求的时候,咱们能够自定义bean的做用域。
自定义Scope主要分为三个步骤,以下所示。
(1)实现Scope接口
咱们先来看下Scope接口的定义,以下所示。
package org.springframework.beans.factory.config; import org.springframework.beans.factory.ObjectFactory; import org.springframework.lang.Nullable; public interface Scope { /** * 返回当前做用域中name对应的bean对象 * name:须要检索的bean的名称 * objectFactory:若是name对应的bean在当前做用域中没有找到,那么能够调用这个ObjectFactory来建立这个对象 **/ Object get(String name, ObjectFactory<?> objectFactory); /** * 将name对应的bean从当前做用域中移除 **/ @Nullable Object remove(String name); /** * 用于注册销毁回调,若是想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义做用域选择是否是要销毁相应的对象 */ void registerDestructionCallback(String name, Runnable callback); /** * 用于解析相应的上下文数据,好比request做用域将返回request中的属性。 */ @Nullable Object resolveContextualObject(String key); /** * 做用域的会话标识,好比session做用域将是sessionId */ @Nullable String getConversationId(); }
(2)将Scope注册到容器
须要调用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下这个方法的声明
/** * 向容器中注册自定义的Scope *scopeName:做用域名称 * scope:做用域对象 **/ void registerScope(String scopeName, Scope scope);
(3)使用自定义的做用域
定义bean的时候,指定bean的scope属性为自定义的做用域名称。
例如,咱们来实现一个线程级别的bean做用域,同一个线程中同名的bean是同一个实例,不一样的线程中的bean是不一样的实例。
这里,要求bean在线程中是共享的,因此咱们能够经过ThreadLocal来实现,ThreadLocal能够实现线程中数据的共享。
此时,咱们在io.mykit.spring.plugins.register.scope包下新建ThreadScope类,以下所示。
package io.mykit.spring.plugins.register.scope; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.Scope; import org.springframework.lang.Nullable; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * 自定义本地线程级别的bean做用域,不一样的线程中对应的bean实例是不一样的,同一个线程中同名的bean是同一个实例 */ public class ThreadScope implements Scope { public static final String THREAD_SCOPE = "thread"; private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() { @Override protected Object initialValue() { return new HashMap<>(); } }; @Override public Object get(String name, ObjectFactory<?> objectFactory) { Object bean = beanMap.get().get(name); if (Objects.isNull(bean)) { bean = objectFactory.getObject(); beanMap.get().put(name, bean); } return bean; } @Nullable @Override public Object remove(String name) { return this.beanMap.get().remove(name); } @Override public void registerDestructionCallback(String name, Runnable callback) { //bean做用域范围结束的时候调用的方法,用于bean清理 System.out.println(name); } @Nullable @Override public Object resolveContextualObject(String key) { return null; } @Nullable @Override public String getConversationId() { return Thread.currentThread().getName(); } }
在ThreadScope类中,咱们定义了一个常量THREAD_SCOPE,在定义bean的时候给scope使用。
接下来,咱们在io.mykit.spring.plugins.register.config包下建立PersonConfig3类,并使用@Scope("thread")注解标注Person对象的做用域为Thread范围,以下所示。
package io.mykit.spring.plugins.register.config; import io.mykit.spring.bean.Person; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; /** * @author binghe * @version 1.0.0 * @description 测试@Scope注解设置的做用域 */ @Configuration public class PersonConfig3 { @Scope("thread") @Bean("person") public Person person(){ System.out.println("给容器中添加Person...."); return new Person("binghe002", 18); } }
最后,咱们在SpringBeanTest类中建立testAnnotationConfig4()方法,在testAnnotationConfig4()方法中建立Spring容器,并向Spring容器中注册ThreadScope对象,接下来,使用循环建立两个Thread线程,并分别在每一个线程中获取两个Person对象,以下所示。
@Test public void testAnnotationConfig4(){ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class); //向容器中注册自定义的scope context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope()); //使用容器获取bean for (int i = 0; i < 2; i++) { new Thread(() -> { System.out.println(Thread.currentThread() + "," + context.getBean("person")); System.out.println(Thread.currentThread() + "," + context.getBean("person")); }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
此时,咱们运行SpringBeanTest类的testAnnotationConfig4()方法,输出的结果信息以下所示。
从输出中能够看到,bean在一样的线程中获取到的是同一个bean的实例,不一样的线程中bean的实例是不一样的。
注意:这里,我将Person类进行了相应的调整,去掉Lombok的注解,手动写构造函数和setter与getter方法,以下所示。
package io.mykit.spring.bean; import java.io.Serializable; /** * @author binghe * @version 1.0.0 * @description 测试实体类 */ public class Person implements Serializable { private static final long serialVersionUID = 7387479910468805194L; private String name; private Integer age; public Person() { } public Person(String name, Integer age) { this.name = name; this.age = 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; } }
好了,我们今天就聊到这儿吧!别忘了给个在看和转发,让更多的人看到,一块儿学习一块儿进步!!
项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation
若是以为文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习Spring注解驱动开发。公众号回复“spring注解”关键字,领取Spring注解驱动开发核心知识图,让Spring注解驱动开发再也不迷茫。