免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
Java 編程的動態(tài)性,第 6 部分: 利用 Javassist 進(jìn)行面向方面的更改
Dennis M. Sosnoski (dms@sosnoski.com), 總裁, Sosnoski Software Solutions, Inc.
2004 年 3 月 15 日
Java 顧問 Dennis Sosnoski 在他的關(guān)于 Javassist 框架的三期文章中將精華部分留在了最后。這次他展現(xiàn)了 Javassist 對搜索-替換的支持是如何使對 Java 字節(jié)碼的編輯變得像文本編輯器的“替換所有(Replace All )”命令一樣容易的。想報告所有寫入特定字段的內(nèi)容或者對方法調(diào)用中參數(shù)的更改中的補(bǔ)丁嗎?Javassist 使這變得很容易,Dennis 向您展示了其做法。
本系列的第 4 部分第 5 部分討論了如何用 Javassist 對二進(jìn)制類進(jìn)行局部更改。這次您將學(xué)習(xí)以一種更強(qiáng)大的方式使用該框架,從而充分利用 Javassist 對在字節(jié)碼中查找所有特定方法或者字段的支持。對于 Javassist 功能而言,這個功能至少與它以類似源代碼的方式指定字節(jié)碼的能力同樣重要。對選擇替換操作的支持也有助于使 Javasssist 成為一個在標(biāo)準(zhǔn) Java 代碼中增加面向方面的編程功能的絕好工具。
第 5 部分介紹了 Javassist 是如何讓您攔截類加載過程的 ―― 甚至在二進(jìn)制類表示正在被加載的時候?qū)λ鼈冞M(jìn)行更改。這篇文章中討論的系統(tǒng)字節(jié)碼轉(zhuǎn)換可以用于靜態(tài)類文件轉(zhuǎn)換,也可以用于運(yùn)行時攔截,但是在運(yùn)行時使用尤其有用。
Javassist 提供了兩種不同的系統(tǒng)字節(jié)碼修改的處理方法。第一種技術(shù)是使用 javassist.CodeConverter 類,使用起來要稍微簡單一些,但是可以完成的任務(wù)有很多限制。第二種技術(shù)使用 javassist.ExprEditor 類的自定義子類,它稍微復(fù)雜一些,但是所增加的靈活性足以抵銷所付出的努力。在本文中我將分析這兩種方法的例子。
系統(tǒng)字節(jié)碼修改的第一種 Javassist 技術(shù)使用 javassist.CodeConverter 類。要利用這種技術(shù),只需要創(chuàng)建 CodeConverter 類的一個實(shí)例并用一個或者多個轉(zhuǎn)換操作配置它。每一個轉(zhuǎn)換都是用識別轉(zhuǎn)換類型的方法調(diào)用來配置的。轉(zhuǎn)換類型可分為三類:方法調(diào)用轉(zhuǎn)換、字段訪問轉(zhuǎn)換和新對象轉(zhuǎn)換。
第 1 部分, “類和類裝入”(2003 年 4 月)
第 2 部分, “引入反射”(2003 年 6 月)
第 3 部分, “應(yīng)用反射”(2003 年 7 月)
第 4 部分, “用 Javassist 進(jìn)行類轉(zhuǎn)換”(2003 年 9 月)
第 5 部分, “動態(tài)轉(zhuǎn)換類”(2004 年 2 月)
清單 1 給出了使用方法調(diào)用轉(zhuǎn)換的一個例子。在這個例子中,轉(zhuǎn)換只是增加了一個方法正在被調(diào)用的通知。在代碼中,首先得到將要使用的 javassist.ClassPool 實(shí)例,將它配置為與一個翻譯器一同工作 (正如在前面第 5 部分 所看到的)。然后,通過 ClassPool 訪問兩個方法定義。第一個方法定義針對的是要監(jiān)視的“set”類型的方法(類和方法名來自命令行參數(shù)),第二個方法定義針對的是 reportSet() 方法 ,它位于 TranslateConvert 類中,并會報告對第一個方法的調(diào)用。
有了方法信息后,就可以用 CodeConverter insertBeforeMethod() 配置一個轉(zhuǎn)換,以在每次調(diào)用這個 set 方法之前增加一個對報告方法的調(diào)用。然后所要做的就是將這個轉(zhuǎn)換器應(yīng)用到一個或者多個類上。在清單 1 的代碼中,我是通過調(diào)用類對象的 instrument() 方法,在 ConverterTranslator 內(nèi)部類的 onWrite() 方法中完成這項(xiàng)工作的。這將自動對從 ClassPool 實(shí)例中加載的每一個類應(yīng)用這個轉(zhuǎn)換。
public class TranslateConvert { public static void main(String[] args) { if (args.length >= 3) { try { // set up class loader with translator ConverterTranslator xlat = new ConverterTranslator(); ClassPool pool = ClassPool.getDefault(xlat); CodeConverter convert = new CodeConverter(); CtMethod smeth = pool.get(args[0]). getDeclaredMethod(args[1]); CtMethod pmeth = pool.get("TranslateConvert"). getDeclaredMethod("reportSet"); convert.insertBeforeMethod(smeth, pmeth); xlat.setConverter(convert); Loader loader = new Loader(pool); // invoke "main" method of application class String[] pargs = new String[args.length-3]; System.arraycopy(args, 3, pargs, 0, pargs.length); loader.run(args[2], pargs); } catch ... } } else { System.out.println("Usage: TranslateConvert " + "clas-name set-name main-class args..."); } } public static void reportSet(Bean target, String value) { System.out.println("Call to set value " + value); } public static class ConverterTranslator implements Translator { private CodeConverter m_converter; private void setConverter(CodeConverter convert) { m_converter = convert; } public void start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) throws NotFoundException, CannotCompileException { CtClass clas = pool.get(cname); clas.instrument(m_converter); } } }
配置轉(zhuǎn)換是一個相當(dāng)復(fù)雜的操作,但是設(shè)置好以后,在它工作時就不用費(fèi)什么心了。清單 2 給出了代碼示例,可以作為測試案例。這里 Bean 提供了具有類似 bean 的 get 和 set 方法的測試對象, BeanTest 程序用這些方法來訪問值。
public class Bean { private String m_a; private String m_b; public Bean() {} public Bean(String a, String b) { m_a = a; m_b = b; } public String getA() { return m_a; } public String getB() { return m_b; } public void setA(String string) { m_a = string; } public void setB(String string) { m_b = string; } } public class BeanTest { private Bean m_bean; private BeanTest() { m_bean = new Bean("originalA", "originalB"); } private void print() { System.out.println("Bean values are " + m_bean.getA() + " and " + m_bean.getB()); } private void changeValues(String lead) { m_bean.setA(lead + "A"); m_bean.setB(lead + "B"); } public static void main(String[] args) { BeanTest inst = new BeanTest(); inst.print(); inst.changeValues("new"); inst.print(); } }
如果直接運(yùn)行清單 2 中的 中的 BeanTest 程序,則輸出如下:
[dennis]$ java -cp . BeanTest Bean values are originalA and originalB Bean values are newA and newB
如果用清單 1 中的 TranslateConvert 程序運(yùn)行它并指定監(jiān)視其中的一個 set 方法,那么輸出將如下所示:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest Bean values are originalA and originalB Call to set value newA Bean values are newA and newB
每項(xiàng)工作都與以前一樣,但是現(xiàn)在在執(zhí)行這個程序時,所選的方法被調(diào)用時會有一個通知。
在這個例子中,可以用其他的方法容易地實(shí)現(xiàn)同樣的效果,例如通過使用第 4 部分 中的技術(shù)在實(shí)際的 set 方法體中增加代碼。這里的區(qū)別是,在使用位置增加代碼讓我有了靈活性。例如,可以容易地修改 TranslateConvert.ConverterTranslator onWrite() 方法來檢查正在加載的類名,并只轉(zhuǎn)換在我想要監(jiān)視的類的清單中列出的類。直接在 set 方法體中添加代碼無法進(jìn)行這種有選擇的監(jiān)視。
系統(tǒng)字節(jié)碼轉(zhuǎn)換由于提供了靈活性而使其成為為標(biāo)準(zhǔn) Java 代碼實(shí)現(xiàn)面向方面的擴(kuò)展的強(qiáng)大工具。在本文后面您會看到更多這方面的內(nèi)容。
由 CodeConverter 處理的轉(zhuǎn)換很有用,但是有局限性。例如,如果希望在調(diào)用目標(biāo)方法之前或者之后調(diào)用一個監(jiān)視方法,那么這個監(jiān)視方法必須定義為 static void 并且必須先接受一個目標(biāo)方法的類的參數(shù),然后是與目標(biāo)方法所要求的同樣數(shù)量和類型的參數(shù)。
這種嚴(yán)格的結(jié)構(gòu)意味著監(jiān)視方法需要與目標(biāo)類和方法完全匹配。舉一個例子,假設(shè)我改變了清單 1 中 reportSet() 方法的定義,讓它接受一個一般性的 java.lang.Object 參數(shù),想使它可以用于不同的目標(biāo)類:
public static void reportSet(Object target, String value) { System.out.println("Call to set value " + value); }
編譯沒有問題,但是當(dāng)我運(yùn)行它時它就會中斷:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest Bean values are A and B java.lang.NoSuchMethodError: TranslateConvert.reportSet(LBean;Ljava/lang/String;)V at BeanTest.changeValues(BeanTest.java:17) at BeanTest.main(BeanTest.java:23) at ...
有辦法繞過這種限制。一種解決方案是在運(yùn)行時實(shí)際生成與目標(biāo)方法相匹配的自定義監(jiān)視方法。不過這要做很多工作,在本文中我不打算試驗(yàn)這種方法。幸運(yùn)的是,Javassist 還提供了另一種處理系統(tǒng)字節(jié)碼轉(zhuǎn)換的方法。這種方法使用 javassist.ExprEditor ,與 CodeConverter 相比,它更靈活、也更強(qiáng)大。
用 CodeConverter 進(jìn)行字節(jié)碼轉(zhuǎn)換與用 javassist.ExprEditor 的原理一樣。不過, ExprEditor 方式也許更難理解一些,所以我首先展示基本原理,然后再加入實(shí)際的轉(zhuǎn)換。
清單 3 顯示了如何用 ExprEditor 來報告面向方面的轉(zhuǎn)換的可能目標(biāo)的基本項(xiàng)目。這里我在自己的 VerboseEditor 中派生了 ExprEditor 子類,重寫了三個基本的類方法 ―― 它們的名字都是 edit() ,但是有不同的參數(shù)類型。如清單 1 中的代碼,我實(shí)際上是在 DissectionTranslator 內(nèi)部類的 onWrite() 方法中使用這個子類,對從 ClassPool 實(shí)例中加載的每一個類,在對類對象的 instrument() 方法的調(diào)用中傳遞一個實(shí)例。
public class Dissect { public static void main(String[] args) { if (args.length >= 1) { try { // set up class loader with translator Translator xlat = new DissectionTranslator(); ClassPool pool = ClassPool.getDefault(xlat); Loader loader = new Loader(pool); // invoke the "main" method of the application class String[] pargs = new String[args.length-1]; System.arraycopy(args, 1, pargs, 0, pargs.length); loader.run(args[0], pargs); } catch (Throwable ex) { ex.printStackTrace(); } } else { System.out.println ("Usage: Dissect main-class args..."); } } public static class DissectionTranslator implements Translator { public void start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) throws NotFoundException, CannotCompileException { System.out.println("Dissecting class " + cname); CtClass clas = pool.get(cname); clas.instrument(new VerboseEditor()); } } public static class VerboseEditor extends ExprEditor { private String from(Expr expr) { CtBehavior source = expr.where(); return " in " + source.getName() + "(" + expr.getFileName() + ":" + expr.getLineNumber() + ")"; } public void edit(FieldAccess arg) { String dir = arg.isReader() ? "read" : "write"; System.out.println(" " + dir + " of " + arg.getClassName() + "." + arg.getFieldName() + from(arg)); } public void edit(MethodCall arg) { System.out.println(" call to " + arg.getClassName() + "." + arg.getMethodName() + from(arg)); } public void edit(NewExpr arg) { System.out.println(" new " + arg.getClassName() + from(arg)); } } }
清單 4 顯示了對清單 2 中的 BeanTest 程序運(yùn)行清單 3 中的 Dissect 程序所產(chǎn)生的輸出。它給出了加載的每一個類的每一個方法中所做的工作的詳細(xì)分析,列出了所有方法調(diào)用、字段訪問和新對象創(chuàng)建。
[dennis]$ java -cp .:javassist.jar Dissect BeanTest Dissecting class BeanTest new Bean in BeanTest(BeanTest.java:7) write of BeanTest.m_bean in BeanTest(BeanTest.java:7) read of java.lang.System.out in print(BeanTest.java:11) new java.lang.StringBuffer in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) read of BeanTest.m_bean in print(BeanTest.java:11) call to Bean.getA in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) read of BeanTest.m_bean in print(BeanTest.java:11) call to Bean.getB in print(BeanTest.java:11) call to java.lang.StringBuffer.append in print(BeanTest.java:11) call to java.lang.StringBuffer.toString in print(BeanTest.java:11) call to java.io.PrintStream.println in print(BeanTest.java:11) read of BeanTest.m_bean in changeValues(BeanTest.java:16) new java.lang.StringBuffer in changeValues(BeanTest.java:16) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16) call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:16) call to Bean.setA in changeValues(BeanTest.java:16) read of BeanTest.m_bean in changeValues(BeanTest.java:17) new java.lang.StringBuffer in changeValues(BeanTest.java:17) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17) call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17) call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:17) call to Bean.setB in changeValues(BeanTest.java:17) new BeanTest in main(BeanTest.java:21) call to BeanTest.print in main(BeanTest.java:22) call to BeanTest.changeValues in main(BeanTest.java:23) call to BeanTest.print in main(BeanTest.java:24) Dissecting class Bean write of Bean.m_a in Bean(Bean.java:10) write of Bean.m_b in Bean(Bean.java:11) read of Bean.m_a in getA(Bean.java:15) read of Bean.m_b in getB(Bean.java:19) write of Bean.m_a in setA(Bean.java:23) write of Bean.m_b in setB(Bean.java:27) Bean values are originalA and originalB Bean values are newA and newB
通過在 VerboseEditor 中實(shí)現(xiàn)適當(dāng)?shù)姆椒?,可以容易地增加對報告?qiáng)制類型轉(zhuǎn)換、 instanceof 檢查和 catch 塊的支持。但是只列出有關(guān)這些組件項(xiàng)的信息有些乏味,所以讓我們來實(shí)際修改項(xiàng)目吧。
清單 4對類的剖析列出了基本組件操作。容易看出在實(shí)現(xiàn)面向方面的功能時使用這些操作會多么有用。例如,報告對所選字段的所有寫訪問的記錄器(logger)在許多應(yīng)用程序中都會發(fā)揮作用。無論如何,我已經(jīng)承諾要為您介紹如何完成 這類工作。
幸運(yùn)的是,就本文討論的主題來說, ExprEditor 不但讓我知道代碼中有什么操作,它還讓我可以修改所報告的操作。在不同的 ExprEditor.edit() 方法調(diào)用中傳遞的參數(shù)類型分別定義一種 replace() 方法。如果向這個方法傳遞一個普通 Javassist 源代碼格式的語句(在第 4 部分中介紹),那么這個語句將編譯為字節(jié)碼,并且用來替換原來的操作。這使對字節(jié)碼的切片和切塊變得容易。
清單 5 顯示了一個代碼替換的應(yīng)用程序。在這里我不是記錄操作,而是選擇實(shí)際修改存儲在所選字段中的 String 值。在 FieldSetEditor 中,我實(shí)現(xiàn)了匹配字段訪問的方法簽名。在這個方法中,我只檢查兩樣?xùn)|西:字段名是否是我所查找的,操作是否是一個存儲過程。找到匹配后,就用使用實(shí)際的 TranslateEditor 應(yīng)用程序類中 reverse() 方法調(diào)用的結(jié)果來替換原來的存儲。 reverse() 方法就是將原來字符串中的字母順序顛倒并輸出一條消息表明它已經(jīng)使用過了。
public class TranslateEditor { public static void main(String[] args) { if (args.length >= 3) { try { // set up class loader with translator EditorTranslator xlat = new EditorTranslator(args[0], new FieldSetEditor(args[1])); ClassPool pool = ClassPool.getDefault(xlat); Loader loader = new Loader(pool); // invoke the "main" method of the application class String[] pargs = new String[args.length-3]; System.arraycopy(args, 3, pargs, 0, pargs.length); loader.run(args[2], pargs); } catch (Throwable ex) { ex.printStackTrace(); } } else { System.out.println("Usage: TranslateEditor clas-name " + "field-name main-class args..."); } } public static String reverse(String value) { int length = value.length(); StringBuffer buff = new StringBuffer(length); for (int i = length-1; i >= 0; i--) { buff.append(value.charAt(i)); } System.out.println("TranslateEditor.reverse returning " + buff); return buff.toString(); } public static class EditorTranslator implements Translator { private String m_className; private ExprEditor m_editor; private EditorTranslator(String cname, ExprEditor editor) { m_className = cname; m_editor = editor; } public void start(ClassPool pool) {} public void onWrite(ClassPool pool, String cname) throws NotFoundException, CannotCompileException { if (cname.equals(m_className)) { CtClass clas = pool.get(cname); clas.instrument(m_editor); } } } public static class FieldSetEditor extends ExprEditor { private String m_fieldName; private FieldSetEditor(String fname) { m_fieldName = fname; } public void edit(FieldAccess arg) throws CannotCompileException { if (arg.getFieldName().equals(m_fieldName) && arg.isWriter()) { StringBuffer code = new StringBuffer(); code.append("$0."); code.append(arg.getFieldName()); code.append("=TranslateEditor.reverse($1);"); arg.replace(code.toString()); } } } }
如果對清單 2 中的 BeanTest 程序運(yùn)行清單 5 中的 TranslateEditor 程序,結(jié)果如下:
[dennis]$ java -cp .:javassist.jar TranslateEditor Bean m_a BeanTest TranslateEditor.reverse returning Alanigiro Bean values are Alanigiro and originalB TranslateEditor.reverse returning Awen Bean values are Awen and newB
我成功地在每一次存儲到 Bean.m_a 字段時,加入了一個對添加的代碼的調(diào)用(一次是在構(gòu)造函數(shù)中,一次是在 set 方法中)。我可以通過對從字段的加載實(shí)現(xiàn)類似的修改而得到反向的效果,不過我個人認(rèn)為顛倒值比開始使用的值有意思得多,所以我選擇使用它們。
本文介紹了用 Javassist 可以容易地完成系統(tǒng)字節(jié)碼轉(zhuǎn)換。將本文與上兩期文章結(jié)合在一起,您應(yīng)該有了在 Java 應(yīng)用程序中實(shí)現(xiàn)自己面向方面的轉(zhuǎn)換的堅實(shí)基礎(chǔ),這個轉(zhuǎn)換過程可以作為單獨(dú)的編譯步驟,也可以在運(yùn)行時完成。
要想對這種方法的強(qiáng)大之處有更好的了解,還可以分析用 Javassis 建立的 JBoss Aspect Oriented Programming Project (JBossAOP)。JBossAOP 使用一個 XML 配置文件來定義在應(yīng)用程序類中完成的所有不同的操作。其中包括對字段訪問或者方法調(diào)用使用攔截器,在現(xiàn)有類中添加 mix-in 接口實(shí)現(xiàn)等。JBossAOP 將被加入正在開發(fā)的 JBoss 應(yīng)用程序服務(wù)器版本中,但是也可以在 JBoss 以外作為單獨(dú)的工具提供給應(yīng)用程序使用。
本系列的下一步將介紹 Byte Code Engineering Library (BCEL),這是 Apache Software Foundation 的 Jakarta 項(xiàng)目的一部分。BCEL 是 Java classworking 最廣泛使用的一種框架。它使用與我們在最近這三篇文章中看到的 Javassist 方法的不同方法處理字節(jié)碼,注重個別的字節(jié)碼指令而不是 Javassist 所強(qiáng)調(diào)的源代碼級別的工作。下個月將分析在字節(jié)碼匯編器(assembler)級別工作的全部細(xì)節(jié)。
您可以參閱本文在 developerWorks 全球站點(diǎn)上的英文原文.
參閱 Dennis Sosnoski 的Java 編程的動態(tài)性 系列的其他部分。
下載本文的示例代碼。
Javassist 是東京技術(shù)學(xué)院數(shù)學(xué)和計算機(jī)科學(xué)系的 Shigeru Chiba 開發(fā)的。它最近加入了開放源代碼JBoss 應(yīng)用服務(wù)器項(xiàng)目,并成為其中新增加的面向方面的編程功能的基礎(chǔ)。從 Sourceforge 上的JBoss 項(xiàng)目文件頁面下載 Javassist 的最新版本。
想看用 Javassist 構(gòu)建的面向方面的編程框架嗎?請看JBossAOP 項(xiàng)目。
從 Peter Haggar 的“Java bytecode: Understanding bytecode makes you a better programmer”( developerWorks,2001 年 7 月)中學(xué)習(xí)更多關(guān)于 Java 字節(jié)碼設(shè)計的內(nèi)容。
希望找到有關(guān)面向方面編程的更多內(nèi)容嗎?請參閱 Nicholas Lesiechi 的“Improve modularity with aspect-oriented programming”( developerWorks,2002 年 1 月),以了解有關(guān)使用 Aspectj 語言的簡要介紹。Andrew Glover 新發(fā)表的文章“AOP banishes the tight-coupling blues”( developerWorks,2004 年 2 月)展示了 AOP 的一個功能設(shè)計概念 ―― 靜態(tài)橫切 ―― 是如何將纏繞在一起的、緊密耦合的代碼轉(zhuǎn)變?yōu)閺?qiáng)大的、可擴(kuò)展的企業(yè)應(yīng)用程序。
開放源代碼Jikes 項(xiàng)目為 Java 編程語言提供了一個非??焖俸透叨燃嫒莸木幾g器。用它以老的方式(即從 Java 源代碼)生成字節(jié)碼。
訪問Developer Bookstore,獲得技術(shù)圖書的完整列表,其中包括數(shù)百本 Java 相關(guān)的圖書。
developerWorks Java 技術(shù)專區(qū) 上有數(shù)百篇關(guān)于 Java 技術(shù)各個方面的文章。
Dennis Sosnoski(dms@sosnoski.com)是西雅圖地區(qū)的 Java 咨詢公司Sosnoski Software Solutions, Inc.的創(chuàng)始人和首席顧問。他有 30 多年的專業(yè)軟件開發(fā)經(jīng)驗(yàn),最近幾年致力于服務(wù)器端的 Java 技術(shù),包括 servlet、Enterprise JavaBeans 和 XML。他經(jīng)常在全國性的會議上就 Java 中的 XML 和 J2EE 技術(shù)發(fā)表言論。
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
《Java程序設(shè)計教程》07 字符串和常用庫類
Java關(guān)于JSON數(shù)據(jù)格式轉(zhuǎn)換
J2EE程序的性能優(yōu)化技巧
spring依賴注入
SpringMVC 使用JSR
簡單講講@SpringBootApplication
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服