【Spring註解驅動開發】使用@Scope註解設定元件的作用域

冰河團隊發表於2020-06-08

寫在前面

Spring容器中的元件預設是單例的,在Spring啟動時就會例項化並初始化這些物件,將其放到Spring容器中,之後,每次獲取物件時,直接從Spring容器中獲取,而不再建立物件。如果每次從Spring容器中獲取物件時,都要建立一個新的例項物件,該如何處理呢?此時就需要使用@Scope註解設定元件的作用域。

專案工程原始碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

本文內容概覽

  • @Scope註解概述
  • 單例項bean作用域
  • 多例項bean作用域
  • 單例項bean作用域如何建立物件?
  • 多例項bean作用域如何建立物件?
  • 單例項bean注意的事項
  • 多例項bean注意的事項
  • 自定義Scope的實現

@Scope註解概述

@Scope註解能夠設定元件的作用域,我們先來看@Scope註解類的原始碼,如下所示。

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註解中可以設定如下值。

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。這些都是什麼鬼?別急,我們來一個個檢視。

首先,我們進入到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。

綜上,在@Scope註解中的取值如下所示。

  • singleton:表示元件在Spring容器中是單例項的,這個是Spring的預設值,Spring在啟動的時候會將元件進行例項化並載入到Spring容器中,之後,每次從Spring容器中獲取元件時,直接將例項物件返回,而不必再次建立例項物件。從Spring容器中獲取物件,小夥伴們可以理解為從Map物件中獲取物件。
  • prototype:表示元件在Spring容器中是多例項的,Spring在啟動的時候並不會對元件進行例項化操作,而是每次從Spring容器中獲取元件物件時,都會建立一個新的例項物件並返回。
  • request:每次請求都會建立一個新的例項物件,request作用域用在spring容器的web環境中。
  • session:在同一個session範圍內,建立一個新的例項物件,也是用在web環境中。
  • application:全域性web應用級別的作用於,也是在web環境中使用的,一個web應用程式對應一個bean例項,通常情況下和singleton效果類似的,不過也有不一樣的地方,singleton是每個spring容器中只有一個bean例項,一般我們的程式只有一個spring容器,但是,一個應用程式中可以建立多個spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的時候,不管應用中有多少個spring容器,這個應用中同名的bean只有一個。

其中,request和session作用域是需要Web環境支援的,這兩個值基本上使用不到,如果我們使用Web容器來執行Spring應用時,如果需要將元件的例項物件的作用域設定為request和session,我們通常會使用request.setAttribute("key",object)和session.setAttribute("key", object)的形式來將物件例項設定到request和session中,通常不會使用@Scope註解來進行設定。

單例項bean作用域

首先,我們在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容器中獲取例項物件,直接將物件返回,而不必在建立新物件例項。

多例項bean作用域

修改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物件已經不是同一個物件了。

單例項bean作用域何時建立物件?

接下來,我們驗證下在單例項作用域下,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容器中,以後每次從容器中獲取元件例項物件,直接返回相應的物件,而不必在建立新物件。

多例項bean作用域何時建立物件?

如果我們將物件的作用域修改成多例項,那什麼時候建立物件呢?

此時,我們將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注意的事項

單例bean是整個應用共享的,所以需要考慮到執行緒安全問題,之前在玩springmvc的時候,springmvc中controller預設是單例的,有些開發者在controller中建立了一些變數,那麼這些變數實際上就變成共享的了,controller可能會被很多執行緒同時訪問,這些執行緒併發去修改controller中的共享變數,可能會出現資料錯亂的問題;所以使用的時候需要特別注意。

多例項bean注意的事項

多例bean每次獲取的時候都會重新建立,如果這個bean比較複雜,建立時間比較長,會影響系統的效能,這個地方需要注意。

自定義Scope

如果Spring內建的幾種sope都無法滿足我們的需求的時候,我們可以自定義bean的作用域。

1.如何實現自定義Scope

自定義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屬性為自定義的作用域名稱。

2.自定義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註解驅動開發不再迷茫。

相關文章