轉自:
https://www.jianshu.com/p/54b0711a8ec8
1. 問題,Spring管理的某個Bean需要使用多例
在使用了Spring的web工程中,除非特殊情況,我們都會選擇使用Spring的IOC功能來管理Bean,而不是用到時去new一個。Spring管理的Bean默認是單例的(即Spring創建好Bean,需要時就拿來用,而不是每次用到時都去new,又快性能又好),但有時候單例並不滿足要求(比如Bean中不全是方法,有成員,使用單例會有線程安全問題,可以搜索線程安全與線程不安全的相關文章),你上網可以很容易找到解決辦法,即使用@Scope("prototype")
注解,可以通知Spring把被注解的Bean變成多例,如下所示:
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/testScope") public class TestScope { private String name; @RequestMapping(value = "/{username}",method = RequestMethod.GET) public void userProfile(@PathVariable("username") String username) { name = username; try { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getId() + "name:" + name); Thread.sleep(2000); } } catch (Exception e) { e.printStackTrace(); } return; } }
分別發送請求http://localhost:8043/testScope/aaa
和http://localhost:8043/testScope/bbb
,控制台輸出:
34name:aaa 34name:aaa 35name:bbb 34name:bbb
(34和35是兩個線程的ID,每次運行都可能不同,但是兩個請求使用的線程的ID肯定不一樣,可以用來區分兩個請求。)可以看到第二個請求bbb開始后,將name的內容改為了“bbb”,第一個請求的name也從“aaa”改為了“bbb”。要想避免這種情況,可以使用@Scope("prototype")
,注解加在TestScope這個類上。加完注解后重復上面的請求,發現第一個請求一直輸出“aaa”,第二個請求一直輸出“bbb”,成功。
2. 問題升級,多個Bean的依賴鏈中,有一個需要多例
第一節中是一個很簡單的情況,真實的Spring Web工程起碼有Controller、Service、Dao三層,假如Controller層是單例,Service層需要多例,這時候應該怎么辦呢?
2.1 一次失敗的嘗試
首先我們想到的是在Service層加注解@Scope("prototype")
,如下所示:
controller類代碼
import com.example.test.service.Order; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/testScope") public class TestScope { @Autowired private Order order; private String name; @RequestMapping(value = "/{username}", method = RequestMethod.GET) public void userProfile(@PathVariable("username") String username) { name = username; order.setOrderNum(name); try { for (int i = 0; i < 100; i++) { System.out.println( Thread.currentThread().getId() + "name:" + name + "--order:" + order.getOrderNum()); Thread.sleep(2000); } } catch (Exception e) { e.printStackTrace(); } return; } }
Service類代碼
import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; @Service @Scope("prototype") public class Order { private String orderNum; public String getOrderNum() { return orderNum; } public void setOrderNum(String orderNum) { this.orderNum = orderNum; } @Override public String toString() { return "Order{" + "orderNum='" + orderNum + '\'' + '}'; } }
分別發送請求http://localhost:8043/testScope/aaa
和http://localhost:8043/testScope/bbb
,控制台輸出:
32name:aaa--order:aaa 32name:aaa--order:aaa 34name:bbb--order:bbb 32name:bbb--order:bbb
可以看到Controller的name和Service的orderNum都被第二個請求從“aaa”改成了“bbb”,Service並不是多例,失敗。
2.2 一次成功的嘗試
我們再次嘗試,在Controller和Service都加上@Scope("prototype")
,結果成功,這里不重復貼代碼,讀者可以自己試試。
2.3 成功的原因(對2.1、2.2的理解)
Spring定義了多種作用域,可以基於這些作用域創建bean,包括:
- 單例( Singleton):在整個應用中,只創建bean的一個實例。
- 原型( Prototype):每次注入或者通過Spring應用上下文獲取的時候,都會創建一個新的bean實例。
對於以上說明,我們可以這樣理解:雖然Service是多例的,但是Controller是單例的。如果給一個組件加上@Scope("prototype")
注解,每次請求它的實例,spring的確會給返回一個新的。問題是這個多例對象Service是被單例對象Controller依賴的。而單例服務Controller初始化的時候,多例對象Service就已經注入了;當你去使用Controller的時候,Service也不會被再次創建了(注入時創建,而注入只有一次)。
2.4 另一種成功的嘗試(基於2.3的猜想)
為了驗證2.3的猜想,我們在Controller鍾每次去請求獲取Service實例,而不是使用@Autowired
注入,代碼如下:
Controller類
import com.example.test.service.Order; import com.example.test.utils.SpringBeanUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/testScope") public class TestScope { private String name; @RequestMapping(value = "/{username}", method = RequestMethod.GET) public void userProfile(@PathVariable("username") String username) { name = username; Order order = SpringBeanUtil.getBean(Order.class); order.setOrderNum(name); try { for (int i = 0; i < 100; i++) { System.out.println( Thread.currentThread().getId() + "name:" + name + "--order:" + order.getOrderNum()); Thread.sleep(2000); } } catch (Exception e) { e.printStackTrace(); } return; } }
用於獲取Spring管理的Bean的類
package com.example.test.utils; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringBeanUtil implements ApplicationContextAware { /** * 上下文對象實例 */ private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 獲取applicationContext * * @return */ public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 通過name獲取 Bean. * * @param name * @return */ public static Object getBean(String name) { return getApplicationContext().getBean(name); } /** * 通過class獲取Bean. * * @param clazz * @param <T> * @return */ public static <T> T getBean(Class<T> clazz) { return getApplicationContext().getBean(clazz); } /** * 通過name,以及Clazz返回指定的Bean * * @param name * @param clazz * @param <T> * @return */ public static <T> T getBean(String name, Class<T> clazz) { return getApplicationContext().getBean(name, clazz); } }
Order的代碼不變。
分別發送請求http://localhost:8043/testScope/aaa
和http://localhost:8043/testScope/bbb
,控制台輸出:
31name:aaa--order:aaa 33name:bbb--order:bbb 31name:bbb--order:aaa 33name:bbb--order:bbb
可以看到,第二次請求的不會改變第一次請求的name和orderNum。問題解決。我們在2.3節中給出的的理解是對的。
3. Spring給出的解決問題的辦法(解決Bean鏈中某個Bean需要多例的問題)
雖然第二節解決了問題,但是有兩個問題:
- 方法一,為了一個多例,讓整個一串Bean失去了單例的優勢;
- 方法二,破壞IOC注入的優美展現形式,和new一樣不便於管理和修改。
Spring作為一個優秀的、用途廣、發展時間長的框架,一定有成熟的解決辦法。經過一番搜索,我們發現,注解@Scope("prototype")
(這個注解實際上也可以寫成@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE
,使用常量比手打字符串不容易出錯)還有很多用法。
首先value就分為四類:
- ConfigurableBeanFactory.SCOPE_PROTOTYPE,即“prototype”
- ConfigurableBeanFactory.SCOPE_SINGLETON,即“singleton”
- WebApplicationContext.SCOPE_REQUEST,即“request”
- WebApplicationContext.SCOPE_SESSION,即“session”
他們的含義是:
- singleton和prototype分別代表單例和多例;
- request表示請求,即在一次http請求中,被注解的Bean都是同一個Bean,不同的請求是不同的Bean;
- session表示會話,即在同一個會話中,被注解的Bean都是使用的同一個Bean,不同的會話使用不同的Bean。
使用session和request產生了一個新問題,生成controller的時候需要service作為controller的成員,但是service只在收到請求(可能是request也可能是session)時才會被實例化,controller拿不到service實例。為了解決這個問題,@Scope
注解添加了一個proxyMode的屬性,有兩個值ScopedProxyMode.INTERFACES
和ScopedProxyMode.TARGET_CLASS
,前一個表示表示Service是一個接口,后一個表示Service是一個類。
本文遇到的問題中,將@Scope
注解改成@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
就可以了,這里就不重復貼代碼了。
問題解決。
參考:
作者:貓仙草
鏈接:https://www.jianshu.com/p/54b0711a8ec8
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。