前幾日,有朋友分享了這樣一個案例:java
原來的項目一直都正常運行,忽然有一天發現代碼部分功能報錯。通過排查,發現
Controller
裏部分方法爲private
的,原來是同事爲Controller
添加了AOP日誌功能,致使原來的方法報錯。spring
固然了,解決方案就是把private
修飾的方法改成public
,一切就都正常了。bash
不過這到底是爲何呢?若是你也說不太清楚,就跟着筆者一塊兒來探探究竟。app
咱們先爲SpringBoot項目添加一個切面功能。ide
在這裏,筆者的SpringBoot的版本爲2.1.5.RELEASE
,對應的Spring版本爲5.1.7.RELEASE
。函數
咱們必需要先添加AOP的依賴:spring-boot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製代碼
而後來定義一個切面,來攔截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);
}
}
複製代碼
而後寫一個Controller
:post
@RestController
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/list")
public List<User> list() {
return userService.list();
}
}
複製代碼
好了,如今訪問/list
方法,AOP就已經正常工做了。測試
前置通知
後置通知
返回通知:
[
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)
]
複製代碼
首先,咱們要知道的是,在SpringBoot
中,默認使用的就是CGLIB
方式來建立代理。
在它的配置文件中,spring.aop.proxy-target-class
默認是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的原理:
動態生成一個要代理類的子類,子類重寫要代理的類的全部不是final的方法。在子類中採用方法攔截的技術攔截全部父類方法的調用,順勢織入橫切邏輯。它比使用java反射的JDK動態代理要快。
咱們看到,CGLIB代理的重要條件是生成一個子類,而後重寫要代理類的方法。
下面咱們看看CGLIB
最基礎的應用。
假如咱們有一個Student
類,它有一個eat()
方法。
public class Student {
public void eat(String name) {
System.out.println(name+"正在吃飯...");
}
}
複製代碼
而後,建立一個攔截器,在CGLIB
中,它是一個回調函數。
public class TargetInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method,
Object[] params, MethodProxy proxy) throws Throwable {
System.out.println("調用前");
Object result = proxy.invokeSuper(obj, params);
System.out.println("調用後");
return result;
}
}
複製代碼
而後咱們測試它:
public static void main(String[] args){
//建立字節碼加強器
Enhancer enhancer =new Enhancer();
//設置父類
enhancer.setSuperclass(Student.class);
//設置回調函數
enhancer.setCallback(new TargetInterceptor());
//建立代理類
Student student=(Student)enhancer.create();
student.eat("王二桿子");
}
複製代碼
這樣就完成了經過CGLIB對Student
類的代理。
上面代碼中的Student
就是經過CGLIB
建立的代理類,它的Class對象以下:
class com.viewscenes.test.Student$$EnhancerByCGLIB$$121a496f
既然CGLIB
是經過生成子類的方式來建立代理,那麼它生成的子類就要繼承父類咯。
關於Java中的繼承,有一條很重要的特性就是:
看到這裏,也許你已經明白了一大半,不過我們繼續看。若是照這樣說法,若是父類中有private
方法,生成的代理類中是看不到的。
上面的Student
類中,學生不只要吃飯,也許還會偷偷睡覺,那咱們給它加一個私有方法:
public class Student {
public void eat(String name) {
System.out.println(name+"正在吃飯...");
}
private void sleep(String name){
System.out.println(name+"正在偷偷睡覺...");
}
}
複製代碼
不過,怎麼測試呢?這私有方法在外面也調用不到呀。不要緊,咱們用反射來試驗:
//建立代理類
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,"王二桿子");
複製代碼
輸出結果以下:
調用前
王二桿子正在吃飯...
調用後
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)
複製代碼
很明顯,在調用sleep
方法的時候,拋出了java.lang.NoSuchMethodException
異常。
至此,咱們更加肯定了一件事:
由CGLIB
建立的代理類,不會包含父類中的私有方法。
咱們看完了上面的測試,如今把Controller
中的方法也改爲private
。
再訪問的時候,會報出java.lang.NullPointerException
異常,是由於UserService爲null
,沒有成功注入。
這就不太對了呀?若是說由於私有方法的緣由,致使代理類不會包含此方法的話,那麼最多AOP不會生效,爲何UserService
也沒有注入進來呢?
帶着這個問題,筆者又翻了翻Spring aop
相關的源碼,這才理解咋回事。
在這裏,咱們首先要記住一件事:無論方法是否爲私有的,UserController
這個Bean是已經肯定被代理了的。
咱們的一個HTTP請求,會先通過SpringMVC中的DispatcherServlet
,而後找到與之對應的HandlerMethod
來處理。在後面,會先經過Spring的參數解析器,把Request參數解析出來,最後經過Method
來調用方法。
上面代碼就是經過反射來調用Controller
中的方法。
上面咱們說:
無論方法是否爲私有的,
UserController
這個Bean是已經肯定被代理了的。
在這裏,this.getBean()
拿到的就是被代理後的對象。它長這樣:
能夠看到,在這個代理對象中,userService
對象爲NULL。那麼,按理說,無論你方法是否爲私有的,這樣直接調用也都是要報空指針異常的呀。那麼,爲啥只有私有方法纔會報錯,而公共方法不會呢?
在這裏,他們的method
是同樣的,都是java.lang.reflect
包中的對象。
若是是私有方法,那麼在代理類中,不會包含這個方法。此時經過Method.invoke()
來調用目標方法,傳入的實例對象是userController
的代理類,而這個代理類中的userService
爲NULL,因此,執行的時候,纔會看到userService
沒有注入,致使空指針異常。
若是是公共方法,在代理類中,就有它的子類實現,則會先調用到代理類的攔截器MethodInterceptor
。攔截器負責鏈式調用AOP方法和目標方法。在攔截器執行過程當中,又調用了方法。但不一樣的是,此時傳入的實例對象並非代理類,而是代理類的目標對象。
有朋友對這塊不理解,其實就是JDK中java.lang.reflect.Method
的內容,來藉助測試再看一下。
仍是拿上面的Student
爲例,咱們經過Method
來獲取它的方法並調用。
//建立代理類
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,"王二桿子");
複製代碼
上面的代碼中,先經過反射拿到Method
對象,其中eat是公共方法,sleep是私有方法。invoke傳入的對象都是經過CGLIB
生成的代理對象,結果就是eat執行了代理,而sleep並無。
調用前
王二桿子正在吃飯...
調用後
----------------------
王二桿子正在偷偷睡覺...
複製代碼
這也就解釋了,爲啥一樣是調用method.invoke()
,私有方法沒有注入成功,而公共方法正常。
既然說,CGLIB
是經過繼承的方式實現代理。那私有方法能不能經過JDK動態代理
的方式來呢?
不瞞各位,筆者當時確實想到了這個,不過立刻被右腦打臉。JDK動態代理是經過接口來的,接口裏怎麼可能有私有方法?
哈哈,看來此路不通。不過筆者卻發現了另一個有意思的現象。
至此,咱們再也不討論公有私有方法的問題,僅僅看Controller
是否能夠改成JDK動態代理
的方式。
首先,咱們須要在配置文件中,設置spring.aop.proxy-target-class=false
而後還須要搞一個接口,這個接口還必須包含一個方法。不然Spring
在生成代理的時候,還會判斷,若是不包含這些條件,還會是CGLIB
的代理方式。
public interface BaseController {
default void print(){
System.out.println("-------------");
}
}
複製代碼
而後讓咱們的Controller
實現這個接口就好了。如今代理方式就變成了JDK動態代理
。
ok,如今訪問/list
,你會獲得一個友好的404提示:
{
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/list"
}
複製代碼
這是爲啥捏?
在SpringMVC
初始化的時候,會先遍歷全部的Bean
,過濾包含Controller
註解和RequestMapping
註解的類,而後查找類上的方法,獲取方法上的URL。最後把URL和方法的映射註冊到容器。
若是你對這一過程不理解,能夠參閱筆者文章 - Spring源碼分析(四)SpringMVC初始化
在過濾的時候,大概有三個條件:
Controller
相關注解Controller
相關注解Controller
相關注解此時咱們的userController
是一個JDK的代理對象,這三條件都不知足呀,因此Spring認爲它並非一個Controller
。
所以,咱們須要在它接口BaseController
上添加一個@RestController
註解才行。
加完以後,過濾條件知足了。SpringMVC
終於認識它是一個Controller
了。不過,若是你如今去訪問,還會獲得一個404。
筆者當時也是崩潰的,爲啥仍是404呢?
if (beanType != null && this.isHandler(beanType)) {
this.detectHandlerMethods(beanName);
}
複製代碼
原來經過isHandler
條件判斷以後,還須要經過detectHandlerMethods
檢測bean上的方法,註冊url和對象method的映射關係。
可是這裏有個坑~
咱們知道,不論是JDK動態代理
仍是CGLIB動態代理
,此時的bean都是代理對象。檢測bean上的方法,必定得檢測真實的目標對象纔有意義。
Spring也正是這樣作的,它經過ClassUtils.getUserClass(handlerType);
來獲取真實對象。
而後看到這段代碼的時候,才發現:
這裏只處理了CGLIB
代理的狀況。。換言之,若是是JDK的代理對象,這裏返回的仍是代理對象。
那麼在外層,拿着這個代理對象去selectMethods
查找方法,固然一無所得。最後的結果就是,沒有把這個url和對象method映射起來,當咱們訪問/list
的時候,會報出404。
這裏的SpringMVC版本爲5.1.7.RELEASE
,不知道其餘版本是否是也是這樣處理的。歡迎探討~
之前老聽一些人說,在Controller裏面不要用私有方法,也知道可能會產生問題。
但具體會產生哪些問題?產生問題的根源在哪裏?卻一直很朦朧,經過本文也許你對這個問題就有了更新的認識。