解決@Scope(“prototype“)不生效的問題
@Scope(“prototype“)不生效
使用spring的時(shí)候,我們一般都是使用@Component實(shí)現(xiàn)bean的注入,這個(gè)時(shí)候我們的bean如果不指定@Scope,默認(rèn)是單例模式,另外還有很多模式可用,用的最多的就是多例模式了,顧名思義就是每次使用都會(huì)創(chuàng)建一個(gè)新的對(duì)象,比較適用于寫一些job,比如在多線程環(huán)境下可以使用全局變量之類的
創(chuàng)建一個(gè)測(cè)試任務(wù),這里在網(wǎng)上看到大部分都是直接@Scope(“prototype”),這里測(cè)試是不生效的,再加上proxyMode才行,代碼如下
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TestAsyncClient {
private int a = 0;
@Async
public void test(int a) {
this.a = a;
CommonAsyncJobs.list.add(this.a + "");
}
}測(cè)試
import cn.hutool.core.collection.CollectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Set;
import java.util.Vector;
@Slf4j
@EnableAsync
@SpringBootTest
@RunWith(SpringRunner.class)
public class CommonAsyncJobs {
@Autowired
private TestAsyncClient testAsyncClient;
// 多線程環(huán)境下,普通的list.add不適用,用Vector處理就行了,效率低,但無所謂反正測(cè)試的
public static Vector<String> list = new Vector<>();
@Test
public void testAsync() throws Exception {
// 循環(huán)里面異步處理
int a = 100000;
for (int i = 0; i < a; i++) {
testAsyncClient.test(i);
}
System.out.println("多線程結(jié)果:" + list.size());
System.out.println("單線程結(jié)果:" + a);
Set<String> set = CollectionUtil.newHashSet();
Set<String> exist = CollectionUtil.newHashSet();
for (String s : list) {
if (set.contains(s)) {
exist.add(s);
} else {
set.add(s);
}
}
// 沒重復(fù)的值,說明多線程環(huán)境下,全局變量沒有問題
System.out.println("重復(fù)的值:" + exist.size());
System.out.println("重復(fù)的值:" + exist);
// 單元測(cè)試內(nèi)主線程結(jié)束會(huì)終止子線程任務(wù)
Thread.sleep(Long.MAX_VALUE);
}
}
結(jié)果
沒重復(fù)的值,說明多線程環(huán)境下,全局變量沒有問題

@Scope(“prototype“)正確用法——解決Bean多例問題
1.問題,Spring管理的某個(gè)Bean需要使用多例
在使用了Spring的web工程中,除非特殊情況,我們都會(huì)選擇使用Spring的IOC功能來管理Bean,而不是用到時(shí)去new一個(gè)。
Spring管理的Bean默認(rèn)是單例的(即Spring創(chuàng)建好Bean,需要時(shí)就拿來用,而不是每次用到時(shí)都去new,又快性能又好),但有時(shí)候單例并不滿足要求(比如Bean中不全是方法,有成員,使用單例會(huì)有線程安全問題,可以搜索線程安全與線程不安全的相關(guān)文章),你上網(wǎng)可以很容易找到解決辦法,即使用@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ā)送請(qǐng)求http://localhost:8043/testScope/aaa和http://localhost:8043/testScope/bbb,控制臺(tái)輸出:
34name:aaa
34name:aaa
35name:bbb
34name:bbb
(34和35是兩個(gè)線程的ID,每次運(yùn)行都可能不同,但是兩個(gè)請(qǐng)求使用的線程的ID肯定不一樣,可以用來區(qū)分兩個(gè)請(qǐng)求。)可以看到第二個(gè)請(qǐng)求bbb開始后,將name的內(nèi)容改為了“bbb”,第一個(gè)請(qǐng)求的name也從“aaa”改為了“bbb”。要想避免這種情況,可以使用@Scope("prototype"),注解加在TestScope這個(gè)類上。加完注解后重復(fù)上面的請(qǐng)求,發(fā)現(xiàn)第一個(gè)請(qǐng)求一直輸出“aaa”,第二個(gè)請(qǐng)求一直輸出“bbb”,成功。
2.問題升級(jí)
多個(gè)Bean的依賴鏈中,有一個(gè)需要多例
第一節(jié)中是一個(gè)很簡(jiǎn)單的情況,真實(shí)的Spring Web工程起碼有Controller、Service、Dao三層,假如Controller層是單例,Service層需要多例,這時(shí)候應(yīng)該怎么辦呢?
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ā)送請(qǐng)求http://localhost:8043/testScope/aaa和http://localhost:8043/testScope/bbb,控制臺(tái)輸出:
32name:aaa--order:aaa
32name:aaa--order:aaa
34name:bbb--order:bbb
32name:bbb--order:bbb
可以看到Controller的name和Service的orderNum都被第二個(gè)請(qǐng)求從“aaa”改成了“bbb”,Service并不是多例,失敗。
2.2 一次成功的嘗試
我們?cè)俅螄L試,在Controller和Service都加上@Scope("prototype"),結(jié)果成功,這里不重復(fù)貼代碼,讀者可以自己試試。
2.3 成功的原因(對(duì)2.1、2.2的理解)
Spring定義了多種作用域,可以基于這些作用域創(chuàng)建bean,包括:
- 單例( Singleton):在整個(gè)應(yīng)用中,只創(chuàng)建bean的一個(gè)實(shí)例。
- 原型( Prototype):每次注入或者通過Spring應(yīng)用上下文獲取的時(shí)候,都會(huì)創(chuàng)建一個(gè)新的bean實(shí)例。
對(duì)于以上說明,我們可以這樣理解:雖然Service是多例的,但是Controller是單例的。如果給一個(gè)組件加上@Scope("prototype")注解,每次請(qǐng)求它的實(shí)例,spring的確會(huì)給返回一個(gè)新的。問題是這個(gè)多例對(duì)象Service是被單例對(duì)象Controller依賴的。而單例服務(wù)Controller初始化的時(shí)候,多例對(duì)象Service就已經(jīng)注入了;當(dāng)你去使用Controller的時(shí)候,Service也不會(huì)被再次創(chuàng)建了(注入時(shí)創(chuàng)建,而注入只有一次)。
2.4 另一種成功的嘗試(基于2.3的猜想)
為了驗(yàn)證2.3的猜想,我們?cè)贑ontroller鐘每次去請(qǐng)求獲取Service實(shí)例,而不是使用@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 {
/**
* 上下文對(duì)象實(shí)例
*/
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ā)送請(qǐng)求http://localhost:8043/testScope/aaa和http://localhost:8043/testScope/bbb,控制臺(tái)輸出:
31name:aaa--order:aaa
33name:bbb--order:bbb
31name:bbb--order:aaa
33name:bbb--order:bbb
可以看到,第二次請(qǐng)求的不會(huì)改變第一次請(qǐng)求的name和orderNum。問題解決。我們?cè)?.3節(jié)中給出的的理解是對(duì)的。
3. Spring給出的解決問題的辦法(解決Bean鏈中某個(gè)Bean需要多例的問題)
雖然第二節(jié)解決了問題,但是有兩個(gè)問題:
- 方法一,為了一個(gè)多例,讓整個(gè)一串Bean失去了單例的優(yōu)勢(shì);
- 方法二,破壞IOC注入的優(yōu)美展現(xiàn)形式,和new一樣不便于管理和修改。
Spring作為一個(gè)優(yōu)秀的、用途廣、發(fā)展時(shí)間長(zhǎng)的框架,一定有成熟的解決辦法。經(jīng)過一番搜索,我們發(fā)現(xiàn),注解@Scope("prototype")(這個(gè)注解實(shí)際上也可以寫成@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,使用常量比手打字符串不容易出錯(cuò))還有很多用法。
首先value就分為四類:
ConfigurableBeanFactory.SCOPE_PROTOTYPE,即“prototype”ConfigurableBeanFactory.SCOPE_SINGLETON,即“singleton”WebApplicationContext.SCOPE_REQUEST,即“request”WebApplicationContext.SCOPE_SESSION,即“session”
他們的含義是:
singleton和prototype分別代表單例和多例;request表示請(qǐng)求,即在一次http請(qǐng)求中,被注解的Bean都是同一個(gè)Bean,不同的請(qǐng)求是不同的Bean;session表示會(huì)話,即在同一個(gè)會(huì)話中,被注解的Bean都是使用的同一個(gè)Bean,不同的會(huì)話使用不同的Bean。
使用session和request產(chǎn)生了一個(gè)新問題,生成controller的時(shí)候需要service作為controller的成員,但是service只在收到請(qǐng)求(可能是request也可能是session)時(shí)才會(huì)被實(shí)例化,controller拿不到service實(shí)例。為了解決這個(gè)問題,@Scope注解添加了一個(gè)proxyMode的屬性,有兩個(gè)值ScopedProxyMode.INTERFACES和ScopedProxyMode.TARGET_CLASS,前一個(gè)表示表示Service是一個(gè)接口,后一個(gè)表示Service是一個(gè)類。
本文遇到的問題中,將@Scope注解改成@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)就可以了,這里就不重復(fù)貼代碼了。
問題解決。以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java時(shí)間戳轉(zhuǎn)日期格式的實(shí)現(xiàn)代碼
本篇文章是對(duì)java時(shí)間戳轉(zhuǎn)日期格式的實(shí)現(xiàn)代碼進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
Java獲得當(dāng)前時(shí)間前指定幾個(gè)小時(shí)具體時(shí)間的方法示例
這篇文章主要介紹了Java獲得當(dāng)前時(shí)間前指定幾個(gè)小時(shí)具體時(shí)間的方法,涉及java使用Calendar針對(duì)日期時(shí)間的相關(guān)運(yùn)算與轉(zhuǎn)換操作技巧,需要的朋友可以參考下2017-08-08
Java8函數(shù)式編程應(yīng)用小結(jié)
Java8非常重要的就是引入了函數(shù)式編程的思想,使得這門經(jīng)典的面向?qū)ο笳Z(yǔ)言有了函數(shù)式的編程方式,彌補(bǔ)了很大程度上的不足,函數(shù)式思想在處理復(fù)雜問題上有著更為令人稱贊的特性,本文給大家介紹Java8函數(shù)式編程應(yīng)用小結(jié),感興趣的朋友一起看看吧2023-12-12
Java?ServletContext與ServletConfig接口使用教程
ServletConfig對(duì)象,叫Servlet配置對(duì)象。主要用于加載配置文件的初始化參數(shù)。我們知道一個(gè)Web應(yīng)用里面可以有多個(gè)servlet,如果現(xiàn)在有一份數(shù)據(jù)需要傳給所有的servlet使用,那么我們就可以使用ServletContext對(duì)象了2022-09-09
eclipse輸出Hello World的實(shí)現(xiàn)方法
這篇文章主要介紹了eclipse輸出Hello World的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
SpringBoot使用Mybatis注解實(shí)現(xiàn)分頁(yè)動(dòng)態(tài)sql開發(fā)教程
這篇文章主要為大家介紹了SpringBoot使用Mybatis注解實(shí)現(xiàn)分頁(yè)及動(dòng)態(tài)sql開發(fā)教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
Springboot @Configuration與自動(dòng)配置詳解
這篇文章主要介紹了SpringBoot中的@Configuration自動(dòng)配置,在進(jìn)行項(xiàng)目編寫前,我們還需要知道一個(gè)東西,就是SpringBoot對(duì)我們的SpringMVC還做了哪些配置,包括如何擴(kuò)展,如何定制,只有把這些都搞清楚了,我們?cè)谥笫褂貌艜?huì)更加得心應(yīng)手2022-07-07

