前言
前幾日,有朋友分享了這樣一個(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)用方法蛤育。
2宛官、反射調(diào)用
上面代碼就是通過(guò)反射來(lái)調(diào)用Controller
中的方法。
上面我們說(shuō):
不管方法是否為私有的瓦糕,
UserController
這個(gè)Bean是已經(jīng)確定被代理了的底洗。
在這里,this.getBean()
拿到的就是被代理后的對(duì)象咕娄。它長(zhǎng)這樣:
可以看到枷恕,在這個(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ì)象终息。
有朋友對(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):
這里只處理了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í)。