1. 問題夺巩,Spring管理的某個Bean需要使用多例
??在使用了Spring的web工程中贞让,除非特殊情況,我們都會選擇使用Spring的IOC功能來管理Bean劲够,而不是用到時去new一個震桶。Spring管理的Bean默認是單例的(即Spring創(chuàng)建好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;
}
}
??分別發(fā)送請求http://localhost:8043/testScope/aaa
和http://localhost:8043/testScope/bbb
,控制臺輸出:
34name:aaa
34name:aaa
35name:bbb
34name:bbb
??(34和35是兩個線程的ID爹土,每次運行都可能不同甥雕,但是兩個請求使用的線程的ID肯定不一樣,可以用來區(qū)分兩個請求胀茵。)可以看到第二個請求bbb開始后社露,將name的內容改為了“bbb”,第一個請求的name也從“aaa”改為了“bbb”琼娘。要想避免這種情況峭弟,可以使用@Scope("prototype")
,注解加在TestScope這個類上附鸽。加完注解后重復上面的請求,發(fā)現(xiàn)第一個請求一直輸出“aaa”瞒瘸,第二個請求一直輸出“bbb”坷备,成功。
2. 問題升級情臭,多個Bean的依賴鏈中击你,有一個需要多例
??第一節(jié)中是一個很簡單的情況,真實的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 + '\'' +
'}';
}
}
??分別發(fā)送請求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定義了多種作用域,可以基于這些作用域創(chuàng)建bean脖祈,包括:
- 單例( Singleton):在整個應用中肆捕,只創(chuàng)建bean的一個實例。
- 原型( Prototype):每次注入或者通過Spring應用上下文獲取的時候盖高,都會創(chuàng)建一個新的bean實例慎陵。
??對于以上說明,我們可以這樣理解:雖然Service是多例的喻奥,但是Controller是單例的席纽。如果給一個組件加上@Scope("prototype")
注解,每次請求它的實例映凳,spring的確會給返回一個新的胆筒。問題是這個多例對象Service是被單例對象Controller依賴的。而單例服務Controller初始化的時候,多例對象Service就已經注入了仆救;當你去使用Controller的時候抒和,Service也不會被再次創(chuàng)建了(注入時創(chuàng)建,而注入只有一次)彤蔽。
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的代碼不變镊辕。
??分別發(fā)送請求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節(jié)中給出的的理解是對的揩悄。
3. Spring給出的解決問題的辦法(解決Bean鏈中某個Bean需要多例的問題)
??雖然第二節(jié)解決了問題卖哎,但是有兩個問題:
- 方法一,為了一個多例删性,讓整個一串Bean失去了單例的優(yōu)勢亏娜;
- 方法二,破壞IOC注入的優(yōu)美展現(xiàn)形式蹬挺,和new一樣不便于管理和修改维贺。
??Spring作為一個優(yōu)秀的、用途廣巴帮、發(fā)展時間長的框架溯泣,一定有成熟的解決辦法。經過一番搜索晰韵,我們發(fā)現(xiàn)发乔,注解@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)
就可以了院刁,這里就不重復貼代碼了佛点。
??問題解決。
參考: