Spring系列第6篇:玩轉bean scope,避免跳坑裡

本文內容

  1. 詳細介紹5中bean的sope及使用注意點
  2. 自定義作用域的實現

應用中,有時候我們需要一個對象在整個應用中只有一個,有些對象希望每次使用的時候都重新創建一個,spring對我們這種需求也提供了支持,在spring中這個叫做bean的作用域,xml中定義bean的時候,可以通過scope屬性指定bean的作用域,如:

<code><bean> /<code>

spring容器中scope常見的有5種,下面我們分別來介紹一下。

singleton

當scope的值設置為singleton的時候,整個spring容器中只會存在一個bean實例,通過容器多次查找bean的時候(調用BeanFactory的getBean方法或者bean之間注入依賴的bean對象的時候),返回的都是同一個bean對象,singleton是scope的默認值,所以spring容器中默認創建的bean對象是單例的,通常spring容器在啟動的時候,會將scope為singleton的bean創建好放在容器中(有個特殊的情況,當bean的lazy被設置為true的時候,表示懶加載,那麼使用的時候才會創建),用的時候直接返回。

案例

bean xml配置
<code>
<bean>

    <constructor-arg>
/<bean>/<code>
BeanScopeModel代碼
<code>package com.javacode2018.lesson001.demo4;

public class BeanScopeModel {
    public BeanScopeModel(String beanScope) {
        System.out.println(String.format("create BeanScopeModel,{sope=%s},{this=%s}", beanScope, this));
    }
}/<code>

上面構造方法中輸出了一段文字,一會我們可以根據輸出來看一下這個bean什麼時候創建的,是從容器中獲取bean的時候創建的還是容器啟動的時候創建的。

測試用例
<code>package com.javacode2018.lesson001.demo4;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 
 * 


 * bean作用域
 */
public class ScopeTest {

    ClassPathXmlApplicationContext context;

    @Before
    public void before() {
        System.out.println("spring容器準備啟動.....");
        //1.bean配置文件位置
        String beanXml = "classpath:/com/javacode2018/lesson001/demo4/beans.xml";
        //2.創建ClassPathXmlApplicationContext容器,給容器指定需要加載的bean配置文件
        this.context = new ClassPathXmlApplicationContext(beanXml);


        System.out.println("spring容器啟動完畢!");
    }

    /**
     * 單例bean
     */
    @Test
    public void singletonBean() {
        System.out.println("---------單例bean,每次獲取的bean實例都一樣---------");
        System.out.println(context.getBean("singletonBean"));
        System.out.println(context.getBean("singletonBean"));
        System.out.println(context.getBean("singletonBean"));
    }

}

/<code>

上面代碼中before方法上面有@Before註解,這個是junit提供的功能,這個方法會在所有@Test標註的方法之前之前運行,before方法中我們對容器進行初始化,並且在容器初始化前後輸出了一段文字。

上面代碼中,singletonBean方法中,3次獲取singletonBean對應的bean。

運行測試用例
<code>spring容器準備啟動.....
create BeanScopeModel,{sope=singleton},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@e874448}
spring容器啟動完畢!
---------單例bean,每次獲取的bean實例都一樣---------
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448/<code>
結論

從輸出中得到2個結論

  • 前3行的輸出可以看出,BeanScopeModel的構造方法是在容器啟動過程中調用的,說明這個bean實例在容器啟動過程中就創建好了,放在容器中緩存著
  • 最後3行輸出的是一樣的,說明返回的是同一個bean對象

單例bean使用注意

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

prototype

如果scope被設置為prototype類型的了,表示這個bean是多例的,通過容器每次獲取的bean都是不同的實例,每次獲取都會重新創建一個bean實例對象。

案例

bean xml配置
<code>
<bean>

    <constructor-arg>
/<bean>/<code>
新增一個測試用例

ScopeTest中新增一個方法

<code>/**
 * 多例bean
 */
@Test
public void prototypeBean() {
    System.out.println("---------單例bean,每次獲取的bean實例都一樣---------");
    System.out.println(context.getBean("prototypeBean"));
    System.out.println(context.getBean("prototypeBean"));
    System.out.println(context.getBean("prototypeBean"));
}/<code>
運行測試用例
<code>spring容器準備啟動.....
spring容器啟動完畢!
---------單例bean,每次獲取的bean實例都一樣---------
create BeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@289d1c02}
com.javacode2018.lesson001.demo4.BeanScopeModel@289d1c02
create BeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@22eeefeb}
com.javacode2018.lesson001.demo4.BeanScopeModel@22eeefeb
create BeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@17d0685f}
com.javacode2018.lesson001.demo4.BeanScopeModel@17d0685f/<code>
結論

輸出中可以看出,容器啟動過程中並沒有去創建BeanScopeModel對象,3次獲取prototypeBean得到的都是不同的實例,每次獲取的時候才會去調用構造方法創建bean實例。

多例bean使用注意

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

下面要介紹的3個:request、session、application都是在spring web容器環境中才會有的。

request

當一個bean的作用域為request,表示在一次http請求中,一個bean對應一個實例;對每個http請求都會創建一個bean實例,request結束的時候,這個bean也就結束了,request作用域用在spring容器的web環境中,這個以後講springmvc的時候會說,spring中有個web容器接口WebApplicationContext,這個裡面對request作用域提供了支持,配置方式:

<code><bean>/<code>

session

這個和request類似,也是用在web環境中,session級別共享的bean,每個會話會對應一個bean實例,不同的session對應不同的bean實例,springmvc中我們再細說。

<code><bean>/<code>

application

全局web應用級別的作用於,也是在web環境中使用的,一個web應用程序對應一個bean實例,通常情況下和singleton效果類似的,不過也有不一樣的地方,singleton是每個spring容器中只有一個bean實例,一般我們的程序只有一個spring容器,但是,一個應用程序中可以創建多個spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的時候,不管應用中有多少個spring容器,這個應用中同名的bean只有一個。

<code><bean>/<code>

自定義scope

有時候,spring內置的幾種sope都無法滿足我們的需求的時候,我們可以自定義bean的作用域。

自定義Scope 3步驟

第1步:實現Scope接口

我們來看一下這個接口定義

<code>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();

}/<code>
第2步:將自定義的scope註冊到容器

需要調用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下這個方法的聲明

<code>/**
* 向容器中註冊自定義的Scope
*scopeName:作用域名稱
* scope:作用域對象
**/
void registerScope(String scopeName, Scope scope);/<code>
第3步:使用自定義的作用域

定義bean的時候,指定bean的scope屬性為自定義的作用域名稱。

案例

需求

下面我們來實現一個線程級別的bean作用域,同一個線程中同名的bean是同一個實例,不同的線程中的bean是不同的實例。

實現分析

需求中要求bean在線程中是貢獻的,所以我們可以通過ThreadLocal來實現,ThreadLocal可以實現線程中數據的共享。

下面我們來上代碼。

ThreadScope
<code>package com.javacode2018.lesson001.demo4;

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";//@1

    private ThreadLocal> 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();
    }
}
/<code>

@1:定義了作用域的名稱為一個常量thread,可以在定義bean的時候給scope使用

BeanScopeModel
<code>package com.javacode2018.lesson001.demo4;

public class BeanScopeModel {
    public BeanScopeModel(String beanScope) {
        System.out.println(String.format("線程:%s,create BeanScopeModel,{sope=%s},{this=%s}", Thread.currentThread(), beanScope, this));
    }
}/<code>

上面的構造方法中會輸出當前線程的信息,到時候可以看到創建bean的線程。

bean配置文件

beans-thread.xml內容

<code>
<beans>       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

    
    <bean>
        <constructor-arg>
    /<bean>
/<beans>/<code>

注意上面的scope是我們自定義的,值為thread

測試用例
<code>package com.javacode2018.lesson001.demo4;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.concurrent.TimeUnit;

/**
 * 
 * 


 * 自定義scope
 */
public class ThreadScopeTest {
    public static void main(String[] args) throws InterruptedException {
        String beanXml = "classpath:/com/javacode2018/lesson001/demo4/beans-thread.xml";
        //手動創建容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();
        //設置配置文件位置
        context.setConfigLocation(beanXml);
        //啟動容器
        context.refresh();
        //向容器中註冊自定義的scope


        context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());//@1

        //使用容器獲取bean
        for (int i = 0; i             new Thread(() -> {
                System.out.println(Thread.currentThread() + "," + context.getBean("threadBean"));
                System.out.println(Thread.currentThread() + "," + context.getBean("threadBean"));
            }).start();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

/<code>

注意上面代碼,重點在@1,這個地方向容器中註冊了自定義的ThreadScope。

@2:創建了2個線程,然後在每個線程中去獲取同樣的bean 2次,然後輸出,我們來看一下效果。

運行輸出
<code>線程:Thread[Thread-1,5,main],create BeanScopeModel,{sope=thread},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530}
Thread[Thread-1,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530
Thread[Thread-1,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530
線程:Thread[Thread-2,5,main],create BeanScopeModel,{sope=thread},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da}
Thread[Thread-2,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da
Thread[Thread-2,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da/<code>

從輸出中可以看到,bean在同樣的線程中獲取到的是同一個bean的實例,不同的線程中bean的實例是不同的。


總結

  1. spring容器自帶的有2種作用域,分別是singleton和prototype;還有3種分別是spring web容器環境中才支持的request、session、application
  2. singleton是spring容器默認的作用域,一個spring容器中同名的bean實例只有一個,多次獲取得到的是同一個bean;單例的bean需要考慮線程安全問題
  3. prototype是多例的,每次從容器中獲取同名的bean,都會重新創建一個;多例bean使用的時候需要考慮創建bean對性能的影響
  4. 一個應用中可以有多個spring容器
  5. 自定義scope 3個步驟,實現Scope接口,將實現類註冊到spring容器,使用自定義的sope


分享到:


相關文章: