最近在複習Spring
的相關內容,剛剛大體研究了一下Spring
中,AOP
的實現原理。這篇博客就來簡單地聊一聊Spring
的AOP
是如何實現的,並經過一個簡單的測試用例來驗證一下。廢話很少說,直接開始。html
Spring
的AOP
實現原理其實很簡單,就是經過動態代理實現的。若是咱們爲Spring
的某個bean
配置了切面,那麼Spring
在建立這個bean
的時候,實際上建立的是這個bean
的一個代理對象,咱們後續對bean
中方法的調用,實際上調用的是代理類重寫的代理方法。而Spring
的AOP
使用了兩種動態代理,分別是JDK的動態代理,以及CGLib的動態代理。java
(一)JDK動態代理spring
Spring默認使用JDK的動態代理實現AOP,類若是實現了接口,Spring就會使用這種方式實現動態代理。熟悉Java
語言的應該會對JDK
動態代理有所瞭解。JDK
實現動態代理須要兩個組件,首先第一個就是InvocationHandler
接口。咱們在使用JDK
的動態代理時,須要編寫一個類,去實現這個接口,而後重寫invoke
方法,這個方法其實就是咱們提供的代理方法。而後JDK
動態代理須要使用的第二個組件就是Proxy
這個類,咱們能夠經過這個類的newProxyInstance
方法,返回一個代理對象。生成的代理類實現了原來那個類的全部接口,並對接口的方法進行了代理,咱們經過代理對象調用這些方法時,底層將經過反射,調用咱們實現的invoke
方法。框架
(二)CGLib動態代理ide
JDK
的動態代理存在限制,那就是被代理的類必須是一個實現了接口的類,代理類須要實現相同的接口,代理接口中聲明的方法。若須要代理的類沒有實現接口,此時JDK
的動態代理將沒有辦法使用,因而Spring
會使用CGLib
的動態代理來生成代理對象。CGLib
直接操做字節碼,生成類的子類,重寫類的方法完成代理。單元測試
以上就是Spring
實現動態的兩種方式,下面咱們具體來談一談這兩種生成動態代理的方式。測試
(一)實現原理.net
JDK
的動態代理是基於反射實現。JDK
經過反射,生成一個代理類,這個代理類實現了原來那個類的所有接口,並對接口中定義的全部方法進行了代理。當咱們經過代理對象執行原來那個類的方法時,代理類底層會經過反射機制,回調咱們實現的InvocationHandler
接口的invoke
方法。而且這個代理類是Proxy類的子類(記住這個結論,後面測試要用)。這就是JDK
動態代理大體的實現方式。代理
(二)優勢code
JDK
動態代理是JDK
原生的,不須要任何依賴便可使用;CGLib
操做字節碼生成代理類的速度更快;(三)缺點
JDK
動態代理,被代理的類必須實現了接口,不然沒法代理;JDK
動態代理沒法爲沒有在接口中定義的方法實現代理,假設咱們有一個實現了接口的類,咱們爲它的一個不屬於接口中的方法配置了切面,Spring
仍然會使用JDK
的動態代理,可是因爲配置了切面的方法不屬於接口,爲這個方法配置的切面將不會被織入。JDK
動態代理執行代理方法時,須要經過反射機制進行回調,此時方法執行的效率比較低;(一)實現原理
CGLib
實現動態代理的原理是,底層採用了ASM
字節碼生成框架,直接對須要代理的類的字節碼進行操做,生成這個類的一個子類,並重寫了類的全部能夠重寫的方法,在重寫的過程當中,將咱們定義的額外的邏輯(簡單理解爲Spring
中的切面)織入到方法中,對方法進行了加強。而經過字節碼操做生成的代理類,和咱們本身編寫並編譯後的類沒有太大區別。
(二)優勢
CGLib
代理的類,不須要實現接口,由於CGLib
生成的代理類是直接繼承自須要被代理的類;CGLib
生成的代理類是原來那個類的子類,這就意味着這個代理類能夠爲原來那個類中,全部可以被子類重寫的方法進行代理;CGLib
生成的代理類,和咱們本身編寫並編譯的類沒有太大區別,對方法的調用和直接調用普通類的方式一致,因此CGLib
執行代理方法的效率要高於JDK
的動態代理;(三)缺點
CGLib
的代理類使用的是繼承,這也就意味着若是須要被代理的類是一個final
類,則沒法使用CGLib
代理;CGLib
實現代理方法的方式是重寫父類的方法,因此沒法對final
方法,或者private
方法進行代理,由於子類沒法重寫這些方法;CGLib
生成代理類的方式是經過操做字節碼,這種方式生成代理類的速度要比JDK
經過反射生成代理類的速度更慢;(一)測試JDK動態代理
下面咱們經過一個簡單的例子,來驗證上面的說法。首先咱們須要一個接口和它的一個實現類,而後再爲這個實現類的方法配置切面,看看Spring
是否真的使用的是JDK
的動態代理。假設接口的名稱爲Human
,而實現類爲Student
:
public interface Human { void display(); } @Component public class Student implements Human { @Override public void display() { System.out.println("I am a student"); } }
而後咱們定義一個切面,將這個display
方法做爲切入點,爲它配置一個前置通知,代碼以下:
@Aspect @Component public class HumanAspect { // 爲Student這個類的全部方法,配置這個前置通知 @Before("execution(* cn.tewuyiang.pojo.Student.*(..))") public void before() { System.out.println("before student"); } }
下面能夠開始測試了,咱們經過Java
類的方式進行配置,而後編寫一個單元測試方法:
// 配置類 @Configuration @ComponentScan(basePackages = "cn.tewuyiang") @EnableAspectJAutoProxy public class AOPConfig { } // 測試方法 @Test public void testProxy() { ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class); // 注意,這裏只能經過Human.class獲取,而沒法經過Student.class,由於在Spirng容器中, // 由於使用JDK動態代理,Ioc容器中,存儲的是一個類型爲Human的代理對象 Human human = context.getBean(Human.class); human.display(); // 輸出代理類的父類,以此判斷是JDK仍是CGLib System.out.println(human.getClass().getSuperclass()); }
注意看上面代碼中,最長的那一句註釋。因爲咱們須要代理的類實現了接口,則Spring
會使用JDK
的動態代理,生成的代理類會實現相同的接口,而後建立一個代理對象存儲在Spring
容器中。這也就是說,在Spring
容器中,這個代理bean
的類型不是Student
類型,而是Human
類型,因此咱們不能經過Student.class
獲取,只能經過Human.class
(或者經過它的名稱獲取)。這也證實了咱們上面說過的另外一個問題,JDK
動態代理沒法代理沒有定義在接口中的方法。假設Student
這個類有另一個方法,它不是Human
接口定義的方法,此時就算咱們爲它配置了切面,也沒法將切面織入。並且因爲在Spring
容器中保存的代理對象並非Student
類型,而是Human
類型,這就致使咱們連那個不屬於Human
的方法都沒法調用。這也說明了JDK
動態代理的侷限性。
咱們前面說過,JDK
動態代理生成的代理類繼承了Proxy
這個類,而CGLib
生成的代理類,則繼承了須要進行代理的那個類,因而咱們能夠經過輸出代理對象所屬類的父類,來判斷Spring
使用了何種代理。下面是輸出結果:
before student I am a student class java.lang.reflect.Proxy // 注意看,父類是Proxy
經過上面的輸出結果,咱們發現,代理類的父類是Proxy
,也就意味着果真使用的是JDK
的動態代理。
(二)測試CGLib動態代理
好,測試完JDK
動態代理,咱們開始測試CGLib
動態代理。咱們前面說過,只有當須要代理的類沒有實現接口時,Spring
纔會使用CGLib
動態代理,因而咱們修改Student
這個類的定義,不讓他實現接口:
@Component public class Student { public void display() { System.out.println("I am a student"); } }
因爲Student
沒有實現接口,因此咱們的測試方法也須要作一些修改。以前咱們是經過Human.class
這個類型從Spring
容器中獲取代理對象,可是如今,因爲沒有實現接口,因此咱們不能再這麼寫了,而是要寫成Student.class
,以下:
@Test public void testProxy() { ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class); // 修改成Student.class Student student = context.getBean(Student.class); student.display(); // 一樣輸出父類 System.out.println(student.getClass().getSuperclass()); }
由於CGLib
動態代理是生成了Student
的一個子類,因此這個代理對象也是Student
類型(子類也是父類類型),因此能夠經過Student.class
獲取。下面是輸出結果:
before student I am a student class cn.tewuyiang.pojo.Student // 此時,父類是Student
能夠看到,AOP
成功生效,而且代理對象所屬類的父類是Student
,驗證了咱們以前的說法。下面咱們修改一下Student
類的定義,將display
方法加上final
修飾符,再看看效果:
@Component public class Student { // 加上final修飾符 public final void display() { System.out.println("I am a student"); } } // 輸出結果以下: I am a student class cn.tewuyiang.pojo.Student
能夠看到,輸出的父類仍然是Student
,也就是說Spring
依然使用了CGLib
生成代理。可是咱們發現,咱們爲display
方法配置的前置通知並無執行,也就是代理類並無爲display
方法進行代理。這也驗證了咱們以前的說法,CGLib
沒法代理final
方法,由於子類沒法重寫父類的final
方法。下面咱們能夠試着爲Student
類加上final
修飾符,讓他沒法被繼承,此時看看結果。運行的結果會拋出異常,由於沒法生成代理類,這裏就不貼出來了,能夠本身去試試。
經過上面的測試咱們會發現,CGLib
的動態代理好像更增強大,而JDK
的動態代理卻限制頗多。並且前面也提過,CGLib
的代理對象,執行代理方法的速度更快,只是生成代理類的效率較低。可是咱們使用到的bean
大部分都是單例的,並不須要頻繁建立代理類,也就是說CGLib
應該會更合適。可是爲何Spring
默認使用JDK
呢?這我也不太清楚,網上也沒有找到相關的描述(若是有人知道,麻煩告訴我)。可是聽說SpringBoot
如今已經默認使用CGLib
做爲AOP
的實現了。
那咱們能夠強制Spring
使用CGLib
,而不使用JDK
的動態代理嗎?答案固然是能夠的。咱們知道,若是要使用註解(@Aspect
)方式配置切面,則須要在xml
文件中配置下面一行開啓AOP
:
<aop:aspectj-autoproxy />
若是咱們但願只使用CGLib
實現AOP
,則能夠在上面的這一行加點東西:
<!-- 將proxy-target-class配置設置爲true --> <aop:aspectj-autoproxy proxy-target-class="true"/>
固然,若是咱們是使用Java
類進行配置,好比說咱們上面用到的AOPConfig
這個類,若是是經過這種方式配置,則強制使用CGLib
的方式以下:
@Configuration @ComponentScan(basePackages = "cn.tewuyiang") // 以下:@EnableAspectJAutoProxy開啓AOP, // 而proxyTargetClass = true就是強制使用CGLib @EnableAspectJAutoProxy(proxyTargetClass = true) public class AOPConfig { }
若是咱們是在xml文件中配置切面,則能夠經過如下方式來強制使用CGLib
:
<!-- aop:config用來在xml中配置切面,指定proxy-target-class="true" --> <aop:config proxy-target-class="true"> <!-- 在其中配置AOP --> </aop:config>
上面咱們就對Spring
中AOP
的實現原理作了一個大體的介紹。歸根到底,Spring AOP
的實現是經過動態代理,而且有兩種實現方式,分別是JDK
動態代理和CGLib
動態代理。Spring
默認使用JDK
動態代理,只有在類沒有實現接口時,纔會使用CGLib
。
上面的內容若存在錯誤或者不足,歡迎指正或補充。也但願這篇博客對須要瞭解Spring AOP
的人有所幫助。