解析Spring中面向切面編程
AOP (Aspect Orient Programming),直譯過來就是 面向切面編程。AOP 是一種編程思想,是面向對象編程(OOP)的一種補充。面向對象編程將程序抽象成各個層次的對象,而面向切面編程是將程序抽象成各個切面。
從《Spring實戰(第4版)》圖書中扒了一張圖:
從該圖可以很形象地看出,所謂切面,相當于應用對象間的橫切點,我們可以將其單獨抽象為單獨的模塊。
1.2、為什么需要 AOP想象下面的場景,開發中在多個模塊間有某段重復的代碼,我們通常是怎么處理的?顯然,沒有人會靠“復制粘貼”吧。在傳統的面向過程編程中,我們也會將這段代碼,抽象成一個方法,然后在需要的地方分別調用這個方法,這樣當這段代碼需要修改時,我們只需要改變這個方法就可以了。然而需求總是變化的,有一天,新增了一個需求,需要再多出做修改,我們需要再抽象出一個方法,然后再在需要的地方分別調用這個方法,又或者我們不需要這個方法了,我們還是得刪除掉每一處調用該方法的地方。實際上涉及到多個地方具有相同的修改的問題我們都可以通過 AOP 來解決。
1.3、AOP 實現分類AOP 要達到的效果是,保證開發者不修改源代碼的前提下,去為系統中的業務組件添加某種通用功能。AOP 的本質是由 AOP 框架修改業務組件的多個方法的源代碼,看到這其實應該明白了,AOP 其實就是前面一篇文章講的代理模式的典型應用。按照 AOP 框架修改源代碼的時機,可以將其分為兩類:
靜態 AOP 實現, AOP 框架在編譯階段對程序源代碼進行修改,生成了靜態的 AOP 代理類(生成的 *.class 文件已經被改掉了,需要使用特定的編譯器),比如 AspectJ。 動態 AOP 實現, AOP 框架在運行階段對動態生成代理對象(在內存中以 JDK 動態代理,或 CGlib 動態地生成 AOP 代理類),如 SpringAOP。下面給出常用 AOP 實現比較
如不清楚動態代理的,可參考我前面的一篇文章,有講解靜態代理、JDK動態代理和 CGlib 動態代理。
二、AOP 術語AOP 領域中的特性術語:
通知(Advice): AOP 框架中的增強處理。通知描述了切面何時執行以及如何執行增強處理。 連接點(join point): 連接點表示應用執行過程中能夠插入切面的一個點,這個點可以是方法的調用、異常的拋出。在 Spring AOP 中,連接點總是方法的調用。 切點(PointCut): 可以插入增強處理的連接點。 切面(Aspect): 切面是通知和切點的結合。 引入(Introduction):引入允許我們向現有的類添加新的方法或者屬性。 織入(Weaving): 將增強處理添加到目標對象中,并創建一個被增強的對象,這個過程就是織入。概念看起來總是有點懵,并且上述術語,不同的參考書籍上翻譯還不一樣,所以需要慢慢在應用中理解。
三、初步認識 Spring AOP3.1、Spring AOP 的特點AOP 框架有很多種,1.3節中介紹了 AOP 框架的實現方式有可能不同, Spring 中的 AOP 是通過動態代理實現的。不同的 AOP 框架支持的連接點也有所區別,例如,AspectJ 和 JBoss,除了支持方法切點,它們還支持字段和構造器的連接點。而 Spring AOP 不能攔截對對象字段的修改,也不支持構造器連接點,我們無法在 Bean 創建時應用通知。
3.2、Spring AOP 的簡單例子下面先上代碼,對著代碼說比較好說,看下面這個例子:這個例子是基于gradle創建的,首先 build.gradle 文件添加依賴:
dependencies { compile ’org.springframework:spring-context:5.0.6.RELEASE’}
首先創建一個接口 IBuy.java
package com.sharpcj.aopdemo.test1;public interface IBuy { String buy();}
Boy 和 Gril 兩個類分別實現了這個接口:
Boy.java
package com.sharpcj.aopdemo.test1;import org.springframework.stereotype.Component;@Componentpublic class Boy implements IBuy { @Override public String buy() {System.out.println('男孩買了一個游戲機');return '游戲機'; }}
Girl.java
package com.sharpcj.aopdemo.test1;import org.springframework.stereotype.Component;@Componentpublic class Girl implements IBuy { @Override public String buy() {System.out.println('女孩買了一件漂亮的衣服');return '衣服'; }}
配置文件, AppConfig.java
package com.sharpcj.aopdemo;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class})public class AppConfig {}
測試類, AppTest.java
package com.sharpcj.aopdemo;import com.sharpcj.aopdemo.test1.Boy;import com.sharpcj.aopdemo.test1.Girl;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class AppTest { public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);Boy boy = context.getBean('boy',Boy.class);Girl girl = (Girl) context.getBean('girl');boy.buy();girl.buy(); }}
運行結果:
這里運用SpringIOC里的自動部署。現在需求改變了,我們需要在男孩和女孩的 buy 方法之前,需要打印出“男孩女孩都買了自己喜歡的東西”。用 Spring AOP 來實現這個需求只需下面幾個步驟:
1、 既然用到 Spring AOP, 首先在 build.gralde 文件中引入相關依賴:
dependencies { compile ’org.springframework:spring-context:5.0.6.RELEASE’ compile ’org.springframework:spring-aspects:5.0.6.RELEASE’}
2、 定義一個切面類,BuyAspectJ.java
package com.sharpcj.aopdemo.test1;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;@Aspect@Componentpublic class BuyAspectJ { @Before('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void haha(){System.out.println('男孩女孩都買自己喜歡的東西'); }}
這個類,我們使用了注解 @Component 表明它將作為一個Spring Bean 被裝配,使用注解 @Aspect 表示它是一個切面。
類中只有一個方法 haha 我們使用 @Before 這個注解,表示他將在方法執行之前執行。關于這個注解后文再作解釋。
參數('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') 聲明了切點,表明在該切面的切點是com.sharpcj.aopdemo.test1.Ibuy這個接口中的buy方法。至于為什么這么寫,下文再解釋。
3、 在配置文件中啟用AOP切面功能
package com.sharpcj.aopdemo;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration@ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class})@EnableAspectJAutoProxy(proxyTargetClass = true)public class AppConfig {}
我們在配置文件類增加了@EnableAspectJAutoProxy注解,啟用了 AOP 功能,參數proxyTargetClass的值設為了 true 。默認值是 false,兩者的區別下文再解釋。
OK,下面只需測試代碼,運行結果如下:
我們看到,結果與我們需求一致,我們并沒有修改 Boy 和 Girl 類的 Buy 方法,也沒有修改測試類的代碼,幾乎是完全無侵入式地實現了需求。這就是 AOP 的“神奇”之處。
四、通過注解配置 Spring AOP4.1、通過注解聲明切點指示器Spring AOP 所支持的 AspectJ 切點指示器
在spring中嘗試使用AspectJ其他指示器時,將會拋出IllegalArgumentException異常。
當我們查看上面展示的這些spring支持的指示器時,注意只有execution指示器是唯一的執行匹配,而其他的指示器都是用于限制匹配的。這說明execution指示器是我們在編寫切點定義時最主要使用的指示器,在此基礎上,我們使用其他指示器來限制所匹配的切點。
下圖的切點表達式表示當Instrument的play方法執行時會觸發通知。
我們使用execution指示器選擇Instrument的play方法,方法表達式以 * 號開始,標識我們不關心方法的返回值類型。然后我們指定了全限定類名和方法名。對于方法參數列表,我們使用 .. 標識切點選擇任意的play方法,無論該方法的入參是什么。
多個匹配之間我們可以使用鏈接符 &&、||、!來表示 “且”、“或”、“非”的關系。但是在使用 XML 文件配置時,這些符號有特殊的含義,所以我們使用 “and”、“or”、“not”來表示。
舉例:
限定該切點僅匹配的包是 com.sharpcj.aopdemo.test1,可以使用
execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*)
在切點中選擇 bean,可以使用
execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && bean(girl)
修改 BuyAspectJ.java
package com.sharpcj.aopdemo.test1;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;@Aspect@Componentpublic class BuyAspectJ { @Before('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*) && bean(girl)') public void hehe(){System.out.println('男孩女孩都買自己喜歡的東西'); }}
此時,切面只會對 Girl.java 這個類生效,執行結果:
細心的你,可能發現了,切面中的方法名,已經被我悄悄地從haha改成了hehe,絲毫沒有影響結果,說明方法名沒有影響。和 Spring IOC 中用 java 配置文件裝配 Bean 時,用@Bean 注解修飾的方法名一樣,沒有影響。
4.2、通過注解聲明 5 種通知類型Spring AOP 中有 5 中通知類型,分別如下:
下面修改切面類:
package com.sharpcj.aopdemo.test1;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Aspect@Componentpublic class BuyAspectJ { @Before('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void hehe() {System.out.println('before ...'); } @After('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void haha() {System.out.println('After ...'); } @AfterReturning('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void xixi() {System.out.println('AfterReturning ...'); } @Around('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void xxx(ProceedingJoinPoint pj) {try { System.out.println('Around aaa ...'); pj.proceed(); System.out.println('Around bbb ...');} catch (Throwable throwable) { throwable.printStackTrace();} }}
為了方便看效果,我們測試類中,只要 Boy 類:
package com.sharpcj.aopdemo;import com.sharpcj.aopdemo.test1.Boy;import com.sharpcj.aopdemo.test1.Girl;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class AppTest { public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);Boy boy = context.getBean('boy',Boy.class);Girl girl = (Girl) context.getBean('girl');boy.buy();// girl.buy(); }}
執行結果如下:
結果顯而易見。指的注意的是 @Around 修飾的環繞通知類型,是將整個目標方法封裝起來了,在使用時,我們傳入了 ProceedingJoinPoint 類型的參數,這個對象是必須要有的,并且需要調用 ProceedingJoinPoint 的 proceed() 方法。 如果沒有調用 該方法,執行結果為 :
Around aaa ...
Around bbb ...
After ...
AfterReturning ...
可見,如果不調用該對象的 proceed() 方法,表示原目標方法被阻塞調用,當然也有可能你的實際需求就是這樣。
4.3、通過注解聲明切點表達式如你看到的,上面我們寫的多個通知使用了相同的切點表達式,對于像這樣頻繁出現的相同的表達式,我們可以使用 @Pointcut注解聲明切點表達式,然后使用表達式,修改代碼如下:
BuyAspectJ.java
package com.sharpcj.aopdemo.test1;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Aspect@Componentpublic class BuyAspectJ { @Pointcut('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void point(){} @Before('point()') public void hehe() {System.out.println('before ...'); } @After('point()') public void haha() {System.out.println('After ...'); } @AfterReturning('point()') public void xixi() {System.out.println('AfterReturning ...'); } @Around('point()') public void xxx(ProceedingJoinPoint pj) {try { System.out.println('Around aaa ...'); pj.proceed(); System.out.println('Around bbb ...');} catch (Throwable throwable) { throwable.printStackTrace();} }}
程序運行結果沒有變化。
這里,我們使用
@Pointcut('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))')public void point(){}
聲明了一個切點表達式,該方法 point 的內容并不重要,方法名也不重要,實際上它只是作為一個標識,供通知使用。
4.4、通過注解處理通知中的參數上面的例子,我們要進行增強處理的目標方法沒有參數,下面我們來說說有參數的情況,并且在增強處理中使用該參數。下面我們給接口增加一個參數,表示購買所花的金錢。通過AOP 增強處理,如果女孩買衣服超過了 68 元,就可以贈送一雙襪子。
更改代碼如下:
IBuy.java
package com.sharpcj.aopdemo.test1;public interface IBuy { String buy(double price);}
Girl.java
package com.sharpcj.aopdemo.test1;import org.springframework.stereotype.Component;@Componentpublic class Girl implements IBuy { @Override public String buy(double price) {System.out.println(String.format('女孩花了%s元買了一件漂亮的衣服', price));return '衣服'; }}
Boy.java
package com.sharpcj.aopdemo.test1;import org.springframework.stereotype.Component;@Componentpublic class Boy implements IBuy { @Override public String buy(double price) {System.out.println(String.format('男孩花了%s元買了一個游戲機', price));return '游戲機'; }}
再看 BuyAspectJ 類,我們將之前的通知都注釋掉。用一個環繞通知來實現這個功能:
package com.sharpcj.aopdemo.test1;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Aspect@Componentpublic class BuyAspectJ { /* @Pointcut('execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))') public void point(){} @Before('point()') public void hehe() {System.out.println('before ...'); } @After('point()') public void haha() {System.out.println('After ...'); } @AfterReturning('point()') public void xixi() {System.out.println('AfterReturning ...'); } @Around('point()') public void xxx(ProceedingJoinPoint pj) {try { System.out.println('Around aaa ...'); pj.proceed(); System.out.println('Around bbb ...');} catch (Throwable throwable) { throwable.printStackTrace();} } */ @Pointcut('execution(String com.sharpcj.aopdemo.test1.IBuy.buy(double)) && args(price) && bean(girl)') public void gif(double price) { } @Around('gif(price)') public String hehe(ProceedingJoinPoint pj, double price){try { pj.proceed(); if (price > 68) {System.out.println('女孩買衣服超過了68元,贈送一雙襪子');return '衣服和襪子'; }} catch (Throwable throwable) { throwable.printStackTrace();}return '衣服'; }}
前文提到,當不關心方法返回值的時候,我們在編寫切點指示器的時候使用了 * , 當不關心方法參數的時候,我們使用了 ..。現在如果我們需要傳入參數,并且有返回值的時候,則需要使用對應的類型。在編寫通知的時候,我們也需要聲明對應的返回值類型和參數類型。
測試類:AppTest.java
package com.sharpcj.aopdemo;import com.sharpcj.aopdemo.test1.Boy;import com.sharpcj.aopdemo.test1.Girl;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class AppTest { public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);Boy boy = context.getBean('boy',Boy.class);Girl girl = (Girl) context.getBean('girl');String boyBought = boy.buy(35);String girlBought = girl.buy(99.8);System.out.println('男孩買到了:' + boyBought);System.out.println('女孩買到了:' + girlBought); }}
測試結果:
可以看到,我們成功通過 AOP 實現了需求,并將結果打印了出來。
前面還有一個遺留問題,在配置文件中,我們用注解 @EnableAspectJAutoProxy() 啟用Spring AOP 的時候,我們給參數 proxyTargetClass 賦值為 true,如果我們不寫參數,默認為 false。這個時候運行程序,程序拋出異常
這是一個強制類型轉換異常。為什么會拋出這個異常呢?或許已經能夠想到,這跟Spring AOP 動態代理的機制有關,這個 proxyTargetClass 參數決定了代理的機制。當這個參數為 false 時,通過jdk的基于接口的方式進行織入,這時候代理生成的是一個接口對象,將這個接口對象強制轉換為實現該接口的一個類,自然就拋出了上述類型轉換異常。
反之,proxyTargetClass 為 true,則會使用 cglib 的動態代理方式。這種方式的缺點是拓展類的方法被final修飾時,無法進行織入。
測試一下,我們將 proxyTargetClass 參數設為 true,同時將 Girl.java 的 Buy 方法用 final 修飾:
AppConfig.java
package com.sharpcj.aopdemo;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration@ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class})@EnableAspectJAutoProxy(proxyTargetClass = true)public class AppConfig {}
Girl.java
package com.sharpcj.aopdemo.test1;import org.springframework.stereotype.Component;@Componentpublic class Girl implements IBuy { @Override public final String buy(double price) {System.out.println(String.format('女孩花了%s元買了一件漂亮的衣服', price));return '衣服'; }}
此時運行結果:
可以看到,我們的切面并沒有織入生效。
五、通過 XML 配置文件聲明切面前面的示例中,我們已經展示了如何通過注解配置去聲明切面,下面我們看看如何在 XML 文件中聲明切面。下面先列出 XML 中聲明 AOP 的常用元素:
我們依然可以使用 <aop:aspectj-autoproxy> 元素,他能夠自動代理AspectJ注解的通知類。
5.1、XML 配置文件中切點指示器在XML配置文件中,切點指示器表達式與通過注解配置的寫法基本一致,區別前面有提到,即XML文件中需要使用 “and”、“or”、“not”來表示 “且”、“或”、“非”的關系。
5.2、XML 文件配置 AOP 實例下面我們不使用任何注解改造上面的例子:BuyAspectJ.java
package com.sharpcj.aopdemo.test2;import org.aspectj.lang.ProceedingJoinPoint;public class BuyAspectJ { public void hehe() {System.out.println('before ...'); } public void haha() {System.out.println('After ...'); } public void xixi() {System.out.println('AfterReturning ...'); } public void xxx(ProceedingJoinPoint pj) {try { System.out.println('Around aaa ...'); pj.proceed(); System.out.println('Around bbb ...');} catch (Throwable throwable) { throwable.printStackTrace();} }}
在 Resource 目錄下新建一個配置文件 aopdemo.xml :
<?xml version='1.0' encoding='UTF-8'?><beans xmlns='http://www.springframework.org/schema/beans' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:aop='http://www.springframework.org/schema/aop' xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd'> <bean class='com.sharpcj.aopdemo.test2.Boy'></bean> <bean class='com.sharpcj.aopdemo.test2.Girl'></bean> <bean class='com.sharpcj.aopdemo.test2.BuyAspectJ'></bean> <aop:config proxy-target-class='true'><aop:aspect ref='buyAspectJ'> <aop:before pointcut='execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))' method='hehe'/> <aop:after pointcut='execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))' method='haha'/> <aop:after-returning pointcut='execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))' method='xixi'/> <aop:around pointcut='execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))' method='xxx'/></aop:aspect> </aop:config></beans>
這里分別定義了一個切面,里面包含四種類型的通知。
測試文件中,使用
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext('aopdemo.xml');
來獲取 ApplicationContext,其它代碼不變。
5.3、XML 文件配置聲明切點對于頻繁重復使用的切點表達式,我們也可以聲明成切點。
配置文件如下:aopdemo.xml
<?xml version='1.0' encoding='UTF-8'?><beans xmlns='http://www.springframework.org/schema/beans' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:aop='http://www.springframework.org/schema/aop' xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd'> <bean class='com.sharpcj.aopdemo.test2.Boy'></bean> <bean class='com.sharpcj.aopdemo.test2.Girl'></bean> <bean class='com.sharpcj.aopdemo.test2.BuyAspectJ'></bean> <aop:config proxy-target-class='true'><aop:pointcut expression='execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))'/><aop:aspect ref='buyAspectJ'> <aop:before pointcut-ref='apoint' method='hehe'/> <aop:after pointcut-ref='apoint' method='haha'/> <aop:after-returning pointcut-ref='apoint' method='xixi'/> <aop:around pointcut-ref='apoint' method='xxx'/></aop:aspect> </aop:config></beans>5.4、XML文件配置為通知傳遞參數
BuyAspectJ.java
package com.sharpcj.aopdemo.test2;import org.aspectj.lang.ProceedingJoinPoint;public class BuyAspectJ {public String hehe(ProceedingJoinPoint pj, double price){try { pj.proceed(); if (price > 68) {System.out.println('女孩買衣服超過了68元,贈送一雙襪子');return '衣服和襪子'; }} catch (Throwable throwable) { throwable.printStackTrace();}return '衣服'; }}
aopdemo.xml
<?xml version='1.0' encoding='UTF-8'?><beans xmlns='http://www.springframework.org/schema/beans' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:aop='http://www.springframework.org/schema/aop' xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd'> <bean class='com.sharpcj.aopdemo.test2.Boy'></bean> <bean class='com.sharpcj.aopdemo.test2.Girl'></bean> <bean class='com.sharpcj.aopdemo.test2.BuyAspectJ'></bean> <aop:config proxy-target-class='true'><aop:pointcut expression='execution(String com.sharpcj.aopdemo.test2.IBuy.buy(double)) and args(price) and bean(girl)'/><aop:aspect ref='buyAspectJ'> <aop:around pointcut-ref='apoint' method='hehe'/></aop:aspect> </aop:config></beans>5.5、Xml 文件配置織入的方式
同注解配置類似,
CGlib 代理方式:
<aop:config proxy-target-class='true'> </aop:config>
JDK 代理方式:
<aop:config proxy-target-class='false'> </aop:config>六、總結
本文簡單記錄了 AOP 的編程思想,然后介紹了 Spring 中 AOP 的相關概念,以及通過注解方式和XML配置文件兩種方式使用 Spring AOP進行編程。 相比于 AspectJ 的面向切面編程,Spring AOP 也有一些局限性,但是已經可以解決開發中的絕大多數問題了,如果確實遇到了 Spring AOP 解決不了的場景,我們依然可以在 Spring 中使用 AspectJ 來解決。
以上就是解析Spring中面向切面編程的詳細內容,更多關于Spring面向切面編程的資料請關注好吧啦網其它相關文章!
相關文章:
1. 解析原生JS getComputedStyle2. Idea 2019.3 本應該搜索到的插件卻搜索不到的解決方法3. SpringBoot+SpringCache實現兩級緩存(Redis+Caffeine)4. IntelliJ IDEA安裝插件的方法步驟5. 在JSP中使用formatNumber控制要顯示的小數位數方法6. ASP.NET MVC視圖頁使用jQuery傳遞異步數據的幾種方式詳解7. ASP.NET泛型三之使用協變和逆變實現類型轉換8. IntelliJ IDEA導入jar包的方法9. 每日六道java新手入門面試題,通往自由的道路10. 每日六道java新手入門面試題,通往自由的道路--多線程
