聽(tīng)說(shuō)SpringAOP 有坑尚卫?那就來(lái)踩一踩

前言

前幾日,有朋友分享了這樣一個(gè)案例:

原來(lái)的項(xiàng)目一直都正常運(yùn)行尸红,突然有一天發(fā)現(xiàn)代碼部分功能報(bào)錯(cuò)吱涉。經(jīng)過(guò)排查,發(fā)現(xiàn)Controller里部分方法為private的外里,原來(lái)是同事為Controller添加了AOP日志功能怎爵,導(dǎo)致原來(lái)的方法報(bào)錯(cuò)。

當(dāng)然了盅蝗,解決方案就是把private修飾的方法改為public鳖链,一切就都正常了。

不過(guò)這究竟是為什么呢墩莫?如果你也說(shuō)不太清楚芙委,就跟著筆者一起來(lái)探探究竟。

一狂秦、SpringBoot添加AOP

我們先為SpringBoot項(xiàng)目添加一個(gè)切面功能灌侣。

在這里,筆者的SpringBoot的版本為2.1.5.RELEASE裂问,對(duì)應(yīng)的Spring版本為5.1.7.RELEASE顶瞳。

我們必須要先添加AOP的依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后來(lái)定義一個(gè)切面,來(lái)攔截Controller中的所有方法:

@Component
@Aspect
public class ControllerAspect {

    @Pointcut(value = "execution(* com.viewscenes.controller..*.*(..))")
    public void pointcut(){}

    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        System.out.println("前置通知");
    }
    @After("pointcut()")
    public void after(JoinPoint joinPoint){
        System.out.println("后置通知");
    }
    @AfterReturning(pointcut="pointcut()",returning = "result")
    public void result(JoinPoint joinPoint,Object result){
        System.out.println("返回通知:"+result);
    }
}

然后寫一個(gè)Controller:

@RestController
public class UserController {

    @Autowired
    UserService userService;
    
    @RequestMapping("/list")
    public List<User> list() {
        return userService.list();
    }
}

好了愕秫,現(xiàn)在訪問(wèn)/list方法慨菱,AOP就已經(jīng)正常工作了。

前置通知
后置通知
返回通知:
[
User(id=59ffbdca-6b50-4466-936d-dddd693aa96b, name=0), 
User(id=ff600c29-2013-493a-aab1-e66329251666, name=1), 
User(id=85527844-bb3d-4cd3-98a1-786f0f754a98, name=2)
]

二戴甩、CGLIB原理

首先符喝,我們要知道的是,在SpringBoot中甜孤,默認(rèn)使用的就是CGLIB方式來(lái)創(chuàng)建代理协饲。

在它的配置文件中畏腕,spring.aop.proxy-target-class默認(rèn)是true。

{
  "name": "spring.aop.proxy-target-class",
  "type": "java.lang.Boolean",
  "description": "Whether subclass-based (CGLIB) proxies are to be created (true), 
    as opposed to standard Java interface-based proxies (false).",
  "defaultValue": true
}

然后再回顧下CGLIB的原理:

動(dòng)態(tài)生成一個(gè)要代理類的子類茉稠,子類重寫要代理的類的所有不是final的方法描馅。在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,順勢(shì)織入橫切邏輯而线。它比使用java反射的JDK動(dòng)態(tài)代理要快铭污。

我們看到,CGLIB代理的重要條件是生成一個(gè)子類膀篮,然后重寫要代理類的方法嘹狞。

下面我們看看CGLIB最基礎(chǔ)的應(yīng)用。

假如我們有一個(gè)Student類誓竿,它有一個(gè)eat()方法磅网。

public class Student {

    public void eat(String name) {
        System.out.println(name+"正在吃飯...");
    }
}

然后,創(chuàng)建一個(gè)攔截器筷屡,在CGLIB中涧偷,它是一個(gè)回調(diào)函數(shù)。

public class TargetInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, 
        Object[] params, MethodProxy proxy) throws Throwable {
        System.out.println("調(diào)用前");
        Object result = proxy.invokeSuper(obj, params);
        System.out.println("調(diào)用后");
        return result;
    }
}

然后我們測(cè)試它:

public static void main(String[] args){

    //創(chuàng)建字節(jié)碼增強(qiáng)器
    Enhancer enhancer =new Enhancer();
    //設(shè)置父類
    enhancer.setSuperclass(Student.class);
    //設(shè)置回調(diào)函數(shù)
    enhancer.setCallback(new TargetInterceptor());
    //創(chuàng)建代理類
    Student student=(Student)enhancer.create();
    student.eat("王二桿子");
}

這樣就完成了通過(guò)CGLIB對(duì)Student類的代理毙死。

上面代碼中的Student就是通過(guò)CGLIB創(chuàng)建的代理類嫂丙,它的Class對(duì)象如下:

class com.viewscenes.test.Student$$EnhancerByCGLIB$$121a496f

既然CGLIB是通過(guò)生成子類的方式來(lái)創(chuàng)建代理,那么它生成的子類就要繼承父類咯规哲。

關(guān)于Java中的繼承,有一條很重要的特性就是:

  • 子類擁有父類非 private 的屬性诽表、方法唉锌。

看到這里,也許你已經(jīng)明白了一大半竿奏,不過(guò)咱們繼續(xù)看袄简。如果照這樣說(shuō)法,如果父類中有private方法泛啸,生成的代理類中是看不到的绿语。

上面的Student類中,學(xué)生不僅要吃飯候址,也許還會(huì)偷偷睡覺(jué)吕粹,那我們給它加一個(gè)私有方法:

public class Student {

    public void eat(String name) {
        System.out.println(name+"正在吃飯...");
    }
    private void sleep(String name){
        System.out.println(name+"正在偷偷睡覺(jué)...");
    }
}

不過(guò),怎么測(cè)試呢岗仑?這私有方法在外面也調(diào)用不到呀匹耕。沒(méi)關(guān)系,我們用反射來(lái)試驗(yàn):

//創(chuàng)建代理類
Student student=(Student)enhancer.create();
    
Method eat = student.getClass().getMethod("eat", String.class);
eat.invoke(student,"王二桿子");

Method sleep = student.getClass().getMethod("sleep", String.class);
sleep.invoke(student,"王二桿子");

輸出結(jié)果如下:

調(diào)用前
王二桿子正在吃飯...
調(diào)用后
Exception in thread "main" java.lang.NoSuchMethodException: com.viewscenes.test.Student$$EnhancerByCGLIB$$121a496f.sleep(java.lang.String)
    at java.lang.Class.getMethod(Class.java:1786)
    at com.viewscenes.test.Test.main(Test.java:23)

很明顯荠雕,在調(diào)用sleep方法的時(shí)候稳其,拋出了java.lang.NoSuchMethodException異常驶赏。

至此,我們更加確定了一件事:

CGLIB創(chuàng)建的代理類既鞠,不會(huì)包含父類中的私有方法煤傍。

三、為啥其他屬性無(wú)法注入

我們看完了上面的測(cè)試嘱蛋,現(xiàn)在把Controller中的方法也改成private蚯姆。

再訪問(wèn)的時(shí)候,會(huì)報(bào)出java.lang.NullPointerException異常浑槽,是因?yàn)?code>UserService為null蒋失,沒(méi)有成功注入。

這就不太對(duì)了呀桐玻?如果說(shuō)因?yàn)樗接蟹椒ǖ脑蚋萃欤瑢?dǎo)致代理類不會(huì)包含此方法的話,那么最多AOP不會(huì)生效镊靴,為什么UserService也沒(méi)有注入進(jìn)來(lái)呢铣卡?

帶著這個(gè)問(wèn)題,筆者又翻了翻Spring aop相關(guān)的源碼偏竟,這才理解咋回事煮落。

在這里,我們首先要記住一件事:不管方法是否為私有的踊谋,UserController這個(gè)Bean是已經(jīng)確定被代理了的蝉仇。

1、SpringMVC處理請(qǐng)求

我們的一個(gè)HTTP請(qǐng)求殖蚕,會(huì)先經(jīng)過(guò)SpringMVC中的DispatcherServlet轿衔,然后找到與之對(duì)應(yīng)的HandlerMethod來(lái)處理。在后面睦疫,會(huì)先通過(guò)Spring的參數(shù)解析器害驹,把Request參數(shù)解析出來(lái),最后通過(guò)Method來(lái)調(diào)用方法蛤育。

image

2宛官、反射調(diào)用

image

上面代碼就是通過(guò)反射來(lái)調(diào)用Controller中的方法。

上面我們說(shuō):

不管方法是否為私有的瓦糕,UserController這個(gè)Bean是已經(jīng)確定被代理了的底洗。

在這里,this.getBean()拿到的就是被代理后的對(duì)象咕娄。它長(zhǎng)這樣:

image

可以看到枷恕,在這個(gè)代理對(duì)象中,userService對(duì)象為NULL谭胚。那么徐块,按理說(shuō)未玻,不管你方法是否為私有的,這樣直接調(diào)用也都是要報(bào)空指針異常的呀胡控。那么扳剿,為啥只有私有方法才會(huì)報(bào)錯(cuò),而公共方法不會(huì)呢昼激?

3庇绽、有啥不一樣

在這里,他們的method是一樣的橙困,都是java.lang.reflect包中的對(duì)象瞧掺。

如果是私有方法,那么在代理類中凡傅,不會(huì)包含這個(gè)方法辟狈。此時(shí)通過(guò)Method.invoke()來(lái)調(diào)用目標(biāo)方法,傳入的實(shí)例對(duì)象是userController的代理類夏跷,而這個(gè)代理類中的userService為NULL哼转,所以,執(zhí)行的時(shí)候槽华,才會(huì)看到userService沒(méi)有注入壹蔓,導(dǎo)致空指針異常。

如果是公共方法猫态,在代理類中佣蓉,就有它的子類實(shí)現(xiàn),則會(huì)先調(diào)用到代理類的攔截器MethodInterceptor亲雪。攔截器負(fù)責(zé)鏈?zhǔn)秸{(diào)用AOP方法和目標(biāo)方法勇凭。在攔截器執(zhí)行過(guò)程中,又調(diào)用了方法匆光。但不同的是,此時(shí)傳入的實(shí)例對(duì)象并不是代理類酿联,而是代理類的目標(biāo)對(duì)象终息。

image

有朋友對(duì)這塊不理解,其實(shí)就是JDK中java.lang.reflect.Method的內(nèi)容贞让,來(lái)借助測(cè)試再看一下周崭。

還是拿上面的Student為例,我們通過(guò)Method來(lái)獲取它的方法并調(diào)用喳张。

//創(chuàng)建代理類
Student student=(Student)enhancer.create();

Method eat = Student.class.getDeclaredMethod("eat", String.class);
eat.setAccessible(true);
eat.invoke(student,"王二桿子");

System.out.println("----------------------");
Method sleep = Student.class.getDeclaredMethod("sleep", String.class);
sleep.setAccessible(true);
sleep.invoke(student,"王二桿子");

上面的代碼中续镇,先通過(guò)反射拿到Method對(duì)象,其中eat是公共方法销部,sleep是私有方法摸航。invoke傳入的對(duì)象都是通過(guò)CGLIB生成的代理對(duì)象制跟,結(jié)果就是eat執(zhí)行了代理,而sleep并沒(méi)有酱虎。

調(diào)用前
王二桿子正在吃飯...
調(diào)用后
----------------------
王二桿子正在偷偷睡覺(jué)...

這也就解釋了雨膨,為啥同樣是調(diào)用method.invoke(),私有方法沒(méi)有注入成功读串,而公共方法正常聊记。

四、JDK代理

既然說(shuō)恢暖,CGLIB是通過(guò)繼承的方式實(shí)現(xiàn)代理排监。那私有方法能不能通過(guò)JDK動(dòng)態(tài)代理的方式來(lái)呢?

不瞞各位杰捂,筆者當(dāng)時(shí)確實(shí)想到了這個(gè)舆床,不過(guò)馬上被右腦打臉。JDK動(dòng)態(tài)代理是通過(guò)接口來(lái)的琼娘,接口里怎么可能有私有方法峭弟?

哈哈,看來(lái)此路不通脱拼。不過(guò)筆者卻發(fā)現(xiàn)了另外一個(gè)有意思的現(xiàn)象瞒瘸。

至此,我們不再討論公有私有方法的問(wèn)題熄浓,僅僅看Controller是否可以改為JDK動(dòng)態(tài)代理的方式情臭。

1、改為jdk動(dòng)態(tài)代理

首先赌蔑,我們需要在配置文件中俯在,設(shè)置spring.aop.proxy-target-class=false

然后還需要搞一個(gè)接口,這個(gè)接口還必須包含一個(gè)方法娃惯。否則Spring在生成代理的時(shí)候跷乐,還會(huì)判斷,如果不包含這些條件趾浅,還會(huì)是CGLIB的代理方式愕提。

public interface BaseController {
    default void print(){
        System.out.println("-------------");
    }
}

然后讓我們的Controller實(shí)現(xiàn)這個(gè)接口就行了。現(xiàn)在代理方式就變成了JDK動(dòng)態(tài)代理皿哨。

ok浅侨,現(xiàn)在訪問(wèn)/list,你會(huì)得到一個(gè)友好的404提示:

{
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/list"
}

2证膨、為何404如输?

這是為啥捏?

SpringMVC初始化的時(shí)候,會(huì)先遍歷所有的Bean不见,過(guò)濾包含Controller注解和RequestMapping注解的類澳化,然后查找類上的方法,獲取方法上的URL脖祈。最后把URL和方法的映射注冊(cè)到容器肆捕。

如果你對(duì)這一過(guò)程不理解,可以參閱筆者文章 - Spring源碼分析(四)SpringMVC初始化

在過(guò)濾的時(shí)候盖高,大概有三個(gè)條件:

  • 對(duì)象本身是否包含Controller相關(guān)注解
  • 對(duì)象的父類是否包含Controller相關(guān)注解
  • 對(duì)象的接口是否包含Controller相關(guān)注解

此時(shí)我們的userController是一個(gè)JDK的代理對(duì)象慎陵,這三條件都不滿足呀,所以Spring認(rèn)為它并不是一個(gè)Controller喻奥。

因此席纽,我們需要在它接口BaseController上添加一個(gè)@RestController注解才行。

加完之后撞蚕,過(guò)濾條件滿足了润梯。SpringMVC終于認(rèn)識(shí)它是一個(gè)Controller了。不過(guò)甥厦,如果你現(xiàn)在去訪問(wèn)纺铭,還會(huì)得到一個(gè)404。

3刀疙、為何還是404舶赔?

筆者當(dāng)時(shí)也是崩潰的,為啥還是404呢谦秧?

if (beanType != null && this.isHandler(beanType)) {
    this.detectHandlerMethods(beanName);
}

原來(lái)通過(guò)isHandler條件判斷之后竟纳,還需要通過(guò)detectHandlerMethods檢測(cè)bean上的方法,注冊(cè)u(píng)rl和對(duì)象method的映射關(guān)系疚鲤。

但是這里有個(gè)坑~

我們知道锥累,不管是JDK動(dòng)態(tài)代理還是CGLIB動(dòng)態(tài)代理,此時(shí)的bean都是代理對(duì)象集歇。檢測(cè)bean上的方法桶略,一定得檢測(cè)真實(shí)的目標(biāo)對(duì)象才有意義。

Spring也正是這樣做的诲宇,它通過(guò)ClassUtils.getUserClass(handlerType);來(lái)獲取真實(shí)對(duì)象际歼。

然后看到這段代碼的時(shí)候,才發(fā)現(xiàn):

image

這里只處理了CGLIB代理的情況焕窝。蹬挺。換言之维贺,如果是JDK的代理對(duì)象它掂,這里返回的還是代理對(duì)象。

那么在外層,拿著這個(gè)代理對(duì)象去selectMethods查找方法虐秋,當(dāng)然一無(wú)所獲榕茧。最后的結(jié)果就是,沒(méi)有把這個(gè)url和對(duì)象method映射起來(lái)客给,當(dāng)我們?cè)L問(wèn)/list的時(shí)候用押,會(huì)報(bào)出404。

這里的SpringMVC版本為5.1.7.RELEASE靶剑,不知道其他版本是不是也是這樣處理的蜻拨。歡迎探討~

總結(jié)

以前老聽(tīng)一些人說(shuō),在Controller里面不要用私有方法桩引,也知道可能會(huì)產(chǎn)生問(wèn)題缎讼。

但具體會(huì)產(chǎn)生哪些問(wèn)題?產(chǎn)生問(wèn)題的根源在哪里坑匠?卻一直很朦朧血崭,通過(guò)本文也許你對(duì)這個(gè)問(wèn)題就有了更新的認(rèn)識(shí)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厘灼,一起剝皮案震驚了整個(gè)濱河市夹纫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌设凹,老刑警劉巖舰讹,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異围来,居然都是意外死亡跺涤,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門监透,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)桶错,“玉大人,你說(shuō)我怎么就攤上這事胀蛮≡旱螅” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵粪狼,是天一觀的道長(zhǎng)退腥。 經(jīng)常有香客問(wèn)我,道長(zhǎng)再榄,這世上最難降的妖魔是什么狡刘? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮困鸥,結(jié)果婚禮上嗅蔬,老公的妹妹穿的比我還像新娘剑按。我一直安慰自己,他們只是感情好澜术,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布艺蝴。 她就那樣靜靜地躺著,像睡著了一般鸟废。 火紅的嫁衣襯著肌膚如雪猜敢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天盒延,我揣著相機(jī)與錄音缩擂,去河邊找鬼。 笑死添寺,一個(gè)胖子當(dāng)著我的面吹牛撇叁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播畦贸,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼陨闹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了薄坏?” 一聲冷哼從身側(cè)響起趋厉,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胶坠,沒(méi)想到半個(gè)月后君账,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沈善,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年乡数,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闻牡。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡净赴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出罩润,到底是詐尸還是另有隱情玖翅,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布割以,位于F島的核電站金度,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏严沥。R本人自食惡果不足惜猜极,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望消玄。 院中可真熱鬧跟伏,春花似錦扎筒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)奥溺。三九已至辞色,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浮定,已是汗流浹背相满。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桦卒,地道東北人立美。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像方灾,于是被迫代替她去往敵國(guó)和親建蹄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • IOC和DI是什么裕偿? Spring IOC 的理解洞慎,其初始化過(guò)程? BeanFactory 和 FactoryBe...
    justlpf閱讀 3,477評(píng)論 1 21
  • 前言: 正文之前嘿棘,容我小小的矯情一下劲腿。我知道每個(gè)人的生活有很多意外、有很多迷茫鸟妙、但是“我相信”一件事堅(jiān)持下去焦人,就不...
    java小瓜哥閱讀 2,623評(píng)論 0 0
  • Java設(shè)計(jì)模式——代理模式 代理模式主要分為接口,委托類重父,代理類 接口:規(guī)定具體方法委托類:實(shí)現(xiàn)接口花椭,完成具體的...
    vczyh閱讀 665評(píng)論 0 0
  • Spring之IoC IoC注入之DI 1.什么是DI? 依賴注入:Depend...
    袁小勝閱讀 448評(píng)論 0 0
  • 打碎 挖掘沉睡的黏土 拾荒野殘跡 朦朧 在當(dāng)代的疲倦中 嘔吐后狂奔 在諷刺中 留下最真實(shí)的記號(hào) 不周山下 獨(dú)看 云凋落
    北郊PM2丶5閱讀 230評(píng)論 1 6