Spring之Bean的作用域

常見的scope

Spring及其其他組件提供了多種Scope,但是我們在使用Spring和他們的組件時用的最多的Scope只有幾個。

  1. singleton:Spring默認的Scope,表示在Spring同一容器中只會存在一個實例,它會在Spring第一次創(chuàng)建完成之后緩存起來,后面都不會再創(chuàng)建,再次獲取時會從緩存中獲取。這個也是目前使用最多的一個Scope。
  2. prototype:該Scope表示每次獲取該范圍內(nèi)的實例都會生成一個新的實例。
  3. request:表示每個request作用域內(nèi)只會創(chuàng)建一次。
  4. session:表示在每個session作用域內(nèi)只會創(chuàng)建一次。
  5. application:表示在ServletContext作用域內(nèi)只會創(chuàng)建一次。

上面這些作用域就是我們平常接觸過最多的作用域了,而在Spring中只提供singleton和prototype兩種,而后面的三種都是在web環(huán)境中才提供的。

設置Bean的作用域

在定義Spring Bean的時候可以設置Bean的作用域,常用的就是在xml定義Bean的時候設置或者使用@Scope在注解中設置Bean的作用域。

xml設置:

<bean id="person" class="com.buydeem.bean.Person" scope="singleton">
    <constructor-arg name="name" value="mac"/>
    <constructor-arg name="age" value="18"/>
</bean>

@Scope設置:

@Bean
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
public User user(){
    User user = new User();
    user.setName("mac");
    user.setAge(18);
    return user;
}

使用示例

public class ScopeDemo1 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(ConfigClazz.class);
        context.refresh();
        for (int i = 0; i < 5; i++) {
            User mac = (User) context.getBean("mac");
            User tom = (User) context.getBean("tom");
            System.out.println("mac = "+mac);
            System.out.println("tom = "+tom);
        }
    }
}

class ConfigClazz{
    @Bean
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
    public User mac(){
        User user = new User();
        user.setName("mac");
        user.setAge(18);
        return user;
    }
    @Bean
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public User tom(){
        User user = new User();
        user.setName("tom");
        user.setAge(19);
        return user;
    }
}
@Getter
@Setter
class User {
    private String name;
    private Integer age;
}

上面我們定義了兩個User類型的Bean,但是它們的Scope是不一樣的,一個為singleton,另一個為prototype。下面我們從容器中循環(huán)獲取5次實例,看看每次獲取的實例結(jié)果有什么不同。

mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@7e057f43
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@6c284af
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@5890e879
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@6440112d
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@31ea9581

從打印的結(jié)果可以看出,scope為singleton的實例每次獲取的都是同一個對象,而prototype每次獲取的實例都是不同的。

如何自定義Scope

如果Spring提供的Scope無法滿足我們的要求,我們是可以自定義Scope的。在說如何定義Scope之前我們了解下面幾個知識點。

如何注冊Scope

自定義的Scope如果只是創(chuàng)建肯定是沒有用,要想自定義的Scope生效必須先將自定義的Scope注冊到容器中。Spring中提供Scope注冊的接口為ConfigurableBeanFactory,該接口中定義了注冊Scope的方法,定義如下:

void registerScope(String scopeName, Scope scope);

該接口的只需要我們提供Scope的名字和Scope實例對象即可,而它的實現(xiàn)在AbstractBeanFactory中,且在Spring中只有一處實現(xiàn),具體實現(xiàn)如下:

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 + "]");
      }
   }
}

從源碼我們可以了解到,對于singleton和prototype這兩個Scope我們是不能自定義的,而其他的Scope我們可以自己定義,且還能覆蓋之前的實現(xiàn)。而它的內(nèi)部只是使用了一個LinkedHashMap來存放這些Scope。
同樣我們還可以使用CustomScopeConfigurer來完成自定義Scope的注冊。查看其源碼,其核心還是通過registerScope該方法來向容器注冊Scope。該類實現(xiàn)了BeanFactoryPostProcessor接口,而實現(xiàn)了該接口的類可以在BeanFactory實例化之后對象還沒創(chuàng)建之前執(zhí)行我們自己的擴展。通過postProcessBeanFactory方法的實現(xiàn)可以了解該類是如何將自定義的Scope注冊到BeanFactory中的。

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
   if (this.scopes != null) {
      this.scopes.forEach((scopeKey, value) -> {
         if (value instanceof Scope) {
            beanFactory.registerScope(scopeKey, (Scope) value);
         }
         else if (value instanceof Class) {
            Class<?> scopeClass = (Class<?>) value;
            Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class");
            beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass));
         }
         else if (value instanceof String) {
            Class<?> scopeClass = ClassUtils.resolveClassName((String) value, this.beanClassLoader);
            Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class");
            beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass));
         }
         else {
            throw new IllegalArgumentException("Mapped value [" + value + "] for scope key [" +
                  scopeKey + "] is not an instance of required type [" + Scope.class.getName() +
                  "] or a corresponding Class or String value indicating a Scope implementation");
         }
      });
   }
}

通過內(nèi)部實現(xiàn),可以看見其最后還是調(diào)用的registerScope方法對我們定義的Scope進行注冊。
所以我們在自定義Scope的時候,既可以自己調(diào)用registerScope方法手動注冊,同樣還可以使用CustomScopeConfigurer來完成自定義Scope的注冊。

Scope接口

如果想自定義Scope,我們自定義的Scope就必須實現(xiàn)Scope接口。該接口的定義如下:

public interface Scope {
    /**
     * 獲取該Scope中的實例,如果不存在,則會調(diào)用objectFactory創(chuàng)建
     */
    Object get(String name, ObjectFactory<?> objectFactory);
    /**
     * 刪除該Scope中的實例
     */
    @Nullable
    Object remove(String name);
    /**
     * 注冊實例銷毀回調(diào)邏輯
     */
    void registerDestructionCallback(String name, Runnable callback);
    /**
     * 用于解析相應的上下文數(shù)據(jù),比如request作用域?qū)⒎祷豶equest中的屬性
     */
    @Nullable
    Object resolveContextualObject(String key);
    /**
     * 作用域的會話標識,比如session作用域?qū)⑹莝essionId
     */
    @Nullable
    String getConversationId();
}

通常我們自定義Scope主要就是實現(xiàn)get和remove方法,而其他方法我們可以根據(jù)自己的情況來決定要不要實現(xiàn)。

ScopedProxyMode-作用域代理模式

在Scope的注解中有一個proxyMode可以設置,該值主要用來設置代理模式。該值在Spring中有四個取值,可以通過ScopedProxyMode枚舉類查看所有的取值。其中No和DEFAULT取值的效果相同,另外還有兩種分別為INTERFACES和TARGET_CLASS。INTERFACES代表使用JDK原生的方式來實現(xiàn)動態(tài)代理,而TARGET_CLASS代表使用CGLIB來實現(xiàn)動態(tài)代理。

知道了ScopeProxyMode四種值的區(qū)別,但是為什么要用代理呢?例如在Web環(huán)境中,我定義了一個Person的是Scope為session,也就是說對于同一個會話獲取到的Person實例是同一個。但是現(xiàn)在存在一個問題,我應用啟動時,這個時候沒有用戶訪問,如果我把Person實例注入到別的實例中,這個時候豈不是不能注入了。但是實際情況是可以的,而且注入的那個Person實例還不是null。實際上Spring注入的是一個代理對象,而這個代理對象是通過JDK還是CGLIB實現(xiàn)的,這個就取決于我們設置的ScopeProxyMode的值了。

Person的定義如下:

@Component
@Scope(scopeName = "session",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Person {
}

ScopeProxyModeTest定義如下:

@Component
public class ScopeProxyModeTest {
    @Autowired
    private Person person;
    @PostConstruct
    public void init(){
        System.out.println(person.getClass());
    }
}

程序啟動后,可以看見打印出來的結(jié)果如下:

class com.buydeem.springbootdemo.service.Person$$EnhancerBySpringCGLIB$$32dc2cee

從打印的結(jié)果可以看出,它是代理對象。上面Person中的Scope是我們手動設置的ScopeName和proxyMode屬性值,SpringWeb中其實已經(jīng)提供了@SessionScope注解,其定義如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
   /**
    * Alias for {@link Scope#proxyMode}.
    * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
    */
   @AliasFor(annotation = Scope.class)
   ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

從SessionScope注解的定義也可以看出,它默認指定的proxyMode就是TARGET_CLASS,也就是使用CGLIB的方式。關于JDK和CGLIB代理的不同,還請自行查詢資料了解。

實現(xiàn)自定義Scope

如何實現(xiàn)自定義Scope簡單的說就只有兩步,第一步就對Scope接口進行實現(xiàn),第二部就是將自定義的Scope注冊到容器中。這里我就以SimpleThreadScope作為示例來演示具體如何實現(xiàn)。該類是Spring中提供的一個基于ThreadLocal實現(xiàn)的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();
      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();
   }
}

從源碼中可以知道這是一個基于ThreadLocal實現(xiàn)的Scope,它的效果就是單同一個線程獲取該域中的實例會是相同的,而其他線程獲取的則是不同的。
Spring并沒有將該Scope注冊到容器中,所以我們在使用時需要自己手動將該Scope注入到容器中,注冊和使用的代碼如下:

public class ScopeDemo2 {
    public static void main(String[] args) throws InterruptedException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(Demo2Config.class);
        context.refresh();
        User user = (User) context.getBean("user");
        System.out.printf("main 線程中的user:%s,是否相等:%s\n",user,user == context.getBean("user"));
        new Thread(()->{
            User user_t1 = (User) context.getBean("user");
            System.out.printf("t1 線程中的user:%s,是否相等:%s\n",user_t1,user_t1 == context.getBean("user"));
        },"t1").start();
        new Thread(()->{
            User user_t2 = (User) context.getBean("user");
            System.out.printf("t2 線程中的user:%s,是否相等:%s\n",user_t2,user_t2 == context.getBean("user"));
        },"t2").start();
        Thread.sleep(1000L);
    }
}
class Demo2Config{
    /**
     * 創(chuàng)建CustomScopeConfigurer實例,注冊SimpleThreadScope域
     * @return
     */
    @Bean
    public CustomScopeConfigurer customScopeConfigurer(){
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        //該域名與Bean中指定的域名需要保持一致
        configurer.addScope("threadScope",new SimpleThreadScope());
        return configurer;
    }
    @Bean
    //域名與注冊的域名保持一致
    @Scope(scopeName = "threadScope")
    public User user(){
        return new User();
    }
}

最后打印的結(jié)果如下:

main 線程中的user:com.buydeem.scope.User@76494737,是否相等:true
t2 線程中的user:com.buydeem.scope.User@45d33648,是否相等:true
t1 線程中的user:com.buydeem.scope.User@4a418f9c,是否相等:true

從上面結(jié)果可以看出,同一個線程獲取的實例是相同的,而不同的線程獲取的實例是不同的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內(nèi)容