1. 程式人生 > >Spring AOP原始碼分析--代理方式的選擇

Spring AOP原始碼分析--代理方式的選擇

能堅持別人不能堅持的,才能擁有別人未曾擁有的。
關注程式設計大道公眾號,讓我們一同堅持心中所想,一起成長!!

 

年前寫了一個面試突擊系列的文章,目前只有redis相關的。在這個系列裡,我整理了一些面試題與大家分享,幫助年後和我一樣想要在金三銀四準備跳槽的同學。我們一起鞏固、突擊面試官常問的一些面試題,加油!!

《【面試突擊】— Redis篇》--Redis資料型別?適用於哪些場景?
《【面試突擊】— Redis篇》--Redis的執行緒模型瞭解嗎?為啥單執行緒效率還這麼高?
《【面試突擊】— Redis篇》-- Redis的主從複製?哨兵機制?
《【面試突擊】— Redis篇》-- Redis哨兵原理及持久化機制

《【面試突擊】— Redis篇》--Redis Cluster及快取使用和架構設計的常見問題

什麼是 AOP ?

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期間動態代理實現程式功能的統一維護的一種技術。

AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

Spring AOP面向切面程式設計

介面呼叫耗時

現在我們有個介面要在日誌中記錄介面耗時,我們會怎麼做呢?一般我們會在介面開始和介面結束時獲取系統時間,然後二者一減就是介面耗時時間了。如下,在20行我們打印出介面耗時。

 1@RestController
2@Slf4j
3public class LoginController {
4    @Autowired
5    LoginService loginService;
6    @RequestMapping("/login/{id}")
7    public Map<String,Object> login(@PathVariable("id") Integer id){
8        long start = System.currentTimeMillis();
9        Map<String,Object> result = new HashMap<>();
10        result.put("status","0");
11        result.put("msg" , "失敗");
12        if (loginService.login(id)) {
13            result.put("status","1");
14            result.put("msg" , "成功");
15        }
16        long end = System.currentTimeMillis();
17        log.info("耗時=>{}ms",end-start);
18        return result;
19    }
20}

啟動類:

1@SpringBootApplication
2public class SpringaopSbApplication {
3    public static void main(String[] args) {
4        SpringApplication.run(SpringaopSbApplication.class, args);
5    }
6}

但是,如果所有介面都要記錄耗時時間呢?我們還按這種方式嗎?顯然不行,這種要在每個介面都加上同樣的程式碼,而且如果後期你老闆說去掉的話,你還有一個個的刪掉麼?簡直是不可想象。。
所以對於這種需求,其實是可以提煉出來的。我們想,統計介面的耗時時間,無非就是在介面的執行前後記錄一下時然後相減打印出來即可,然後在這樣的地方去加入我們提煉出來的公共的程式碼。這就好比在原來的業務程式碼的基礎上,把原來的程式碼橫切開來,在需要的地方加入公共的程式碼,對原來的業務程式碼起到功能增強的作用。
這就是AOP的作用。

Spring AOP應用場景 - 介面耗時記錄

下面我們來看看使用Spring AOP怎麼滿足這個需求。

首先定義一個切面類TimeMoitor,其中pointCut()方法(修飾一組連線點)是一個切點,@Pointcut定義了一組連線點(使用表示式匹配)
aroundTimeCounter()是要加入的功能,被@Around註解修飾,是一個環繞通知(Spring AOP通知的一種),其實就是上面說的在方法執行前後記錄時間然後相減再打印出來耗時時間。

 1@Aspect
2@Component
3@Slf4j
4public class TimeMoitor {
5    @Pointcut(value = "execution(* com.walking.springaopsb.controller.*.*(..))")
6    public void pointCut(){}
7
8    @Around(value = "com.walking.springaopsb.aop.TimeMoitor.pointCut()")
9    public Object aroundTimeCounter(ProceedingJoinPoint jpx){
10        long start = System.currentTimeMillis();
11        Object proceed = null;
12        try {
13             proceed = jpx.proceed();
14        } catch (Throwable throwable) {
15            throwable.printStackTrace();
16        }
17        long end = System.currentTimeMillis();
18        log.info("耗時=>{}ms",end-start);
19        return proceed;
20    }
21}

然後在LoginController#login方法裡我們就可以把日誌列印耗時時間的程式碼刪掉了。

 1@RestController
2@Slf4j
3public class LoginController {
4    @Autowired
5    LoginService loginService;
6    @RequestMapping("/login/{id}")
7    public Map<String,Object> login(@PathVariable("id") Integer id){
8        Map<String,Object> result = new HashMap<>();
9        result.put("status","0");
10        result.put("msg" , "失敗");
11        if (loginService.login(id)) {
12            result.put("status","1");
13            result.put("msg" , "成功");
14        }
15        return result;
16    }
17}

再比如,LoginController裡若是還有別的方法,也一樣可以應用到。
使用Spring AOP的控制檯日誌:

Spring AOP的原理

以上就是Spring AOP的一個應用場景。那Spring AOP的原理是什麼呢,用的什麼技術呢?
其實就是反射+動態代理。代理用的就是JDK動態代理或cglib,那麼Spring AOP什麼時候用JDK動態代理什麼時候用cglib?預設使用哪種?

原始碼分析

那麼我們就通過原始碼來看一下吧。首先我們將啟動類改一下,方便我們對原始碼debug。

啟動類:

1@ComponentScan("com.walking.springaopsb.*")
2@EnableAspectJAutoProxy
3public class SpringaopSbApplication {
4    public static void main(String[] args) {
5        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringaopSbApplication.class);
6        LoginController loginController = (LoginController) applicationContext.getBean("loginController");
7        loginController.login(123);
8    }
9}

我們修改了一下啟動類,把斷點打在第6行,啟動,往下走一步,看loginController這個變數。

我們發現是cglib方式產生的代理類,說明從IoC容器裡拿到的是代理類,到底是初始化IoC容器時生成的還是獲取時產生的呢?我們也跟隨原始碼來看一下吧。

要知道的是,我們現在要看的是第5行還是第6行生成的代理類。先看第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<String, Object> singletonObjects = new ConcurrentHashMap(256);
所以到這裡我們就可以知道,這個代理類不是在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動態代理。

為什麼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動態代理只能是介面。

 

總結

​通過以上的原始碼分析我們弄清楚了,Spring AOP使用的代理機制了,並且是沒有預設的代理,不是JDK動態代理就是cglib,以及為啥java的動態代理只能是介面。並且我們還看了一下spring的原始碼,雖然看的不是非常的仔細,但是通過這樣看原始碼我們的理解更加的加深了,也鍛鍊了看原始碼的能力。