級別: 初級
王和全,
2003 年 8 月 02 日
作為基于MVC模式的Web應(yīng)用最經(jīng)典框架,Struts已經(jīng)正式推出了1.1版本,該版本在以往版本的基礎(chǔ)上,提供了許多激動(dòng)人心的新功能。本文就將帶你走進(jìn)Struts 1.1去深入地了解這些功能。
說明:希望本文的讀者能有一定的Struts使用基礎(chǔ)。
Struts 是基于Model 2之上的,而Model 2是經(jīng)典的MVC(模型-視圖-控制器)模型的Web應(yīng)用變體,這個(gè)改變主要是由于網(wǎng)絡(luò)應(yīng)用的特性--HTTP協(xié)議的無狀態(tài)性引起的。Model 2的目的和MVC一樣,也是利用控制器來分離模型和視圖,達(dá)到一種層間松散耦合的效果,提高系統(tǒng)靈活性、復(fù)用性和可維護(hù)性。在多數(shù)情況下,你可以將 Model 2與MVC等同起來。
下圖表示一個(gè)基于Java技術(shù)的典型網(wǎng)絡(luò)應(yīng)用,從中可以看出Model 2中的各個(gè)部分是如何對應(yīng)于Java中各種現(xiàn)有技術(shù)的。

在利用Model 2之前,我們是把所有的表示邏輯和業(yè)務(wù)邏輯都集中在一起(比如大雜燴似的JSP),有時(shí)也稱這種應(yīng)用模式為Model 1,Model 1的主要缺點(diǎn)就是緊耦合,復(fù)用性差以及維護(hù)成本高。
![]() ![]() |
![]()
|
既然Struts 1.1是基于Model 2之上,那它的底層機(jī)制也就是MVC,下面是Struts 1.1中的MVC實(shí)現(xiàn)示意圖:

圖解說明:其中不同顏色代表MVC的不同部分:紅色(控制器)、紫色(模型)和綠色(視圖)
首先,控制器(ActionServlet)進(jìn)行初始化工作,讀取配置文件(struts- config.xml),為不同的Struts模塊初始化相應(yīng)的ModuleConfig對象。比如配置文件中的Action映射定義都保存在 ActionConfig集合中。相應(yīng)地有ControlConfig集合、FormBeanConfig集合、ForwardConfig集合和 MessageResourcesConfig集合等。
提示:模塊是在Struts 1.1中新提出的概念,在稍后的內(nèi)容中我們將詳細(xì)介紹,你現(xiàn)在可以簡單地把模塊看作是一個(gè)子系統(tǒng),它們共同組成整個(gè)應(yīng)用,同時(shí)又各自獨(dú)立。Struts 1.1中所有的處理都是在特定模塊環(huán)境中進(jìn)行的。模塊的提出主要是為了解決Struts 1.0中單配置文件的問題。
控制器接收HTTP請求,并從ActionConfig中找出對應(yīng)于該請求的Action子類,如果沒有對應(yīng)的Action,控制器直接將請求轉(zhuǎn)發(fā)給JSP或者靜態(tài)頁面。否則控制器將請求分發(fā)至具體Action類進(jìn)行處理。
在控制器調(diào)用具體Action的execute方法之前,ActionForm對象將利用HTTP請求中的參數(shù)來填充自己(可選步驟,需要在配置文件中指定)。具體的ActionForm對象應(yīng)該是ActionForm的子類對象,它其實(shí)就是一個(gè)JavaBean。此外,還可以在ActionForm類中調(diào)用validate方法來檢查請求參數(shù)的合法性,并且可以返回一個(gè)包含所有錯(cuò)誤信息的ActionErrors對象。如果執(zhí)行成功, ActionForm自動(dòng)將這些參數(shù)信息以JavaBean(一般稱之為form bean)的方式保存在Servlet Context中,這樣它們就可以被其它Action對象或者JSP調(diào)用。
Struts將這些ActionForm的配置信息都放在FormBeanConfig集合中,通過它們Struts能夠知道針對某個(gè)客戶請求是否需要?jiǎng)?chuàng)建相應(yīng)的ActionForm實(shí)例。
Action 很簡單,一般只包含一個(gè)execute方法,它負(fù)責(zé)執(zhí)行相應(yīng)的業(yè)務(wù)邏輯,如果需要,它也進(jìn)行相應(yīng)的數(shù)據(jù)檢查。執(zhí)行完成之后,返回一個(gè) ActionForward對象,控制器通過該ActionForward對象來進(jìn)行轉(zhuǎn)發(fā)工作。我們主張將獲取數(shù)據(jù)和執(zhí)行業(yè)務(wù)邏輯的功能放到具體的 JavaBean當(dāng)中,而Action只負(fù)責(zé)完成與控制有關(guān)的功能。遵循該原則,所以在上圖中我將Action對象歸為控制器部分。
提示:其實(shí)在Struts 1.1中,ActionMapping的作用完全可以由ActionConfig來替代,只不過由于它是公共API的一部分以及兼容性的問題得以保留。 ActionMapping通過繼承ActionConfig來獲得與其一致的功能,你可以等同地看待它們。同理,其它例如ActionForward與 ForwardConfig的關(guān)系也是如此。
下圖給出了客戶端從發(fā)出請求到獲得響應(yīng)整個(gè)過程的圖解說明。

下面我們就來詳細(xì)地討論一下其中的每個(gè)部分,在這之前,先來了解一下模塊的概念。
![]() ![]() |
![]()
|
我們知道,在Struts 1.0中,我們只能在web.xml中為ActionServlet指定一個(gè)配置文件,這對于我們這些網(wǎng)上的教學(xué)例子來說當(dāng)然沒什么問題,但是在實(shí)際的應(yīng)用開發(fā)過程中,可能會(huì)有些麻煩。因?yàn)樵S多開發(fā)人員都可能同時(shí)需要修改配置文件,但是配置文件只能同時(shí)被一個(gè)人修改,這樣肯定會(huì)造成一定程度上的資源爭奪,勢必會(huì)影響開發(fā)效率和引起開發(fā)人員的抱怨。
在Struts 1.1中,為了解決這個(gè)并行開發(fā)的問題,提出了兩種解決方案:
- 多個(gè)配置文件的支持
- 模塊的支持
支持多個(gè)配置文件,是指你能夠?yàn)锳ctionServlet同時(shí)指定多個(gè)xml配置文件,文件之間以逗號分隔,比如Struts提供的MailReader演示例子中就采用該種方法。
|
這種方法可以很好地解決修改沖突的問題,不同的開發(fā)人員可以在不同的配置文件中設(shè)置自己的Action、ActionForm等等(當(dāng)然不是說每個(gè)開發(fā)人員都需要自己的配置文件,可以按照系統(tǒng)的功能模塊進(jìn)行劃分)。但是,這里還是存在一個(gè)潛在的問題,就是可能不同的配置文件之間會(huì)產(chǎn)生沖突,因?yàn)樵贏ctionServlet初始化的時(shí)候這幾個(gè)文件最終還是需要合并到一起的。比如,在struts-config.xml中配置了一個(gè)名為success的< forward>,而在struts-config-registration.xml中也配置了一個(gè)同樣的<forward>,那么執(zhí)行起來就會(huì)產(chǎn)生沖突。
為了徹底解決這種沖突,Struts 1.1中引進(jìn)了模塊(Module)的概念。一個(gè)模塊就是一個(gè)獨(dú)立的子系統(tǒng),你可以在其中進(jìn)行任意所需的配置,同時(shí)又不必?fù)?dān)心和其它的配置文件產(chǎn)生沖突。因?yàn)榍懊嫖覀冎v過,ActionServlet是將不同的模塊信息保存在不同的ModuleConfig對象中的。要使用模塊的功能,需要進(jìn)行以下的準(zhǔn)備工作:
1、為每個(gè)模塊準(zhǔn)備一個(gè)配置文件
2、配置web.xml文件,通知控制器
決定采用多個(gè)模塊以后,你需要將這些信息告訴控制器,這需要在web.xml文件進(jìn)行配置。下面是一個(gè)典型的多模塊配置:
|
要配置多個(gè)模塊,你需要在原有的一個(gè)<init-param>(在Struts 1.1中將其對應(yīng)的模塊稱為缺省模塊)的基礎(chǔ)之上,增加模塊對應(yīng)的<init-param>。其中<param-name>表示為config/XXX的形式,其中XXX為對應(yīng)的模塊名,<param-value>中還是指定模塊對應(yīng)的配置文件。上面這個(gè)例子說明該應(yīng)用有三個(gè)模塊,分別是缺省模塊、customer和order,它們分別對應(yīng)不同的配置文件。
3、準(zhǔn)備各個(gè)模塊所需的ActionForm、Action和JSP等資源
但是要注意的是,模塊的出現(xiàn)也同時(shí)帶來了一個(gè)問題,即如何在不同模塊間進(jìn)行轉(zhuǎn)發(fā)?有兩種方法可以實(shí)現(xiàn)模塊間的轉(zhuǎn)發(fā),一種就是在< forward>(全局或者本地)中定義,另外一種就是利用org.apache.struts.actions.SwitchAction。
下面就是一個(gè)全局的例子:
|
可以看出,只需要在原有的path屬性前加上模塊名,同時(shí)將contextRelative屬性置為true即可。此外,你也可以在<action>中定義一個(gè)類似的本地<forward>。
|
如果你已經(jīng)處在其他模塊,需要轉(zhuǎn)回到缺省模塊,那應(yīng)該類似下面這樣定義,即模塊名為空。
|
此外,你也可以使用org.apache.struts.actions.SwitchAction,例如:
|
![]() ![]() |
![]()
|
我們首先來了解MVC中的控制器。在Struts 1.1中缺省采用ActionServlet類來充當(dāng)控制器。當(dāng)然如果ActionServlet不能滿足你的需求,你也可以通過繼承它來實(shí)現(xiàn)自己的類。這可以在/WEB-INF/web.xml中來具體指定。
要掌握ActionServlet,就必須了解它所扮演的角色。首先,ActionServlet表示MVC結(jié)構(gòu)中的控制器部分,它需要完成控制器所需的前端控制及轉(zhuǎn)發(fā)請求等職責(zé)。其次,ActionServlet被實(shí)現(xiàn)為一個(gè)專門處理HTTP請求的Servlet,它同時(shí)具有servlet的特點(diǎn)。在 Struts 1.1中它主要完成以下功能:
- 接收客戶端請求
- 根據(jù)客戶端的URI將請求映射到一個(gè)相應(yīng)的Action類
- 從請求中獲取數(shù)據(jù)填充Form Bean(如果需要)
- 調(diào)用Action類的execute()方法獲取數(shù)據(jù)或者執(zhí)行業(yè)務(wù)邏輯
- 選擇正確的視圖響應(yīng)客戶
此外,ActionServlet還負(fù)責(zé)初始化和清除應(yīng)用配置信息的任務(wù)。ActionServlet的初始化工作在init方法中完成,它可以分為兩個(gè)部分:初始化ActionServlet自身的一些信息以及每個(gè)模塊的配置信息。前者主要通過initInternal、initOther和 initServlet三個(gè)方法來完成。
我們可以在/WEB-INF/web.xml中指定具體的控制器以及初始參數(shù),由于版本的變化以及Struts 1.1中模塊概念的引進(jìn),一些初始參數(shù)被廢棄或者移入到/WEB-INF/struts-config.xml中定義。下面列出所有被廢棄的參數(shù),相應(yīng)地在web.xml文件中也不鼓勵(lì)再使用。
- application
- bufferSize
- content
- debug
- factory
- formBean
- forward
- locale
- mapping
- maxFileSize
- multipartClass
- nocache
- null
- tempDir
ActionServlet根據(jù)不同的模塊來初始化ModuleConfig類,并在其中以XXXconfig集合的方式保存該模塊的各種配置信息,比如ActionConfig,F(xiàn)ormBeanConfig等。
初始化工作完成之后,ActionServlet準(zhǔn)備接收客戶請求。針對每個(gè)請求,方法process(HttpServletRequest request, HttpServletResponse response)將被調(diào)用。該方法指定具體的模塊,然后調(diào)用該模塊的RequestProcessor的process方法。
|
RequestProcessor包含了Struts控制器的所有處理邏輯,它調(diào)用不同的processXXX方法來完成不同的處理。下表列出其中幾個(gè)主要的方法:
方法 | 功能 |
processPath | 獲取客戶端的請求路徑 |
processMapping | 利用路徑來獲得相應(yīng)的ActionMapping |
processActionForm | 初始化ActionForm(如果需要)并存入正確的scope中 |
processActionCreate | 初始化Action |
processActionPerform | 調(diào)用Action的execute方法 |
processForwardConfig | 處理Action返回的ActionForward |
![]() ![]() |
![]()
|
對于ActionForm你可以從以下幾個(gè)方面來理解它:
- ActionForm 表示HTTP窗體中的數(shù)據(jù),可以將其看作是模型和視圖的中介,它負(fù)責(zé)保存視圖中的數(shù)據(jù)供模型或者視圖使用。Struts 1.1文檔中把它比作HTTP和Action之間的防火墻,這體現(xiàn)了ActionForm具有的過濾保護(hù)的作用,只有通過ActionForm驗(yàn)證的數(shù)據(jù)才能夠發(fā)送到Action處理。
- ActionForm是與一個(gè)或多個(gè)ActionConfig關(guān)聯(lián)的JavaBean,在相應(yīng)的action的execute方法被調(diào)用之前,ActionForm會(huì)自動(dòng)利用請求參數(shù)來填充自己(初始化屬性)。
- ActionForm是一個(gè)抽象類,你必須通過繼承來實(shí)現(xiàn)自己的類。
ActionForm 首先利用屬性的getter和setter方法來實(shí)現(xiàn)初始化,初始化完畢后,ActionForm的validate方法被調(diào)用,你可以在其中來檢查請求參數(shù)的正確性和有效性,并且可以將錯(cuò)誤信息以ActionErrors的形式返回到輸入窗體。否則,ActionForm將被作為參數(shù)傳給action的 execute方法以供使用。
ActionForm bean的生命周期可以設(shè)置為session(缺省)和request,當(dāng)設(shè)置為session時(shí),記得在reset方法中將所有的屬性重新設(shè)置為初始值。
由于ActionForm對應(yīng)于HTTP窗體,所以隨著頁面的增多,你的ActionForm將會(huì)急速增加。而且可能同一類型頁面字段將會(huì)在不同的 ActionForm中出現(xiàn),并且在每個(gè)ActionForm中都存在相同的驗(yàn)證代碼。為了解決這個(gè)問題,你可以為整個(gè)應(yīng)用實(shí)現(xiàn)一個(gè)ActionForm 或者至少一個(gè)模塊對應(yīng)于一個(gè)ActionForm。
但是,聚合的代價(jià)就是復(fù)用性很差,而且難維護(hù)。針對這個(gè)問題,在Struts 1.1中提出了DynaActionForm的概念。
DynaActionForm類
DynaActionForm 的目的就是減少ActionForm的數(shù)目,利用它你不必創(chuàng)建一個(gè)個(gè)具體的ActionForm類,而是在配置文件中配置出所需的虛擬 ActionForm。例如,在下表中通過指定<form-bean>的type為 "org.apache.struts.action.DynaActionForm"來創(chuàng)建一個(gè)動(dòng)態(tài)的ActionForm--loginForm。
|
動(dòng)態(tài)的ActionForm 的使用方法跟普通的ActionForm相同,但是要注意一點(diǎn)。普通的ActionForm對象需要為每個(gè)屬性提供getter和setter方法,以上面的例子而言,我們需要提供getUsername() 和 setUsername()方法取得和設(shè)置username屬性,同樣地有一對方法用于取得和設(shè)置password屬性和actionClass屬性。
如果使用DynaActionForm,它將屬性保存在一個(gè)HashMap類對象中,同時(shí)提供相應(yīng)的get(name) 和 set(name)方法,其中參數(shù)name是要訪問的屬性名。例如要訪問DynaActionForm中username的值,可以采用類似的代碼:
|
由于值存放于一個(gè)HashMap對象,所以要記得對get()方法返回的Object對象做強(qiáng)制性類型轉(zhuǎn)換。正是由于這點(diǎn)區(qū)別,如果你在Action中非常頻繁地使用ActionForm對象,建議還是使用普通的ActionForm對象。
在Struts 1.1中,除了DynaActionForm以外,還提供了表單輸入自動(dòng)驗(yàn)證的功能,在包org.apache.struts.validator中提供了許多有用的類,其中最常見的就是DynaValidatorForm類。
DynaValidatorForm類
DynaValidatorForm是DynaActionForm的子類,它能夠提供動(dòng)態(tài)ActionForm和自動(dòng)表單輸入驗(yàn)證的功能。和使用DynaActionForm類似,你必須首先在配置文件中進(jìn)行配置:
|
同時(shí)要定義驗(yàn)證的插件:
|
其中的validator.xml和validator-rules.xml分別表示驗(yàn)證定義和驗(yàn)證規(guī)則的內(nèi)容(可以合并在一起),比如針對上例中的DynaValidatorForm,我們有如下驗(yàn)證定義(validator.xml):
|
從上述定義中,我們可以看到對于字段username有三項(xiàng)驗(yàn)證:required, minlength, maxlength,意思是該字段不能為空,而且長度在3和16之間。而validator-rules.xml文件則可以采用Struts提供的缺省文件。注意在<form-bean>中定義的form是如何與validation.xml中的form關(guān)聯(lián)起來的。最后,要啟動(dòng)自動(dòng)驗(yàn)證功能,還需要將Action配置的validate屬性設(shè)置為true。
|
此時(shí),Struts將根據(jù)xml配置文件中的定義來檢驗(yàn)表單輸入,并將不符合要求的錯(cuò)誤信息輸出到頁面。但是你可能會(huì)想:這個(gè)功能雖然好,可是什么檢驗(yàn)都跑到服務(wù)器端執(zhí)行,效率方面和用戶易用性方面是不是有些問題?你可能會(huì)懷念起那簡單的JavaScript客戶端驗(yàn)證。
不用擔(dān)心,在Struts 1.1中也支持JavaScript客戶端驗(yàn)證。如果你選擇了客戶端驗(yàn)證,當(dāng)某個(gè)表單被提交以后,Struts 1.1啟動(dòng)客戶端驗(yàn)證,如果瀏覽器不支持JavaScript驗(yàn)證,則服務(wù)器端驗(yàn)證被啟動(dòng),這種雙重驗(yàn)證機(jī)制能夠最大限度地滿足各種開發(fā)者的需要。 JavaScript驗(yàn)證代碼也是在validator-rules.xml文件中定義的。要啟動(dòng)客戶端驗(yàn)證,你必須在相應(yīng)的JSP文件中做如下設(shè)置:
- 為<html:form>增加onsubmit屬性
- 設(shè)置Javascript支持
下表中列出了一JSP文件的示例代碼,紅字部分為Javascript驗(yàn)證所需代碼。
|
其中onsubmit的值為"return validateLoginForm(this);",它的語法為:
return validate + struts-config.xml中定義的form-bean名稱 + (this);
staticJavascript.jsp的內(nèi)容為:
|
如果validator-rules.xml中定義的基本驗(yàn)證功能不能滿足你的需求,你可以自己添加所需的驗(yàn)證類型。
![]() ![]() |
![]()
|
我們通過繼承Action類來實(shí)現(xiàn)具體的執(zhí)行類。具體Action類的功能一般都在execute(以前是perform方法)方法中完成,其中主要涉及到以下幾個(gè)方面:
- 輔助ActionForm進(jìn)行一些表單數(shù)據(jù)的檢查。
- 執(zhí)行必要的業(yè)務(wù)邏輯,比如存取數(shù)據(jù)庫,調(diào)用實(shí)體bean等。
- 更新服務(wù)器端的bean數(shù)據(jù),后續(xù)對象中可能會(huì)用到這些數(shù)據(jù),比如在JSP中利用bean:write來獲得這些數(shù)據(jù)。
- 根據(jù)處理結(jié)果決定程序的去處,并以ActionForward對象的形式返回給ActionServlet。
提示:由于在Action和ActionForm中都可以實(shí)現(xiàn)驗(yàn)證方法,那么如何來安排它們之間的分工呢?一般來說,我們秉著MVC分離的原則,也就是視圖級的驗(yàn)證工作放在ActionForm來完成,比如輸入不能為空,email格式是否正確,利用ValidatorForm可以很輕松地完成這些工作。而與具體業(yè)務(wù)相關(guān)的驗(yàn)證則放入Action中,這樣就可以獲得最大ActionForm重用性的可能。
前面我們提到過,我們主張將業(yè)務(wù)邏輯執(zhí)行分離到單獨(dú)的 JavaBean中,而Action只負(fù)責(zé)錯(cuò)誤處理和流程控制。而且考慮到重用性的原因,在執(zhí)行業(yè)務(wù)邏輯的JavaBean中不要引用任何與Web應(yīng)用相關(guān)的對象,比如HttpServletRequest,HttpServletResponse等對象,而應(yīng)該將其轉(zhuǎn)化為普通的Java對象。關(guān)于這一點(diǎn),可以參考Petstore中WAF框架的實(shí)現(xiàn)思路。
此外,你可能還注意到execute與perform的一個(gè)區(qū)別:execute方法簡單地?cái)S出Exception異常,而perform方法則擲出ServletException和IOException 異常。這不是說Struts 1.1在異常處理功能方面弱化了,而是為了配合Struts 1.1中一個(gè)很好的功能--宣稱式異常處理機(jī)制。
![]() ![]() |
![]()
|
和EJB中的宣稱式事務(wù)處理概念類似,宣稱式異常處理其實(shí)就是可配置的異常處理,你可以在配置文件中指定由誰來處理Action類中擲出的某種異常。你可以按照以下步驟來完成該功能:
- 實(shí)現(xiàn)org.apache.struts.action.ExceptionHandler的子類,覆蓋execute方法,在該方法中處理異常并且返回一個(gè)ActionForward對象
- 在配置文件中配置異常處理對象,你可以配置一個(gè)全局的處理類或者單獨(dú)為每個(gè)Action配置處理類
下表就定義了一個(gè)全局的處理類CustomizedExceptionHandler,它被用來處理所有的異常。
|
其中具體的參數(shù)含義,可以參考ExceptionHandler.java源文件。
![]() ![]() |
![]()
|
講完了模型和控制器,接下來我們要涉及的是視圖。視圖的角色主要是由JSP來完成,從JSP的規(guī)范中可以看出,在視圖層可以"折騰"的技術(shù)不是很多,主要的就是自定義標(biāo)記庫的應(yīng)用。Struts 1.1在原有的四個(gè)標(biāo)記庫的基礎(chǔ)上新增了兩個(gè)標(biāo)記庫--Tiles和Nested。
其中Tiles除了替代Template的基本模板功能外,還增加了布局定義、虛擬頁面定義和動(dòng)態(tài)頁面生成等功能。Tiles強(qiáng)大的模板功能能夠使頁面獲得最大的重用性和靈活性,此外可以結(jié)合Tiles配置文件中的頁面定義和Action的轉(zhuǎn)發(fā)邏輯,即你可以將一個(gè)Action轉(zhuǎn)發(fā)到一個(gè)在Tiles配置文件中定義的虛擬頁面,從而減少頁面的數(shù)量。比如,下表中的Action定義了一個(gè)轉(zhuǎn)發(fā)路徑,它的終點(diǎn)是tile.userMain,而后者是你在 Tiles配置文件中定義的一個(gè)頁面。
|
Tiles配置文件:tiles-defs.xml
|
而Nested標(biāo)記庫的作用是讓以上這些基本標(biāo)記庫能夠嵌套使用,發(fā)揮更大的作用。
![]() ![]() |
![]()
|
所謂的Commons Logging接口,是指將日志功能的使用與日志具體實(shí)現(xiàn)分開,通過配置文件來指定具體使用的日志實(shí)現(xiàn)。這樣你就可以在Struts 1.1中通過統(tǒng)一的接口來使用日志功能,而不去管具體是利用的哪種日志實(shí)現(xiàn),有點(diǎn)于類似JDBC的功能。Struts 1.1中支持的日志實(shí)現(xiàn)包括:Log4J,JDK Logging API, LogKit,NoOpLog和SimpleLog。
你可以按照如下的方式來使用Commons Logging接口(可以參照Struts源文中的許多類實(shí)現(xiàn)):
|
而開啟日志功能最簡單的辦法就是在WEB-INF/classes目錄下添加以下兩個(gè)文件:
commons-logging.properties文件:
|
simplelog.properties文件:
|
這里我們采用的日志實(shí)現(xiàn)是 SimpleLog,你可以在simplelog.properties文件指定日志明細(xì)的級別:trace,debug,info,warn, error和fatal,從trace到fatal錯(cuò)誤級別越來越高,同時(shí)輸出的日志信息也越來越少。而這些級別是和 org.apache.commons.logging.log接口中的方法一一對應(yīng)的。這些級別是向后包含的,也就是前面的級別包含后面級別的信息。
![]() ![]() |
![]()
|
![]() ![]() |
![]()
|
![]() |
||
|
![]() |
王和全,郵件地址: ok_winnerboy@sina.com |