天天看點

Spring scope解惑

Spring的作用域scope

在2.0之前隻有兩種singleton和prototype(網上說的,沒去驗證),後面增加了session、request、global session三種專門用于web應用程式上下文的Bean

Singleton

這是spring的bean的預設的作用域-單例。但是此單例又與設計模式中的單例模式不一樣,設計模式的單例在設計模式的文章中再介紹。

singleton不要簡單的了解為全局隻有一個,真正的意義是在一個上下文(ApplicationContext)中隻有一個。如果在一個項目中有多個ApplicationContext,那麼擷取的Bean對象就不隻一個了

在同一個容器或上下文中,所有對單例bean的請求,隻要id與該bean定義相比對,都會傳回同一個執行個體。簡單說就是bean定義為singleton時,ioc容器隻會建立該bean的唯一執行個體,然後存儲在單例緩存(singleton cache)中。

singleton的bean是無狀态的,隻有一份,是以無論哪個線程擷取的都是同一個同樣的執行個體,具有線程無關性。Spring使用ThreadLocal解決線程安全問題,這就要求在使用singleton的bean時不能存在屬性的改變,如果存在屬性改變,就需要慎重使用,采用同步來處理多線程問題,或者考慮使用prototype作用域。

基于上面,我們常用的Controller、Service、Repository、Configuration這些bean基本上都是項目啟動就已經初始化完畢,每個bean的屬性都已經确定,在運作過程中也不會更改,這樣就很适合單例模式。這裡再說一下實體類(用@Entity注解修飾的)這個可不是一個bean,試想一下,每次使用實體的時候是不是都是

DomainA a = new DomainA();

,應該沒有人這麼用

@Autowired private DomainA domianA;

使用scope的方法如下:

@Scope("singleton")
@Component
public class MainService {...}
           

Prototype

相對應singleton,prototype就屬于多例模式,每次請求都會建立一個新的執行個體,相當于new操作。

對于prototype類型的bean,spring隻負責建立初始化,不會對整個生命周期負責,随後的所有操作都交給用戶端來處理

現在問一個問題,如何聲明一個prototype的bean并使用呢?(先不要急着往下看,想一下,就以在ScopeTestController裡面注入prototype的PrototypeService來說明)

~
~
~
~
思考中...
~
~
~
~
           

可能很多人(包括我)開始會以為像以下這種寫法就可以

public interface PrototypeService {
}

@Service
@Scope("prototype")
public class PrototypeServiceImpl implements PrototypeService {

}

@RestController
public class ScopeTestController {

    @Autowired
    PrototypeService prototypeService;

    @GetMapping("/")
    public void testPrototype() {
        System.out.println(prototypeService);
    }

}
           

啟動項目,兩次次請求,檢視控制台輸出

[email protected]
[email protected]
           

?一樣?什麼情況?

其實仔細想一下也就明白了錯在哪,Controller是一個單例,在啟動時就已經把Service注入了,是以不可能改變,當然現在你可以這麼改,将Controller同樣改為prototype的。那麼恭喜你回答正确,但是,這不是我們的目的,我們的目的是要看在單例中如何使用。

方法1:從ApplicationContext裡面擷取

将Controller裡面的代碼做以下改動

@RestController
public class ScopeTestController {

//  @Autowired
//  PrototypeService prototypeService;

    @Autowired
    WebApplicationContext applicationContext;

    @GetMapping("/")
    public void testPrototype() {
//      System.out.println(prototypeService);
        System.out.println(applicationContext.getBean(PrototypeService.class));
    }

}
           

這樣每次都從上下文中請求一個執行個體,容器對每個請求都執行個體化一個新的Service,沒有問題

方法2:方法1的變種,采用工廠模式來生成Service

代碼就不寫了...

方法3:使用代理

很簡單,在@Scope裡面加上代理模式即可

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
public class PrototypeServiceImpl implements PrototypeService {

}

@RestController
public class ScopeTestController {

    @Autowired
    PrototypeService prototypeService;

//  @Autowired
//  WebApplicationContext applicationContext;

    @GetMapping("/")
    public void testPrototype() {
        System.out.println(prototypeService);
//      System.out.println(applicationContext.getBean(PrototypeService.class));
    }

}
           

這樣,就可以每次擷取不同的Service了,這個的原理就是,在初始化Controller的時候并不是講一個Service的實體注入,而是注入一個代理,當真正調用Service的時候,代理會對其進行依賴解析,并調用真正的實體bean

額外需要注意的一點是,如果注入的不是接口的實作,而是一個類,那麼需要将

proxyMode = ScopedProxyMode.INTERFACES

改為

proxyMode = ScopedProxyMode.TARGET_CLASS

request

該作用域将 bean 的定義限制為 HTTP 請求。隻在 web-aware Spring ApplicationContext 的上下文中有效

上述的實驗結果是每次請求都會輸出不一樣的結果,在這裡可能會與prototype産生困惑,做以下的實驗可以解決你的困惑

public interface PrototypeService {
}

@Service
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public class PrototypeServiceImpl implements PrototypeService {

}

public interface MiddleService {
    void test();
}

@Service
public class MiddleServiceImpl implements MiddleService {

    @Autowired
    PrototypeService prototypeService;

    @Override
    public void test() {
        System.out.println("middle     : " + prototypeService);
    }
}

@RestController
public class ScopeTestController {

    @Autowired
    MiddleService middleService;

    @Autowired
    PrototypeService prototypeService;

//  @Autowired
//  WebApplicationContext applicationContext;

    @GetMapping("/")
    public void testPrototype() {
        System.out.println("controller : " + prototypeService);
//      System.out.println(applicationContext.getBean(PrototypeService.class));

        middleService.test();
    }

}
           

輸出結果為:

controller : [email protected]
middle     : [email protected]
           

然後将作用域改為prototype再測試一下

輸出結果為:

controller : [email protected]
middle     : [email protected]
           

結果顯而易見

session

該作用域将 bean 的定義限制為 HTTP 會話。 隻在web-aware Spring ApplicationContext的上下文中有效。

上述的實驗結果是一個會話内輸出結果是一樣的

global-session

該作用域将 bean 的定義限制為全局 HTTP 會話。隻在 web-aware Spring ApplicationContext 的上下文中有效。

參考連結:https://www.imooc.com/article/23344

繼續閱讀