深度解析Spring內(nèi)置作用域及其在實(shí)踐中的應(yīng)用
1. Spring的內(nèi)置作用域
我們來(lái)看看Spring內(nèi)置的作用域類(lèi)型。在5.x版本中,Spring內(nèi)置了六種作用域:
- singleton:在
IOC容器中,對(duì)應(yīng)的Bean只有一個(gè)實(shí)例,所有對(duì)它的引用都指向同一個(gè)對(duì)象。這種作用域非常適合對(duì)于無(wú)狀態(tài)的Bean,比如工具類(lèi)或服務(wù)類(lèi)。 - prototype:每次請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的
Bean實(shí)例,適合對(duì)于需要維護(hù)狀態(tài)的Bean。 - request:在
Web應(yīng)用中,為每個(gè)HTTP請(qǐng)求創(chuàng)建一個(gè)Bean實(shí)例。適合在一個(gè)請(qǐng)求中需要維護(hù)狀態(tài)的場(chǎng)景,如跟蹤用戶(hù)行為信息。 - session:在
Web應(yīng)用中,為每個(gè)HTTP會(huì)話(huà)創(chuàng)建一個(gè)Bean實(shí)例。適合需要在多個(gè)請(qǐng)求之間維護(hù)狀態(tài)的場(chǎng)景,如用戶(hù)會(huì)話(huà)。 - application:在整個(gè)
Web應(yīng)用期間,創(chuàng)建一個(gè)Bean實(shí)例。適合存儲(chǔ)全局的配置數(shù)據(jù)等。 - websocket:在每個(gè)
WebSocket會(huì)話(huà)中創(chuàng)建一個(gè)Bean實(shí)例。適合WebSocket通信場(chǎng)景。
我們需要重點(diǎn)學(xué)習(xí)兩種作用域:singleton和prototype。在大多數(shù)情況下singleton和prototype這兩種作用域已經(jīng)足夠滿(mǎn)足需求。
2. singleton作用域
2.1 singleton作用域的定義和用途
Singleton是Spring的默認(rèn)作用域。在這個(gè)作用域中,Spring容器只會(huì)創(chuàng)建一個(gè)實(shí)例,所有對(duì)該bean的請(qǐng)求都將返回這個(gè)唯一的實(shí)例。
例如,我們定義一個(gè)名為Plaything的類(lèi),并將其作為一個(gè)bean:
@Component
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}在這個(gè)例子中,Plaything是一個(gè)singleton作用域的bean。無(wú)論我們?cè)趹?yīng)用中的哪個(gè)地方請(qǐng)求這個(gè)bean,Spring都會(huì)返回同一個(gè)Plaything實(shí)例。
下面的例子展示了如何創(chuàng)建一個(gè)單實(shí)例的Bean:
package com.example.demo.bean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Kid {
private Plaything plaything;
@Autowired
public void setPlaything(Plaything plaything) {
this.plaything = plaything;
}
public Plaything getPlaything() {
return plaything;
}
}package com.example.demo.bean;
import org.springframework.stereotype.Component;
@Component
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}這里可以在Plaything類(lèi)加上@Scope(BeanDefinition.SCOPE_SINGLETON),但是因?yàn)槭悄J(rèn)作用域是Singleton,所以沒(méi)必要加。
package com.example.demo.configuration;
import com.example.demo.bean.Kid;
import com.example.demo.bean.Plaything;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanScopeConfiguration {
@Bean
public Kid kid1(Plaything plaything1) {
Kid kid = new Kid();
kid.setPlaything(plaything1);
return kid;
}
@Bean
public Kid kid2(Plaything plaything2) {
Kid kid = new Kid();
kid.setPlaything(plaything2);
return kid;
}
}package com.example.demo.application;
import com.example.demo.bean.Kid;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.example")
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(DemoApplication.class);
context.getBeansOfType(Kid.class).forEach((name, kid) -> {
System.out.println(name + " : " + kid.getPlaything());
});
}
}在Spring IoC容器的工作中,掃描過(guò)程只會(huì)創(chuàng)建bean的定義,真正的bean實(shí)例是在需要注入或者通過(guò)getBean方法獲取時(shí)才會(huì)創(chuàng)建。這個(gè)過(guò)程被稱(chēng)為bean的初始化。
這里運(yùn)行 ctx.getBeansOfType(Kid.class).forEach((name, kid) -> System.out.println(name + " : " + kid.getPlaything())); 時(shí),Spring IoC容器會(huì)查找所有的Kid類(lèi)型的bean定義,然后為每一個(gè)找到的bean定義創(chuàng)建實(shí)例(如果這個(gè)bean定義還沒(méi)有對(duì)應(yīng)的實(shí)例),并注入相應(yīng)的依賴(lài)。
運(yùn)行結(jié)果:

三個(gè) Kid 的 Plaything bean是相同的,說(shuō)明默認(rèn)情況下 Plaything 是一個(gè)單例bean,整個(gè)Spring應(yīng)用中只有一個(gè) Plaything bean被創(chuàng)建。
為什么會(huì)有3個(gè)kid?
- Kid: 這個(gè)是通過(guò)在Kid類(lèi)上標(biāo)注的@Component注解自動(dòng)創(chuàng)建的。Spring在掃描時(shí)發(fā)現(xiàn)這個(gè)注解,就會(huì)自動(dòng)在IOC容器中注冊(cè)這個(gè)bean。這個(gè)Bean的名字默認(rèn)是將類(lèi)名的首字母小寫(xiě)kid。
- kid1: 在 BeanScopeConfiguration 中定義,通過(guò)kid1(Plaything plaything1)方法創(chuàng)建,并且注入了plaything1。
- kid2: 在
BeanScopeConfiguration中定義,通過(guò)kid2(Plaything plaything2)方法創(chuàng)建,并且注入了plaything2。
2.2 singleton作用域線(xiàn)程安全問(wèn)題
需要注意的是,雖然singleton Bean只會(huì)有一個(gè)實(shí)例,但Spring并不會(huì)解決其線(xiàn)程安全問(wèn)題,開(kāi)發(fā)者需要根據(jù)實(shí)際場(chǎng)景自行處理。
我們通過(guò)一個(gè)代碼示例來(lái)說(shuō)明在多線(xiàn)程環(huán)境中出現(xiàn)singleton Bean的線(xiàn)程安全問(wèn)題。
首先,我們創(chuàng)建一個(gè)名為Counter的singleton Bean,這個(gè)Bean有一個(gè)count變量,提供increment方法來(lái)增加count的值:
package com.example.demo.bean;
import org.springframework.stereotype.Component;
@Component
public class Counter {
private int count = 0;
public int increment() {
return ++count;
}
}然后,我們創(chuàng)建一個(gè)名為CounterService的singleton Bean,這個(gè)Bean依賴(lài)于Counter,在increaseCount方法中,我們調(diào)用counter.increment方法:
package com.example.demo.service;
import com.example.demo.bean.Counter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CounterService {
@Autowired
private final Counter counter;
public void increaseCount() {
counter.increment();
}
}我們?cè)诙嗑€(xiàn)程環(huán)境中調(diào)用counterService.increaseCount方法時(shí),就可能出現(xiàn)線(xiàn)程安全問(wèn)題。因?yàn)?code>counter.increment方法并非線(xiàn)程安全,多個(gè)線(xiàn)程同時(shí)調(diào)用此方法可能會(huì)導(dǎo)致count值出現(xiàn)預(yù)期外的結(jié)果。
要解決這個(gè)問(wèn)題,我們需要使counter.increment方法線(xiàn)程安全。
這里可以使用原子變量,在Counter類(lèi)中,我們可以使用AtomicInteger來(lái)代替int類(lèi)型的count,因?yàn)?code>AtomicInteger類(lèi)中的方法是線(xiàn)程安全的,且其性能通常優(yōu)于synchronized關(guān)鍵字。
package com.example.demo.bean;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.incrementAndGet();
}
}盡管優(yōu)化后已經(jīng)使Counter類(lèi)線(xiàn)程安全,但在設(shè)計(jì)Bean時(shí),我們應(yīng)該盡可能地減少可變狀態(tài)。這是因?yàn)榭勺儬顟B(tài)使得并發(fā)編程變得復(fù)雜,而無(wú)狀態(tài)的Bean通常更容易理解和測(cè)試。
什么是無(wú)狀態(tài)的Bean呢? 如果一個(gè)Bean不持有任何狀態(tài)信息,也就是說(shuō),同樣的輸入總是會(huì)得到同樣的輸出,那么這個(gè)Bean就是無(wú)狀態(tài)的。反之,則是有狀態(tài)的Bean。
3. prototype作用域
3.1 prototype作用域的定義和用途
在prototype作用域中,Spring容器會(huì)為每個(gè)請(qǐng)求創(chuàng)建一個(gè)新的bean實(shí)例。
例如,我們定義一個(gè)名為Plaything的類(lèi),并將其作用域設(shè)置為prototype:
package com.example.demo.bean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}在這個(gè)例子中,Plaything是一個(gè)prototype作用域的bean。每次我們請(qǐng)求這個(gè)bean,Spring都會(huì)創(chuàng)建一個(gè)新的Plaything實(shí)例。
我們只需要修改上面的Plaything類(lèi),其他的類(lèi)不用動(dòng)。
打印結(jié)果:

這個(gè)@Scope(BeanDefinition.SCOPE_PROTOTYPE)可以寫(xiě)成@Scope("prototype"),按照規(guī)范,還是利用已有的常量比較好。

3.2 prototype作用域在開(kāi)發(fā)中的例子
以我個(gè)人來(lái)說(shuō),我在excel多線(xiàn)程上傳的時(shí)候用到過(guò)這個(gè),當(dāng)時(shí)是EasyExcel框架,我給一部分關(guān)鍵代碼展示一下如何在Spring中使用prototype作用域來(lái)處理多線(xiàn)程環(huán)境下的任務(wù)(實(shí)際業(yè)務(wù)會(huì)更復(fù)雜),大家可以對(duì)比,如果用prototype作用域和使用new對(duì)象的形式在實(shí)際開(kāi)發(fā)中有什么區(qū)別。
使用prototype作用域的例子
@Resource
private ApplicationContext context;
@PostMapping("/user/upload")
public ResultModel upload(@RequestParam("multipartFile") MultipartFile multipartFile) {
......
ExecutorService es = new ThreadPoolExceutor(10, 16, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2000), new ThreadPoolExecutor.CallerRunPolicy());
......
EasyExcel.read(multipartFile.getInputStream(), UserDataUploadVO.class,
new PageReadListener<UserDataUploadVO>(dataList ->{
......
// 多線(xiàn)程處理上傳excel數(shù)據(jù)
Future<?> future = es.submit(context.getBean(AsyncUploadHandler.class, user, dataList, errorCount));
......
})).sheet().doRead();
......
}AsyncUploadHandler.java
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class AsyncUploadHandler implements Runnable {
private User user;
private List<UserDataUploadVO> dataList;
private AtomicInteger errorCount;
@Resource
private RedisService redisService;
@Resource
private CompanyManagementMapper companyManagementMapper;
public AsyncUploadHandler(user, List<UserDataUploadVO> dataList, AtomicInteger errorCount) {
this.user = user;
this.dataList = dataList;
this.errorCount = errorCount;
}
......
}AsyncUploadHandler類(lèi)是一個(gè)prototype作用域的bean,它被用來(lái)處理上傳的Excel數(shù)據(jù)。由于并發(fā)上傳的每個(gè)任務(wù)可能需要處理不同的數(shù)據(jù),并且可能需要在不同的用戶(hù)上下文中執(zhí)行,因此每個(gè)任務(wù)都需要有自己的AsyncUploadHandler bean。這就是為什么需要將AsyncUploadHandler定義為prototype作用域的原因。
由于AsyncUploadHandler是由Spring管理的,我們可以直接使用@Resource注解來(lái)注入其他的bean,例如RedisService和CompanyManagementMapper。
把AsyncUploadHandler交給Spring容器管理,里面依賴(lài)的容器對(duì)象可以直接用@Resource注解注入。如果采用new出來(lái)的對(duì)象,那么這些對(duì)象只能從外面注入好了再傳入進(jìn)去。
不使用prototype作用域改用new對(duì)象的例子
@PostMapping("/user/upload")
public ResultModel upload(@RequestParam("multipartFile") MultipartFile multipartFile) {
......
ExecutorService es = new ThreadPoolExceutor(10, 16, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2000), new ThreadPoolExecutor.CallerRunPolicy());
......
EasyExcel.read(multipartFile.getInputStream(), UserDataUploadVO.class,
new PageReadListener<UserDataUploadVO>(dataList ->{
......
// 多線(xiàn)程處理上傳excel數(shù)據(jù)
Future<?> future = es.submit(new AsyncUploadHandler(user, dataList, errorCount, redisService, companyManagementMapper));
......
})).sheet().doRead();
......
}AsyncUploadHandler.java
public class AsyncUploadHandler implements Runnable {
private User user;
private List<UserDataUploadVO> dataList;
private AtomicInteger errorCount;
private RedisService redisService;
private CompanyManagementMapper companyManagementMapper;
public AsyncUploadHandler(user, List<UserDataUploadVO> dataList, AtomicInteger errorCount,
RedisService redisService, CompanyManagementMapper companyManagementMapper) {
this.user = user;
this.dataList = dataList;
this.errorCount = errorCount;
this.redisService = redisService;
this.companyManagementMapper = companyManagementMapper;
}
......
}如果直接新建AsyncUploadHandler對(duì)象,則需要手動(dòng)傳入所有的依賴(lài),這會(huì)使代碼變得更復(fù)雜更難以管理,而且還需要手動(dòng)管理AsyncUploadHandler的生命周期。
4. request作用域(了解)
request作用域:Bean在一個(gè)HTTP請(qǐng)求內(nèi)有效。當(dāng)請(qǐng)求開(kāi)始時(shí),Spring容器會(huì)為每個(gè)新的HTTP請(qǐng)求創(chuàng)建一個(gè)新的Bean實(shí)例,這個(gè)Bean在當(dāng)前HTTP請(qǐng)求內(nèi)是有效的,請(qǐng)求結(jié)束后,Bean就會(huì)被銷(xiāo)毀。如果在同一個(gè)請(qǐng)求中多次獲取該Bean,就會(huì)得到同一個(gè)實(shí)例,但是在不同的請(qǐng)求中獲取的實(shí)例將會(huì)不同。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
// 在一次Http請(qǐng)求內(nèi)共享的數(shù)據(jù)
private String requestData;
public void setRequestData(String requestData) {
this.requestData = requestData;
}
public String getRequestData() {
return this.requestData;
}
}上述Bean在一個(gè)HTTP請(qǐng)求的生命周期內(nèi)是一個(gè)單例,每個(gè)新的HTTP請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的Bean實(shí)例。
5. session作用域(了解)
session作用域:Bean是在同一個(gè)HTTP會(huì)話(huà)(Session)中是單例的。也就是說(shuō),從用戶(hù)登錄開(kāi)始,到用戶(hù)退出登錄(或者Session超時(shí))結(jié)束,這個(gè)過(guò)程中,不管用戶(hù)進(jìn)行了多少次HTTP請(qǐng)求,只要是在同一個(gè)會(huì)話(huà)中,都會(huì)使用同一個(gè)Bean實(shí)例。
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedBean {
// 在一個(gè)Http會(huì)話(huà)內(nèi)共享的數(shù)據(jù)
private String sessionData;
public void setSessionData(String sessionData) {
this.sessionData = sessionData;
}
public String getSessionData() {
return this.sessionData;
}
}這樣的設(shè)計(jì)對(duì)于存儲(chǔ)和管理會(huì)話(huà)級(jí)別的數(shù)據(jù)非常有用,例如用戶(hù)的登錄信息、購(gòu)物車(chē)信息等。因?yàn)樗鼈兪窃谕粋€(gè)會(huì)話(huà)中保持一致的,所以使用session作用域的Bean可以很好地解決這個(gè)問(wèn)題。
但是實(shí)際開(kāi)發(fā)中沒(méi)人這么干,會(huì)話(huà)id都會(huì)存在數(shù)據(jù)庫(kù),根據(jù)會(huì)話(huà)id就能在各種表中獲取數(shù)據(jù),避免頻繁查庫(kù)也是把關(guān)鍵信息序列化后存在Redis。
6. application作用域(了解)
application作用域:在整個(gè)Web應(yīng)用的生命周期內(nèi),Spring容器只會(huì)創(chuàng)建一個(gè)Bean實(shí)例。這個(gè)Bean在Web應(yīng)用的生命周期內(nèi)都是有效的,當(dāng)Web應(yīng)用停止后,Bean就會(huì)被銷(xiāo)毀。
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationScopedBean {
// 在整個(gè)Web應(yīng)用的生命周期內(nèi)共享的數(shù)據(jù)
private String applicationData;
public void setApplicationData(String applicationData) {
this.applicationData = applicationData;
}
public String getApplicationData() {
return this.applicationData;
}
}如果在一個(gè)application作用域的Bean上調(diào)用setter方法,那么這個(gè)變更將對(duì)所有用戶(hù)和會(huì)話(huà)可見(jiàn)。后續(xù)對(duì)這個(gè)Bean的所有調(diào)用(包括getter和setter)都將影響到同一個(gè)Bean實(shí)例,后面的調(diào)用會(huì)覆蓋前面的狀態(tài)。
7. websocket作用域(了解)
websocket作用域:Bean 在每一個(gè)新的 WebSocket 會(huì)話(huà)中都會(huì)被創(chuàng)建一次,就像 session 作用域的 Bean 在每一個(gè) HTTP 會(huì)話(huà)中都會(huì)被創(chuàng)建一次一樣。這個(gè)Bean在整個(gè)WebSocket會(huì)話(huà)內(nèi)都是有效的,當(dāng)WebSocket會(huì)話(huà)結(jié)束后,Bean就會(huì)被銷(xiāo)毀。
@Component
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketScopedBean {
// 在一個(gè)WebSocket會(huì)話(huà)內(nèi)共享的數(shù)據(jù)
private String socketData;
public void setSocketData(String socketData) {
this.socketData = socketData;
}
public String getSocketData() {
return this.socketData;
}
}上述Bean在一個(gè)WebSocket會(huì)話(huà)的生命周期內(nèi)是一個(gè)單例,每個(gè)新的WebSocket會(huì)話(huà)都會(huì)創(chuàng)建一個(gè)新的Bean實(shí)例。
這個(gè)作用域需要Spring Websocket模塊支持,并且應(yīng)用需要配置為使用websocket。
以上就是深度解析Spring內(nèi)置作用域及其在實(shí)踐中的應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于Spring內(nèi)置作用域及應(yīng)用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- springboot內(nèi)置的tomcat支持最大的并發(fā)量問(wèn)題
- Spring Boot如何配置內(nèi)置Tomcat的maxPostSize值
- Springboot 使用內(nèi)置tomcat禁止不安全HTTP的方法
- springboot項(xiàng)目中實(shí)現(xiàn)訪(fǎng)問(wèn)druid內(nèi)置監(jiān)控頁(yè)面
- 傳統(tǒng)tomcat啟動(dòng)服務(wù)與springboot啟動(dòng)內(nèi)置tomcat服務(wù)的區(qū)別(推薦)
- SpringBoot內(nèi)置tomcat調(diào)優(yōu)測(cè)試優(yōu)化
相關(guān)文章
springboot2 jackson實(shí)現(xiàn)動(dòng)態(tài)返回類(lèi)字段方式
這篇文章主要介紹了springboot2 jackson實(shí)現(xiàn)動(dòng)態(tài)返回類(lèi)字段方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
Java設(shè)計(jì)模式之訪(fǎng)問(wèn)者模式使用場(chǎng)景及代碼示例
這篇文章主要介紹了Java設(shè)計(jì)模式之訪(fǎng)問(wèn)者模式使用場(chǎng)景及代碼示例,小編覺(jué)得還是挺不錯(cuò)的,這里分享給大家,供需要的朋友參考。2017-11-11
SpringBoot配置連接兩個(gè)或多個(gè)數(shù)據(jù)庫(kù)的常用方法
在Spring Boot應(yīng)用中連接多個(gè)數(shù)據(jù)庫(kù)或數(shù)據(jù)源可以使用多種方式,本文講給大家介紹兩種常用的方法:使用Spring Boot官方支持的多數(shù)據(jù)源配置和使用第三方庫(kù)實(shí)現(xiàn)多數(shù)據(jù)源,文章通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
Spring boot整合連接池實(shí)現(xiàn)過(guò)程圖解
這篇文章主要介紹了Spring boot整合連接池實(shí)現(xiàn)過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
Java實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)提取所需知識(shí)點(diǎn)
這篇文章主要介紹了Java實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)提取所需知識(shí)點(diǎn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
SpringBoot集成ActiveMQ的實(shí)戰(zhàn)全過(guò)程
消息隊(duì)列中間件是分布式系統(tǒng)中重要的組件,主要解決應(yīng)用耦合、異步消息、流量削鋒等問(wèn)題,實(shí)現(xiàn)高性能、高可用、可伸縮和最終一致性架構(gòu),是大型分布式系統(tǒng)不可缺少的中間件,這篇文章主要給大家介紹了關(guān)于SpringBoot集成ActiveMQ的相關(guān)資料,需要的朋友可以參考下2021-11-11
Hibernate一對(duì)多關(guān)聯(lián)雙向關(guān)聯(lián)代碼實(shí)現(xiàn)分享
Hibernate一對(duì)多關(guān)聯(lián)雙向關(guān)聯(lián)代碼實(shí)現(xiàn)分享,大家參考使用吧2013-12-12

