前言
??今天我們來學(xué)習(xí)
AOP
,在最初我們學(xué)習(xí)Spring時說過Spring的兩大特征,一個是IOC
,一個是AOP
,我們現(xiàn)在要學(xué)習(xí)的就是這個AOP。
AOP簡介
??AOP:
面向切面編程
,一種編程范式,指導(dǎo)開發(fā)者如何組織程序結(jié)構(gòu)。
??作用:在不驚動原始設(shè)計的基礎(chǔ)上為其進(jìn)行功能增強
。
??首先我們先來看看代碼環(huán)境,在主方法中獲取
BookDao對象
,并調(diào)用它的save()方法
,在BookDaoImpl
中save()方法是測試它的萬次執(zhí)行效率
,而此類中的別的方法沒有這個功能????。
BookDaoImpl
類
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//記錄程序當(dāng)前執(zhí)行執(zhí)行(開始時間)
Long startTime = System.currentTimeMillis();
//業(yè)務(wù)執(zhí)行萬次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//記錄程序當(dāng)前執(zhí)行時間(結(jié)束時間)
Long endTime = System.currentTimeMillis();
//計算時間差
Long totalTime = endTime-startTime;
//輸出信息
System.out.println("執(zhí)行萬次消耗時間:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
主方法
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}
運行結(jié)果
??現(xiàn)在我們要說的不是這個,如上,
update()、delete()、select()方法
中并沒有測試萬次執(zhí)行效率
這個功能,但是我在運行update()、delete()方法時,它也打印了執(zhí)行萬次消耗的時間,而select()方法沒有????。
??這是因為我用了一種技術(shù)在
不驚動原始設(shè)計的基礎(chǔ)上
想為誰追加功能就為誰追加功能,這就是AOP
,而不驚動原始設(shè)計也是我們Spring的一種理念:無入侵式/無侵入式
。
??那它是怎么做的呢,我們來分析一下????。
??這是我們剛才的程序,在這里邊觀察紅色框出來的四行,這是我們要執(zhí)行的業(yè)務(wù),除了這四行,我們藍(lán)色框出來的是圍繞著save()方法的業(yè)務(wù)上下兩塊東西,這兩塊東西實際上我也想讓別的方法擁有,在這里為了讓別的方法也有這個功能,我們就把這個東西抽取出來了????。
??將它抽取為一個單獨的方法,叫什么不重要,注意看,我這里邊擁有的內(nèi)容和右邊藍(lán)色的內(nèi)容完全對應(yīng),這是將我要每一個方法所具有的功能抽取出來得到的一個方法,這個時候就要區(qū)分一下了,在AOP中,我們稱右邊的save()、update()、select()、方法,也就是原始方法,叫它為
連接點
,而我們剛才是給每一個方法都追加功能了嗎?沒有,我們剛才只給update()、delete()方法追加了功能,select()沒有追加,那么AOP對于要追加功能的這些方法,叫它切入點
,這個切入點就說明了哪些方法要追加功能,那按照剛才的現(xiàn)象來看,我們的select()屬于不屬于切入點,不屬于,為什么,因為我剛才在追加功能的時候select()沒有,接著說,我現(xiàn)在左邊抽取出來的這個方法,這個方法叫什么呢,它說你要大家都有的功能,這是一組共性功能,它管這種共性功能叫通知
,接下來問題又來了,你說這個功能你這次做的要測它的性能,下次我們能不能做個別的功能,當(dāng)然可以, 這種通知是不是可以開發(fā)好多個,可以根據(jù)我的需求開發(fā)第一種、第二種、第三種通知,那么問題就來了,你怎么知道在這倆方法上執(zhí)行這個通知呢,我們是不知道的,因此看來在通知和切入點之間,還得有個東西,把它們倆綁定在一塊,這樣的話,一個通知就對應(yīng)一個切入點,那么 這個東西叫什么呢,叫切面
,也就是說,切面描述的是你的這個通知的共性功能與對應(yīng)的切入點的關(guān)系,有了這個關(guān)系它就知道了這倆方法對應(yīng)這個通知,回頭select()還有可能加入別的功能呢,到這里我們已經(jīng)了解了這幾個概念,最后我們再說一個, 因為通知是一個方法,在Java中我們不能直接寫方法,我們把它放在一個類中,這個類給它個名稱叫通知類
,到這里我們就得到了如下概念????。
AOP入門案例
??在前邊我們已經(jīng)介紹了AOP,現(xiàn)在我們來分析一下怎么做,并且去實現(xiàn)它,我們來實現(xiàn)的
入門案例
是:在接口執(zhí)行前輸出當(dāng)前系統(tǒng)時間,在這里我們使用注解
的開發(fā)方式。
??思路分析:
???1.導(dǎo)入坐標(biāo)(pom.xml)
???2.制作連接點方法(原始操作,Dao接口與實現(xiàn)類)
???3.制作共性功能(通知類,與通知)
???4.定義切入點
???5.綁定切入點與通知關(guān)系(切面)
??分析完以后我們就要開始做了,首先來看一下代碼環(huán)境????。
-
BookDao接口
-
BookDao實現(xiàn)類
-
SpringConfig
-
主方法
-
執(zhí)行save()方法
-
執(zhí)行update()方法
??我們可以通過執(zhí)行結(jié)果看出 update()方法是沒有獲取當(dāng)前系統(tǒng)時間這個功能的,現(xiàn)在我們要做的就是在不動原來代碼的基礎(chǔ)上,給update()方法增加獲取當(dāng)前系統(tǒng)時間的功能????。
??首先第一步,導(dǎo)入坐標(biāo),做AOP開發(fā)要導(dǎo)的坐標(biāo)是spring-aop,但是這里需要說一點,看一下這個依賴關(guān)系????。
??在它的依賴關(guān)系中,
context
一旦導(dǎo)入,你會發(fā)現(xiàn)aop的包自動就導(dǎo)進(jìn)來了,所以aop開發(fā)是默認(rèn)導(dǎo)入的,除此之外還要導(dǎo)入一個aspectj
的包????。
??第二步就是制作連接點方法,也就是做好我們的實現(xiàn)類????。
??第三步就是把 獲取當(dāng)前系統(tǒng)時間的這個
共性功能
給它抽出來單獨做,右鍵創(chuàng)建一個新的類:aop包下的MyAdvice類,在類中隨便起一個方法,然后將功能添加進(jìn)去????。
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
??第四步我們要去定義它的
切入點
,先寫一個私有的方法,方法名任意,但是不要沖突,方法里邊有方法體,但是啥也沒寫,然后在它上邊寫一句話:@Pointcut()
,括號里邊寫什么呢,給它一個參數(shù)("execution()")
,execution自帶一個括號,括號里邊是描述我們剛才寫的那個method方法的,怎么描述呢,它是這樣一個方法,返回值是void類型
的,com.itheima包下的dao包下的BookDao接口里邊的update()方法,沒有參數(shù):@Pointcut("execution(void com.itheima.dao.BookDao.update())")
,這句話就是告訴我們當(dāng)執(zhí)行到了這個方法的時候,這就是一個切入點。
//設(shè)置切入點,要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
??第五步是綁定這個共性功能和這個切入點之間的關(guān)系,怎么綁,在這里我們假定需要讓這個共性功能的方法在切入點前邊執(zhí)行,需要在共性功能的這個method方法上寫上一個注解:
@Before("pt()")
。
//設(shè)置在切入點pt()的前面運行當(dāng)前操作(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
??現(xiàn)在還是不能運行的,因為現(xiàn)在這段程序還得受
Spring控制
,但是它現(xiàn)在不受控,第一件事在類的上方加上@Component
讓它變成Spring控制的bean
,寫完以后雖然Spring能控制它,把它造成bean了,但是Spring并不知道你這里邊是做AOP的,所以我們得告訴Spring,當(dāng)它掃描到我以后我這個當(dāng)AOP處理,所以在MyAdvice類上方再寫一個注解:@Aspect
,這樣就解決了這個問題了,加完這兩個注解還不行,最后一個地方,配置類這,現(xiàn)在這里邊不知道你整個程序里邊是拿注解
開發(fā)的AOP
,所以我們還要在配置類中加一個注解來告訴它:@EnableAspectJAutoProxy
,現(xiàn)在我們整體看一下????。
-
MyAdvice類
-
SpringConfig配置類
-
執(zhí)行save()方法
-
執(zhí)行update()方法
AOP工作流程
??前邊我們講了AOP的入門案例,現(xiàn)在我們要來說說它的
工作流程
,對于AOP的整個工作流程,第一步應(yīng)該做什么事呢,肯定是從Spring開始干活說起,所以第一步是Spring容器啟動
,啟動完以后是不是要進(jìn)行初始化bean
的這些操作了,不著急,對于aop的工作流程來說,它要去讀取所有切面配置中的切入點
,這里可沒有說讀取所有配置
的切入點。
??為什么這么說呢,我們來看一下,這是我們剛才寫的那段代碼, 多了一句話,也就是在里邊多配置了一個切入點,那這句話說的什么意思呢,也就是說如果你定義了一個切入點,并且在這使用了,那么這個切入點我讀取,上邊那個我不讀取,也就是說它只讀取你配置的,為什么這么說呢,我們后邊要做的工作要與這個切入點有關(guān),假定你配了100個切入點,但是你一個也沒用,那不相當(dāng)于沒有配嗎,所以在這個地方我們要看它配了的切入點。
??把這配完以后,接下來就要
初始化bean,判定bean對應(yīng)類中的方法是否匹配到任意切入點
,如果匹配失敗
,創(chuàng)建對象
,然后獲取bean,調(diào)用方法并執(zhí)行,完成操作,我們主要說的是匹配成功
的情況,如果匹配成功,創(chuàng)建原始對象(目標(biāo)對象
)的代理
對象,在這里邊我們看到了一個熟悉的東西代理
, 前邊我們學(xué)習(xí)過jdk的代理模式
,代理可以干什么事:增強
,也就是說我用代理對象
去調(diào)用對應(yīng)的方法然后走我們增強的那些操作,那么aop內(nèi)部是怎么做的呢,Spring的aop內(nèi)部就是用代理模式
來實現(xiàn)的,看到這明白了,鬧了半天它也是代理
。
??接著說,除了這個概念以外,還有一個概念,叫
目標(biāo)對象
,也就是你對那個對象做的代理,原始的那個對象我們叫目標(biāo)對象
,如果匹配成功,創(chuàng)建原始對象(目標(biāo)對象
)的代理
對象,那我們能不能想到執(zhí)行操作的時候是一個什么樣的流程,它在獲取bean
的時候還是拿的哪個原始對象嗎,當(dāng)然不是,在它的Spring容器中保存的就是那個代理對象
,剩下的事就簡單了,獲取的bean是代理對象時,根據(jù)代理對象的運行模式運行原始方法與增強的內(nèi)容,完成操作
,也就是說aop的整個工作實際上用了一套什么樣的形式來做的,代理模式
,這就是aop的一個核心本質(zhì)
,用的代理來做的。
??接下來我們?nèi)ゴa中看????。
??現(xiàn)在我們運行不是為了看結(jié)果,在這執(zhí)行不執(zhí)行都不重要,我們要去打印一下這個
bookdao
對象,來看看這個對象是不是一個代理對象
????。
??現(xiàn)在我們在MyAdvice中修改配置的切入點,讓它匹配不到,并再一次運行程序????。
??我們會發(fā)現(xiàn),兩次運行的結(jié)果幾乎一模一樣,難道出問題了嗎,我們修改一下,在主方法中修改一下,再打印一下
這個對象的所屬的字節(jié)碼文件對象
????。
??也是一樣,運行兩次????。
- 沒配置切入點的
- 配置切入點的
??通過觀察字節(jié)碼文件對象,我們會發(fā)現(xiàn)當(dāng)配置了切入點后,會變成代理類型的對象,也就是說它最終用的是代理的對象,類型是這個沒問題,但是你要打印對象時,它會按
BookDao
對象顯示,為什么會這樣,簡單說一下,就是在aop的這套東西中間,它對它最終的那個對象的toString()方法
進(jìn)行了重寫,所以我們才會看到現(xiàn)在這種顯示效果。
AOP切入點表達(dá)式
??AOP的基礎(chǔ)入門案例我們已經(jīng)做完了,并且也知道內(nèi)部它是用代理的形式進(jìn)行工作的,下面我們就要針對AOP的各個環(huán)節(jié)的細(xì)節(jié)進(jìn)行學(xué)習(xí)了,前邊我們不是寫了一個很長的式子,我們說那是它的切入點,現(xiàn)在我們就要研究那個東西了,那個東西不叫切入點,叫
切入點表達(dá)式
。
??首先我們要知道這個式子寫的時候到底有沒有什么要求呢,也就是說要去學(xué)習(xí)它的
語法格式
,先說兩個東西,切入點是我們要進(jìn)行增強的方法,切入點表達(dá)式是要進(jìn)行增強的方法的描述方式,注意一點:對于任意一個方法,它的描述方式是多種多樣的,看一下我們現(xiàn)在做的方法????。
??對于這一個式子,它的大的描述方向就分
兩種
,第一種方向是按接口
描述,第二種方向是按實現(xiàn)類
描述,現(xiàn)在這兩種形式都可以????。
??接下來我們來說說這么復(fù)雜的一個式子,到底有什么規(guī)則沒有,它是有嚴(yán)格的規(guī)則的????。
??這么寫其實很容易,但是如果你的程序中有幾百個幾千個切入點,還要一一去寫嗎,當(dāng)然不是,不然就累死了,所以我們接下來學(xué)習(xí)一種能簡化這種一個一個去寫的形式:
使用通配符描述切入點
。
??在AOP中,可以使用通配符描述切入點,通配符干嘛的,加速配置,加速描述的,接下來我們看一下????。
??接下來再看一下它的
書寫技巧
????。
AOP通知類型
??前邊我們研究的是切入點表達(dá)式,也就是想控制給誰加aop就給誰加aop,現(xiàn)在我們再研究的細(xì)節(jié)點,
往哪加
,也就是AOP的通知類型
。
??我們要介紹幾種不同的通知類型,先來說第一個問題,我們現(xiàn)在aop實際上是從原來的功能中抽一部分出去做成共性功能,然后用的時候再加回來,問題來了,原來從前邊挖走的你現(xiàn)在運行不能給它執(zhí)行到后邊吧,同樣你原來從后邊挖走的你現(xiàn)在運行不能給它執(zhí)行到前邊吧,所以說放的位置很講究,一種有幾種位置呢,一共有
五種位置
????。
??我們現(xiàn)在進(jìn)入到程序中????。
-
BookDao類
-
BookDao實現(xiàn)類
-
MyAdvice類
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//@Before:前置通知,在原始方法運行之前執(zhí)行
public void before() {
System.out.println("before advice ...");
}
//@After:后置通知,在原始方法運行之后執(zhí)行
public void after() {
System.out.println("after advice ...");
}
//@Around:環(huán)繞通知,在原始方法運行的前后執(zhí)行
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示對原始操作的調(diào)用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
//@AfterReturning:返回后通知,在原始方法執(zhí)行完畢后運行,且原始方法執(zhí)行過程中未出現(xiàn)異?,F(xiàn)象
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
//@AfterThrowing:拋出異常后通知,在原始方法執(zhí)行過程中出現(xiàn)異常后運行
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
- 主方法
??我們提前在MyAdvice類中寫了一些方法用來演示這五種通知類型,接下來我們?nèi)パ菔????。
-
前置通知
-
后置通知
-
環(huán)繞通知
??環(huán)繞通知是比較重要的,我們來分析一下,首先里邊有兩條打印語句,我們想要做的效果是在原始操作的前后,分別打印這兩條語句,我們先按之前的運行一下????。
??運行完我們會發(fā)現(xiàn)兩條打印語句出來了,但是我的原始操作怎么不見了,這是為什么呢,這是因為我們在這做的是
環(huán)繞通知
,環(huán)繞的話肯定是在原始操作的前和后,這里邊有一個核心叫做那一句話代表的是對原始操作的調(diào)用呢?如果沒有的話,你說這兩個打印語句是一前一后,我還說這倆都是前呢,另一個人還說這倆都是后呢,所以這里邊必須有一句話表示對原始操作的調(diào)用
,怎么做呢,格式非常固定,在你的around修飾
的通知上參數(shù)
寫上一個叫ProceedingJoinPoint pjp
,定義完這個參數(shù)使用pjp.proceed()
,這一句話就代表對原始操作的調(diào)用????。
??但是他為什么會畫紅線呢,我們來解釋一下,它告訴你要
拋異常
,為什么要拋異常呢,因為原始操作的調(diào)用你無法預(yù)料它有沒有異常,所以在這需要先拋一個異常,讓你強制的對這個東西進(jìn)行處理????。
??現(xiàn)在我們來運行一下????。
??結(jié)果符合預(yù)期,這就叫做環(huán)繞通知,也是功能最強大的,說一個小細(xì)節(jié),現(xiàn)在我的接口中除了有這個update()方法還有select()方法,我現(xiàn)在在主方法中調(diào)用這個select()方法????。
??因為現(xiàn)在程序中沒有配置它的切入點,所以執(zhí)行結(jié)果只有他自己的功能,現(xiàn)在我們?nèi)yAdvice中定義一個全新的切入點 ????。
??然后改一下剛才的環(huán)繞通知????。
??我們再來運行一下????。
??注意看,原始操作運行了嘛,運行了,環(huán)繞通知的前和后加上沒有,加上了,但是最后
拋了一個異常
,這個異常是啥意思呢,解釋一下,空的返回值從advice中出來了,它不匹配你的原始操作調(diào)用的返回值類型,原始操作的返回值是什么,是 int,那好解釋一下這件事情,我們的環(huán)繞around對原始操作增強的時候,你記得原來如果沒有返回值還好,但是如果原來有返回值的話,在環(huán)繞通知的最后邊要將它的返回值扔出去,所以這個環(huán)繞通知的返回值也得改,改為什么呢,改為Object
,也就是返回一個對象,我們先返回一個200,運行一下,看程序有沒有報錯????。
??我們會發(fā)現(xiàn)首先
不會報錯
,并且原始方法的返回值我已經(jīng)篡改了,這里邊的return將代表你原始操作的運行,簡單一點就是原來你調(diào)你的select方法實際上走的是他對應(yīng)的實現(xiàn)類,現(xiàn)在變成你先運行第一句打印,然后你運行原始操作,再運行第二句打印,再運行return,這個方法才算運行完,換句話說你原來的代理模式走完了,return的是這個200,那有人說那我的100哪里去了,在你運行原始操作那呢,這個方法有一個返回值,把這個返回值給它接出來,最后返回就行了????。
-
返回后通知
-
拋出異常后通知
AOP通知獲取數(shù)據(jù)
??現(xiàn)在我們要來說說AOP里邊的數(shù)據(jù)了,什么意思呢,就是現(xiàn)在我能夠?qū)δ愕墓δ苓M(jìn)行干預(yù)了,加?xùn)|西了,但是有一點,有些時候不是所有的情況都是統(tǒng)一處理的,比如說你過來一個參數(shù),參數(shù)不一樣我處理的方式不一樣,這是我們經(jīng)常見到的一些需求,那面對這種情況我們?nèi)绻谕ㄖ镞吥貌坏轿覀冊纪ㄖ臄?shù)據(jù),你就玩不下去了,因此我們來說說
怎么樣從AOP里邊拿取通知的數(shù)據(jù)
。
??那方式有多少種呢,有三種,第一種,獲取參數(shù),第二種,獲取返回值,第三種,獲取異常,對于這三個東西,我們需要分析一個問題,是不是所有的通知都有這些東西,當(dāng)然不是,對于參數(shù)來說,所有的通知都能拿到,比如你現(xiàn)在調(diào)用一個方法,我不管你最后是正常結(jié)束還是異常結(jié)束了,你最起碼調(diào)用的時候參數(shù)都有的呀,所以說參數(shù)是每個里邊都有的,但是返回值就不是了,它必須得有保障原始操作正常執(zhí)行,你才能在AOP中拿到原始操作的返回值,所以說,這里邊只有兩個東西能拿到返回值,哪兩個呢,一個是返回后通知,一個是環(huán)繞通知,那接下來我們到程序中將這些信息拿一遍????。
-
BookDao接口
-
BookDao實現(xiàn)類
-
主方法
??接下來我們來
實現(xiàn)AOP獲取數(shù)據(jù)
????。
??給方法里邊設(shè)置一個
JoinPoint
參數(shù),并通過getArgs()方法
獲取參數(shù),返回值是一個對象數(shù)組,通過Arrays.toString()去輸出????。
??我們來改一改,給它設(shè)置兩個參數(shù),注意一點,我們的aop里邊沒動,注意看能拿到什么????。
??是不是都拿到了,后置通知也一模一樣????。
??現(xiàn)在我們來說
環(huán)繞通知
,首先想一件事,JoinPoint是ProceedJoinPoint的父接口
,父接口都能調(diào)到的方法,子接口肯定能調(diào)到,在這就不打印它了,因為不是我們主體要說的,下面要說我們是不是在這調(diào)用原始方法了,注意調(diào)用原始方法的時候?qū)τ趐roceed這個操作,除了空參以外,還可以傳遞一個Object數(shù)組
????。
??也就是說我們可以把通過getArgs()獲取的參數(shù)給proceed傳進(jìn)去用,寫與不寫代表的含義都一樣,都代表使用getArgs()得到的參數(shù)來傳遞,我們來運行一下????。
??結(jié)果是沒問題的,我們這里想說的是我要是給它使點壞,比如說在你獲取參數(shù)以后,發(fā)送調(diào)用之前,我要把這里邊的東西給改了,那會是為什么樣呢?
??我們會發(fā)現(xiàn)已經(jīng)變成666了,現(xiàn)在我們已經(jīng)做到了一個非常好的效果了,是如果傳過來的參數(shù)有問題,我們就可以處理一下了????。
??接下來說
返回值
的獲取,我們前邊已經(jīng)說過環(huán)繞通知了,現(xiàn)在只需要說一下返回后通知
????。
??在這里你如果想拿它的返回值,你可以先去定義一個用于接收返回值的形參,如下????。
??但是你要用這個東西,你必須告訴afterReturning你用ret這個變量準(zhǔn)備接它的返回值,屬性returning是專門干這事的????。
??這句話是什么意思,是如果你的原始方法有返回值,那就把返回值裝到形參中叫ret的這個變量里,我們來運行一下????。
??在這里有一個問題,如果JoinPoint跟它同時存在,順序必須是JoinPoint在前,如果不是第一個必報錯????。
??接下來我們來說一下異常的,對于異常的怎么拿,在這雖然能拿到????。
??但是在這里邊拿不到????。
??那怎么辦呢,回歸到最原始的 try catch中,這個 throwable就是你最終的異常對象了????。
??拋出異常后通知接收異常對象????。
??在這寫完以后我們在原操作中拋異常????。
??注意看afterThrowing后邊有一個異常信息,這個就是帶過來的。文章來源:http://www.zghlxwxcb.cn/news/detail-822310.html
總結(jié)
??以上就是我們學(xué)習(xí)AOP的全部內(nèi)容,如果有什么錯誤的話,大家可以私信我????,
希望大家多多關(guān)注+點贊+收藏 ^_ ^????,你們的鼓勵是我不斷前進(jìn)的動力????!?。?/code>文章來源地址http://www.zghlxwxcb.cn/news/detail-822310.html
到了這里,關(guān)于【Spring(十一)】萬字帶你深入學(xué)習(xí)面向切面編程AOP的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!