1. 程式人生 > >淺析Spring中AOP的實現原理——動態代理

淺析Spring中AOP的實現原理——動態代理

# 一、前言   最近在複習``Spring``的相關內容,剛剛大致研究了一下``Spring``中,``AOP``的實現原理。這篇部落格就來簡單地聊一聊``Spring``的``AOP``是如何實現的,並通過一個簡單的測試用例來驗證一下。廢話不多說,直接開始。
# 二、正文 ## 2.1 Spring AOP的實現原理   ``Spring``的``AOP``實現原理其實很簡單,就是通過**動態代理**實現的。如果我們為``Spring``的某個``bean``配置了切面,那麼``Spring``在建立這個``bean``的時候,實際上建立的是這個``bean``的一個代理物件,我們後續對``bean``中方法的呼叫,實際上呼叫的是代理類重寫的代理方法。而``Spring``的``AOP``使用了兩種動態代理,分別是**JDK的動態代理**,以及**CGLib的動態代理**。 **(一)JDK動態代理**   **Spring預設使用JDK的動態代理實現AOP,類如果實現了介面,Spring就會使用這種方式實現動態代理**。熟悉``Java``語言的應該會對``JDK``動態代理有所瞭解。``JDK``實現動態代理需要兩個元件,首先第一個就是``InvocationHandler``介面。我們在使用``JDK``的動態代理時,需要編寫一個類,去實現這個介面,然後重寫``invoke``方法,這個方法其實就是我們提供的代理方法。然後``JDK``動態代理需要使用的第二個元件就是``Proxy``這個類,我們可以通過這個類的``newProxyInstance``方法,返回一個代理物件。生成的代理類實現了原來那個類的所有介面,並對介面的方法進行了代理,我們通過代理物件呼叫這些方法時,底層將通過反射,呼叫我們實現的``invoke``方法。 **(二)CGLib動態代理**   ``JDK``的動態代理存在限制,那就是被代理的類必須是一個實現了介面的類,代理類需要實現相同的介面,代理介面中宣告的方法。若需要代理的類沒有實現介面,此時``JDK``的動態代理將沒有辦法使用,於是``Spring``會使用``CGLib``的動態代理來生成代理物件。``CGLib``直接操作位元組碼,生成類的子類,重寫類的方法完成代理。   以上就是``Spring``實現動態的兩種方式,下面我們具體來談一談這兩種生成動態代理的方式。
## 2.2 JDK的動態代理 **(一)實現原理**   ``JDK``的動態代理是基於**反射**實現。``JDK``通過反射,生成一個代理類,這個代理類實現了原來那個類的全部介面,並對介面中定義的所有方法進行了代理。當我們通過代理物件執行原來那個類的方法時,代理類底層會通過反射機制,回撥我們實現的``InvocationHandler``介面的``invoke``方法。**並且這個代理類是Proxy類的子類**(記住這個結論,後面測試要用)。這就是``JDK``動態代理大致的實現方式。 **(二)優點** 1. ``JDK``動態代理是``JDK``原生的,不需要任何依賴即可使用; 2. 通過反射機制生成代理類的速度要比``CGLib``操作位元組碼生成代理類的速度更快; **(三)缺點** 1. 如果要使用``JDK``動態代理,被代理的類必須實現了介面,否則無法代理; 2. ``JDK``動態代理無法為沒有在介面中定義的方法實現代理,假設我們有一個實現了介面的類,我們為它的一個不屬於介面中的方法配置了切面,``Spring``仍然會使用``JDK``的動態代理,但是由於配置了切面的方法不屬於介面,為這個方法配置的切面將不會被織入。 3. ``JDK``動態代理執行代理方法時,需要通過反射機制進行回撥,此時方法執行的效率比較低;
## 2.3 CGLib動態代理 **(一)實現原理**   ``CGLib``實現動態代理的原理是,底層採用了``ASM``位元組碼生成框架,直接對需要代理的類的位元組碼進行操作,生成這個類的一個子類,並重寫了類的所有可以重寫的方法,在重寫的過程中,將我們定義的額外的邏輯(簡單理解為``Spring``中的切面)織入到方法中,對方法進行了增強。而通過位元組碼操作生成的代理類,和我們自己編寫並編譯後的類沒有太大區別。 **(二)優點** 1. 使用``CGLib``代理的類,不需要實現介面,因為``CGLib``生成的代理類是直接繼承自需要被代理的類; 2. ``CGLib``生成的代理類是原來那個類的子類,這就意味著這個代理類可以為原來那個類中,所有能夠被子類重寫的方法進行代理; 3. ``CGLib``生成的代理類,和我們自己編寫並編譯的類沒有太大區別,對方法的呼叫和直接呼叫普通類的方式一致,所以``CGLib``執行代理方法的效率要高於``JDK``的動態代理; **(三)缺點** 1. 由於``CGLib``的代理類使用的是繼承,這也就意味著如果需要被代理的類是一個``final``類,則無法使用``CGLib``代理; 2. 由於``CGLib``實現代理方法的方式是重寫父類的方法,所以無法對``final``方法,或者``private``方法進行代理,因為子類無法重寫這些方法; 3. ``CGLib``生成代理類的方式是通過操作位元組碼,這種方式生成代理類的速度要比``JDK``通過反射生成代理類的速度更慢;
## 2.4 通過程式碼進行測試 **(一)測試JDK動態代理**   下面我們通過一個簡單的例子,來驗證上面的說法。首先我們需要一個介面和它的一個實現類,然後再為這個實現類的方法配置切面,看看``Spring``是否真的使用的是``JDK``的動態代理。假設介面的名稱為``Human``,而實現類為``Student``: ```java public interface Human { void display(); } @Component public class Student implements Human { @Override public void display() { System.out.println("I am a student"); } } ```   然後我們定義一個切面,將這個``display``方法作為切入點,為它配置一個前置通知,程式碼如下: ```java @Aspect @Component public class HumanAspect { // 為Student這個類的所有方法,配置這個前置通知 @Before("execution(* cn.tewuyiang.pojo.Student.*(..))") public void before() { System.out.println("before student"); } } ```   下面可以開始測試了,我們通過``Java``類的方式進行配置,然後編寫一個單元測試方法: ```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``使用了何種代理。下面是輸出結果: ```txt before student I am a student class java.lang.reflect.Proxy // 注意看,父類是Proxy ```   通過上面的輸出結果,我們發現,代理類的父類是``Proxy``,也就意味著果然使用的是``JDK``的動態代理。
**(二)測試CGLib動態代理**   好,測試完``JDK``動態代理,我們開始測試``CGLib``動態代理。我們前面說過,只有當需要代理的類沒有實現介面時,``Spring``才會使用``CGLib``動態代理,於是我們修改``Student``這個類的定義,不讓他實現介面: ```java @Component public class Student { public void display() { System.out.println("I am a student"); } } ```   由於``Student``沒有實現介面,所以我們的測試方法也需要做一些修改。之前我們是通過``Human.class``這個型別從``Spring``容器中獲取代理物件,但是現在,由於沒有實現介面,所以我們不能再這麼寫了,而是要寫成``Student.class``,如下: ```java @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``獲取。下面是輸出結果: ```txt before student I am a student class cn.tewuyiang.pojo.Student // 此時,父類是Student ```   可以看到,``AOP``成功生效,並且代理物件所屬類的父類是``Student``,驗證了我們之前的說法。下面我們修改一下``Student``類的定義,將``display``方法加上``final``修飾符,再看看效果: ```java @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``修飾符,讓他無法被繼承,此時看看結果。執行的結果會丟擲異常,因為無法生成代理類,這裡就不貼出來了,可以自己去試試。
## 2.5 強制Spring使用CGLib   通過上面的測試我們會發現,``CGLib``的動態代理好像更加強大,而``JDK``的動態代理卻限制頗多。而且前面也提過,``CGLib``的代理物件,執行代理方法的速度更快,只是生成代理類的效率較低。但是我們使用到的``bean``大部分都是單例的,並不需要頻繁建立代理類,也就是說``CGLib``應該會更合適。但是為什麼``Spring``預設使用``JDK``呢?這我也不太清楚,網上也沒有找到相關的描述(如果有人知道,麻煩告訴我)。但是據說``SpringBoot``現在已經預設使用``CGLib``作為``AOP``的實現了。   那我們可以強制``Spring``使用``CGLib``,而不使用``JDK``的動態代理嗎?答案當然是可以的。我們知道,如果要使用註解(``@Aspect``)方式配置切面,則需要在``xml``檔案中配置下面一行開啟``AOP``: ```xml ```   如果我們希望只使用``CGLib``實現``AOP``,則可以在上面的這一行加點東西: ```xml ```   當然,如果我們是使用``Java``類進行配置,比如說我們上面用到的``AOPConfig``這個類,如果是通過這種方式配置,則強制使用``CGLib``的方式如下: ```java @Configuration @ComponentScan(basePackages = "cn.tewuyiang") // 如下:@EnableAspectJAutoProxy開啟AOP, // 而proxyTargetClass = true就是強制使用CGLib @EnableAspectJAutoProxy(proxyTargetClass = true) public class AOPConfig { } ```   如果我們是在xml檔案中配置切面,則可以通過以下方式來強制使用``CGLib``: ```xml ```
# 三、總結   上面我們就對``Spring``中``AOP``的實現原理做了一個大致的介紹。歸根到底,``Spring AOP``的實現是通過動態代理,並且有兩種實現方式,分別是``JDK``動態代理和``CGLib``動態代理。``Spring``預設使用``JDK``動態代理,只有在類沒有實現介面時,才會使用``CGLib``。   上面的內容若存在錯誤或者不足,歡迎指正或補充。也希望這篇部落格對需要了解``Spring AOP``的人有所幫助。
# 四、參考 - [Spring-4.3.21官方文件——AOP](https://www.docs4dev.com/docs/zh/spring-framework/4.3.21.RELEASE/reference/aop.html#aop-proxying) -