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
實作動态的兩種方式,下面我們具體來談一談這兩種生成動态代理的方式。
JDK 的動态代理
- 實作原理
JDK的動态代理是基于反射實作。
JDK
通過反射,生成一個代理類,這個代理類實作了原來那個類的全部接口,并對接口中定義的所有方法進行了代理。當我們通過代理對象執行原來那個類的方法時,代理類底層會通過反射機制,回調我們實作的
InvocationHandler
接口的
invoke
方法。并且這個代理類是 Proxy 類的子類(記住這個結論,後面測試要用)。這就是
JDK
動态代理大緻的實作方式。
- 優點
-
動态代理是JDK
原生的,不需要任何依賴即可使用;JDK
- . 通過反射機制生成代理類的速度要比
操作位元組碼生成代理類的速度更快;CGLib
- 缺點
- 如果要使用
動态代理,被代理的類必須實作了接口,否則無法代理;JDK
-
動态代理無法為沒有在接口中定義的方法實作代理,假設我們有一個實作了接口的類,我們為它的一個不屬于接口中的方法配置了切面,JDK
仍然會使用Spring
的動态代理,但是由于配置了切面的方法不屬于接口,為這個方法配置的切面将不會被織入。JDK
-
動态代理執行代理方法時,需要通過反射機制進行回調,此時方法執行的效率比較低;JDK
CGLib 動态代理
(一)實作原理
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</code-pre> </pre></code-box>
可以看到,輸出的父類仍然是
Student
,也就是說
Spring
依然使用了
CGLib
生成代理。但是我們發現,我們為
display
方法配置的前置通知并沒有執行,也就是代理類并沒有為
display
方法進行代理。這也驗證了我們之前的說法,
CGLib
無法代理
final
方法,因為子類無法重寫父類的
final
方法。下面我們可以試着為
Student
類加上
final
修飾符,讓他無法被繼承,此時看看結果。運作的結果會抛出異常,因為無法生成代理類,這裡就不貼出來了,可以自己去試試。
強制 Spring 使用 CGLib
通過上面的測試我們會發現,
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-4.3.21 官方文檔——AOP