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註解驅動開發再也不迷茫。