01 Spring AOP的原理
1。1 什麼是 AOP ?
在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,透過預編譯方式和執行期間動態代理實現程式功能的統一維護的一種技術。
AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
02 Spring AOP的引入
2。1 介面呼叫耗時
現在我們有個介面要在日誌中記錄介面耗時,我們會怎麼做呢?一般我們會在介面開始和介面結束時獲取系統時間,然後二者一減就是介面耗時時間了。如下,在20行我們打印出介面耗時。
@RestController@Slf4jpublic class LoginController { @Autowired LoginService loginService; @RequestMapping(“/login/{id}”) public Map
啟動類:
@SpringBootApplicationpublic class SpringaopSbApplication { public static void main(String[] args) { SpringApplication。run(SpringaopSbApplication。class, args); }}
但是,如果所有介面都要記錄耗時時間呢?我們還按這種方式嗎?顯然不行,這種要在每個介面都加上同樣的程式碼,而且如果後期你老闆說去掉的話,你還有一個個的刪掉麼?簡直是不可想象。。
所以對於這種需求,其實是可以提煉出來的。我們想,統計介面的耗時時間,無非就是在介面的執行前後記錄一下時然後相減打印出來即可,然後在這樣的地方去加入我們提煉出來的公共的程式碼。這就好比在原來的業務程式碼的基礎上,把原來的程式碼橫切開來,在需要的地方加入公共的程式碼,對原來的業務程式碼起到功能增強的作用。
這就是AOP的作用。
2。2 Spring AOP應用場景-介面耗時記錄
下面我們來看看使用Spring AOP怎麼滿足這個需求。
首先定義一個
切面類
TimeMoitor,其中pointCut()方法(修飾一組連線點)是一個
切點
,@Pointcut定義了一組
連線點
(使用表示式匹配)
aroundTimeCounter()是要加入的功能,被@Around註解修飾,是一個
環繞通知
(Spring AOP通知的一種),其實就是上面說的在方法執行前後記錄時間然後相減再打印出來耗時時間。
@Aspect@Component@Slf4jpublic class TimeMoitor { @Pointcut(value = “execution(* com。walking。springaopsb。controller。*。*(。。))”) public void pointCut(){} @Around(value = “com。walking。springaopsb。aop。TimeMoitor。pointCut()”) public Object aroundTimeCounter(ProceedingJoinPoint jpx){ long start = System。currentTimeMillis(); Object proceed = null; try { proceed = jpx。proceed(); } catch (Throwable throwable) { throwable。printStackTrace(); } long end = System。currentTimeMillis(); log。info(“耗時=>{}ms”,end-start); return proceed; }}
然後在LoginController#login方法裡我們就可以把日誌列印耗時時間的程式碼刪掉了。
@RestController@Slf4jpublic class LoginController { @Autowired LoginService loginService; @RequestMapping(“/login/{id}”) public Map
再比如,LoginController裡若是還有別的方法,也一樣可以應用到。
使用Spring AOP的控制檯日誌:
03 Spring AOP原始碼分析
3。1 Spring AOP的原理
以上就是Spring AOP的一個應用場景。那Spring AOP的原理是什麼呢,用的什麼技術呢?
其實就是反射+動態代理。代理用的就是JDK動態代理或cglib,
那麼Spring AOP什麼時候用JDK動態代理什麼時候用cglib?預設使用哪種?
因為Spring的原始碼非常的複雜,方法呼叫棧很深,很多同學都無從下手,debug進去之後就出不來了,會非常的亂。所以下面我們就針對上述問題來根據原始碼探究一下吧
首先我們將啟動類改一下,方便我們對原始碼debug。
啟動類:
@ComponentScan(“com。walking。springaopsb。*”)@EnableAspectJAutoProxypublic class SpringaopSbApplication { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringaopSbApplication。class); LoginController loginController = (LoginController) applicationContext。getBean(“loginController”); loginController。login(123); }}
我們修改了一下啟動類,把斷點打在第6行,啟動,往下走一步,看loginController這個變數。
我們發現是cglib方式產生的代理類,說明從IoC容器裡拿到的是代理類,到底是初始化IoC容器時生成的還是getBean獲取時產生的呢?我們也跟隨原始碼來看一下吧。
要知道的是,我們現在要看的是第5行還是第6行生成的代理類。因為如果直接看第5行裡面的方法呼叫比較多,不知道到底在哪個方法是初始化例項,所以我們從getBean入手,先看第6 行的getBean吧,進入這個方法
org.springframework.context.support.AbstractApplicationContext#getBean(java.lang.String)
。
然後我們只看有return的地方,在進入這個
getBean(org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String))
。
再看
doGetBean(org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean)
第120行sharedInstance已經變成了代理類
所以我們進入
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)
方法看看,重新執行,然後再加個斷點,打到
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
裡。
走過88行後,singletonObject變成了代理類,所以關鍵點就是在
this.singletonObjects.get(beanName);
我們可以看到singletonObjects 是一個ConcurrentHashMap。原來IoC的例項在這個ConcurrentHashMap裡。
private final Map
所以到這裡我們就可以知道,這個代理類不是在getBean的時候生成的,即不是在啟動類的第6行生成的,那就是在第5行生成的,即在IoC容器初始化時產生的代理類。
剛才那個ConcurrentHashMap是get的,那就肯定有put的時候。搜一下,還在這個類裡,發現一個addSingleton方法,有倆地方呼叫,一個是在
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#registerSingleton
呼叫的,一個是在
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)
那就把斷點打到這倆方法裡,看會走到哪個,把別的斷點都去掉,當然了,因為spring還有別的自己的例項要獲取,IoC容器裡還有spring自己的例項,所以這個斷點要加上條件,當beanName是loginController時進去斷點,這樣就方便多了。我們只保留第5行的程式碼,因為getBean裡面也會調getSingleton。
執行啟動類,發現進入了getSingleton方法,但
Object singletonObject = this.singletonObjects.get(beanName);
返回的為null,所以繼續往下走。發現在第127行返回了代理類,看這行的getObject方法又不知道是那個實現類,所以我們去左下角看方法棧,找一下這個方法的上一個方法,
就是上圖左下角的第二個方法doGetBean,發現傳的是一個匿名內部類,這個匿名內部類裡調的是
org.springframework.beans.factory.support.AbstractBeanFactory#createBean
所以我們把斷點走完,進到這個createBean裡打斷點,同樣加條件。
斷點走過324行時變成代理類,即進入
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
看看,打個斷點同樣加條件
斷點走過doCreateBean方法第380行後產生了代理類,所以把斷點打到這個
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)
方法裡,同樣加上條件,把別的斷點去掉,重新執行。
當走過1240行時已經變成了代理類,所以把斷點打到這個
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization
方法,同樣加上條件,把別的斷點去掉,重新執行。
我們發現,這裡有個迴圈,迭代的是this。getBeanPostProcessors()的結果,我們看看這個是什麼,是List,下圖是這個list的資料
經過幾次debug發現當BeanPostProcessor為第四個元素時
AnnotationAwareAspectJAutoProxyCreator
的例項,result變成了代理類。關鍵就是在
processor.postProcessAfterInitialization()
這個方法,把斷點打進去。
發現沒有
AnnotationAwareAspectJAutoProxyCreator
這個實現類
那就看看這個
AnnotationAwareAspectJAutoProxyCreator
的父類吧,
Ctrl + Alt + Shift + U
檢視
AnnotationAwareAspectJAutoProxyCreator
的類圖依賴關係
發現AbstractAutoProxyCreator在上上個圖中,並且
AnnotationAwareAspectJAutoProxyCreator
沒有重寫
postProcessAfterInitialization
方法,
所以我們就看
AbstractAutoProxyCreator
的這個方法。
打斷點時發現Object bean不是代理類,那就看看
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary
方法。在這個方法中呼叫了createProxy()建立代理類,進去看下。
這個方法最後
return proxyFactory.getProxy(getProxyClassLoader());
進入getProxy方法看看
所以
createAopProxy()
方法返回AopProxy型別的例項,有倆實現類可供建立
CglibAopProxy
和
JdkDynamicAopProxy
,及cglib和jdk動態代理兩種。
那麼究竟建立哪一種,就是我們今天要看的關鍵之處,所以我們進入createAopProxy()方法看看。
再進去
org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
方法看看。
config.isOptimize()
和
config.isProxyTargetClass()
都預設false
這裡建立
logincontroller
時
config
的資料如下
然後判斷targetClass是否為介面,這裡我們的LoginController不是介面,就走了下面的return
所以Spring AOP使用JDK動態代理還是cglib取決於是否是介面,並沒有預設的方式。
我們改一下LoginController讓其實現介面
debug啟動,這時得到的代理類就是JDK動態代理。
3。2 為什麼JDK動態代理必須是介面?
我們看一下這個問題,首先把LoginController改為實現ILoginBaseController介面,然後根據咱們上面的debug分析,在
org.springframework.aop.framework.ProxyFactory#getProxy(java.lang.ClassLoader)
方法裡createAopProxy()。getProxy就是我們解決這個問題的入口,我們在getProxy裡打上斷點,
JdkDynamicAopProxy#getProxy(java.lang.ClassLoader)
方法裡斷點加到return語句上
return Proxy。newProxyInstance(classLoader, proxiedInterfaces, this);
然後在Proxy。newProxyInstance進來加斷點,一步步往下走,在719行是關鍵
進去
進入proxyClassCache。get方法
然後第120行時關鍵,我們看這個apply方法是BiFunction介面的方法,有如下實現類,把滑鼠放到subKeyFactory上去發現是KeyFactory型別的,進debug去看,沒有我們想要的
然後繼續往下走,有個while迴圈,經過幾次debug,發現這個迴圈是關鍵,具體看圖中標註
我們需要進這個get
進來get之後發現有一行關鍵點,就是下圖的230行,還是有個apply方法
剛才也說過了他有如下實現類
透過看valueFactory的型別知道他是ProxyClassFactory型別的,然後進入這個類。他是Proxy類的一個靜態內部類。
經過多次debug發現639-643行是關鍵,其中第639行是獲取位元組碼,然後第642行呼叫defineClass0(一個
native方法
)建立例項。
這裡加個小插曲,為什麼java的動態代理生成的代理類前面有個$Proxy呢,在這裡可以得到答案。
回到剛才,位元組碼我們看不懂,但是可以反編譯我們把639行拿出來寫個測試類
public class Test { public static void main(String[] args) throws Exception { //獲取ILoginBaseController的位元組碼 byte[] bytes = ProxyGenerator。generateProxyClass( “$Proxy#MyLoginController”, new Class[]{ILoginBaseController。class}); //輸出到MyLoginController。class檔案 FileOutputStream fileOutputStream = new FileOutputStream(new File(“MyLoginController。class”)); fileOutputStream。write(bytes); fileOutputStream。flush(); fileOutputStream。close(); }}
我們會看到生成了指定的檔案
看到這個檔案你是不是就明白為啥JDK動態代理只能是介面了嗎?原因就是java中是單繼承多實現,$Proxy#MyLoginController類已經繼承了Proxy類,所以不能在繼承別的類了只能實現介面,所以JDK動態代理只能是介面。
04 總結
透過以上的原始碼分析我們弄清楚了,Spring AOP使用的代理機制了,並且是沒有預設的代理,不是JDK動態代理就是cglib,以及為啥java的動態代理只能是介面。並且我們還看了一下spring的原始碼,雖然看的不是非常的仔細,但是透過這樣看原始碼我們的理解更加的加深了,也鍛鍊了看原始碼的能力。
原文連結:https://mp。weixin。qq。com/s/i57wxhU21CXQOsEmIP4C7w