javascript
SpringBoot的AOP是默认开启的,不需要加注解@EnableAspectJAutoProxy____听说SpringAOP 有坑?那就来踩一踩
另外SpringBoot默認是cglib動態(tài)代理,開啟jdk代碼需要修改配置
spring:aop:proxy-target-class: false以上基于2.2.10版本,至于設置spring.aop.proxy-target-class=false 用aop對controller代理竟然導致接口404 ,復現(xiàn)這個問題controller必須實現(xiàn)接口(有些項目會定義一個BaseController接口),沒實現(xiàn)接口的controller不會有這個問題。spring.aop.proxy-target-class=false 會讓實現(xiàn)了接口類在被代理時使用jdk代理,設置為true時,統(tǒng)一使用cglib代理。
文章摘錄:404的原因是JDK生成的代理類沒有被代理的controller的類注解 @Controller以及方法上的@GetMapping,所以springmvc在掃描bean無法為controller建立起請求地址和 controller方法的映射關系。
jdk動態(tài)代理
這里判斷 是否需要生成映射關系就是看你 類上有沒有@Controller和@RequestMapping 注解。
JDK生成的代理對象 的 類型 沒有 這兩個注解 所以 不需要處理。
cglib
cglib采用繼承被代理類的方式, 是可以溯源 到 父類(被代理類) 去找 注解@Controller 注解的。所以這里 生成接口映射 是 正常運行的。
聽說SpringAOP 有坑?那就來踩一踩
前言
前幾日,有朋友分享了這樣一個案例:
原來的項目一直都正常運行,突然有一天發(fā)現(xiàn)代碼部分功能報錯。經(jīng)過排查,發(fā)現(xiàn)Controller里部分方法為private的,原來是同事為Controller添加了AOP日志功能,導致原來的方法報錯。
當然了,解決方案就是把private修飾的方法改為public,一切就都正常了。
不過這究竟是為什么呢?如果你也說不太清楚,就跟著筆者一起來探探究竟。
一、SpringBoot添加AOP
我們先為SpringBoot項目添加一個切面功能。
在這里,筆者的SpringBoot的版本為2.1.5.RELEASE,對應的Spring版本為5.1.7.RELEASE。
我們必須要先添加AOP的依賴:
<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:
@RestController public class UserController {@AutowiredUserService userService;@RequestMapping("/list")public List<User> list() {return userService.list();} }好了,現(xià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中,默認使用的就是CGLIB方式來創(chuàng)建代理。
在它的配置文件中,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的原理:
動態(tài)生成一個要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中采用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯。它比使用java反射的JDK動態(tài)代理要快。
我們看到,CGLIB代理的重要條件是生成一個子類,然后重寫要代理類的方法。
下面我們看看CGLIB最基礎的應用。
假如我們有一個Student類,它有一個eat()方法。
public class Student {public void eat(String name) {System.out.println(name+"正在吃飯...");} }然后,創(chuàng)建一個攔截器,在CGLIB中,它是一個回調函數(shù)。
public class TargetInterceptor implements MethodInterceptor {@Overridepublic 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){//創(chuàng)建字節(jié)碼增強器Enhancer enhancer =new Enhancer();//設置父類enhancer.setSuperclass(Student.class);//設置回調函數(shù)enhancer.setCallback(new TargetInterceptor());//創(chuàng)建代理類Student student=(Student)enhancer.create();student.eat("王二桿子"); }這樣就完成了通過CGLIB對Student類的代理。
上面代碼中的Student就是通過CGLIB創(chuàng)建的代理類,它的Class對象如下:
class com.viewscenes.test.Student$$EnhancerByCGLIB$$121a496f既然CGLIB是通過生成子類的方式來創(chuàng)建代理,那么它生成的子類就要繼承父類咯。
關于Java中的繼承,有一條很重要的特性就是:
- 子類擁有父類非 private 的屬性、方法。
看到這里,也許你已經(jīng)明白了一大半,不過咱們繼續(xù)看。如果照這樣說法,如果父類中有private方法,生成的代理類中是看不到的。
上面的Student類中,學生不僅要吃飯,也許還會偷偷睡覺,那我們給它加一個私有方法:
public class Student {public void eat(String name) {System.out.println(name+"正在吃飯...");}private void sleep(String name){System.out.println(name+"正在偷偷睡覺...");} }不過,怎么測試呢?這私有方法在外面也調用不到呀。沒關系,我們用反射來試驗:
//創(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,"王二桿子");輸出結果如下:
調用前 王二桿子正在吃飯... 調用后 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創(chuàng)建的代理類,不會包含父類中的私有方法。
三、為啥其他屬性無法注入
我們看完了上面的測試,現(xiàn)在把Controller中的方法也改成private。
再訪問的時候,會報出java.lang.NullPointerException異常,是因為UserService為null,沒有成功注入。
這就不太對了呀?如果說因為私有方法的原因,導致代理類不會包含此方法的話,那么最多AOP不會生效,為什么UserService也沒有注入進來呢?
帶著這個問題,筆者又翻了翻Spring aop相關的源碼,這才理解咋回事。
在這里,我們首先要記住一件事:不管方法是否為私有的,UserController這個Bean是已經(jīng)確定被代理了的。
1、SpringMVC處理請求
我們的一個HTTP請求,會先經(jīng)過SpringMVC中的DispatcherServlet,然后找到與之對應的HandlerMethod來處理。在后面,會先通過Spring的參數(shù)解析器,把Request參數(shù)解析出來,最后通過Method來調用方法。
2、反射調用
上面代碼就是通過反射來調用Controller中的方法。
上面我們說:
不管方法是否為私有的,UserController這個Bean是已經(jīng)確定被代理了的。
在這里,this.getBean()拿到的就是被代理后的對象。它長這樣:
可以看到,在這個代理對象中,userService對象為NULL。那么,按理說,不管你方法是否為私有的,這樣直接調用也都是要報空指針異常的呀。那么,為啥只有私有方法才會報錯,而公共方法不會呢?
3、有啥不一樣
在這里,他們的method是一樣的,都是java.lang.reflect包中的對象。
如果是私有方法,那么在代理類中,不會包含這個方法。此時通過Method.invoke()來調用目標方法,傳入的實例對象是userController的代理類,而這個代理類中的userService為NULL,所以,執(zhí)行的時候,才會看到userService沒有注入,導致空指針異常。
如果是公共方法,在代理類中,就有它的子類實現(xiàn),則會先調用到代理類的攔截器MethodInterceptor。攔截器負責鏈式調用AOP方法和目標方法。在攔截器執(zhí)行過程中,又調用了方法。但不同的是,此時傳入的實例對象并不是代理類,而是代理類的目標對象。
有朋友對這塊不理解,其實就是JDK中java.lang.reflect.Method的內容,來借助測試再看一下。
還是拿上面的Student為例,我們通過Method來獲取它的方法并調用。
//創(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,"王二桿子");上面的代碼中,先通過反射拿到Method對象,其中eat是公共方法,sleep是私有方法。invoke傳入的對象都是通過CGLIB生成的代理對象,結果就是eat執(zhí)行了代理,而sleep并沒有。
調用前 王二桿子正在吃飯... 調用后 ---------------------- 王二桿子正在偷偷睡覺...這也就解釋了,為啥同樣是調用method.invoke(),私有方法沒有注入成功,而公共方法正常。
四、JDK代理
既然說,CGLIB是通過繼承的方式實現(xiàn)代理。那私有方法能不能通過JDK動態(tài)代理的方式來呢?
不瞞各位,筆者當時確實想到了這個,不過馬上被右腦打臉。JDK動態(tài)代理是通過接口來的,接口里怎么可能有私有方法?
哈哈,看來此路不通。不過筆者卻發(fā)現(xiàn)了另外一個有意思的現(xiàn)象。
至此,我們不再討論公有私有方法的問題,僅僅看Controller是否可以改為JDK動態(tài)代理的方式。
1、改為jdk動態(tài)代理
首先,我們需要在配置文件中,設置spring.aop.proxy-target-class=false
然后還需要搞一個接口,這個接口還必須包含一個方法。否則Spring在生成代理的時候,還會判斷,如果不包含這些條件,還會是CGLIB的代理方式。
public interface BaseController {default void print(){System.out.println("-------------");} }然后讓我們的Controller實現(xiàn)這個接口就行了。現(xiàn)在代理方式就變成了JDK動態(tài)代理。
ok,現(xiàn)在訪問/list,你會得到一個友好的404提示:
{"status": 404,"error": "Not Found","message": "No message available","path": "/list" }2、為何404?
這是為啥捏?
在SpringMVC初始化的時候,會先遍歷所有的Bean,過濾包含Controller注解和RequestMapping注解的類,然后查找類上的方法,獲取方法上的URL。最后把URL和方法的映射注冊到容器。
如果你對這一過程不理解,可以參閱筆者文章 - Spring源碼分析(四)SpringMVC初始化
在過濾的時候,大概有三個條件:
- 對象本身是否包含Controller相關注解
- 對象的父類是否包含Controller相關注解
- 對象的接口是否包含Controller相關注解
此時我們的userController是一個JDK的代理對象,這三條件都不滿足呀,所以Spring認為它并不是一個Controller。
因此,我們需要在它接口BaseController上添加一個@RestController注解才行。
加完之后,過濾條件滿足了。SpringMVC終于認識它是一個Controller了。不過,如果你現(xiàn)在去訪問,還會得到一個404。
3、為何還是404?
筆者當時也是崩潰的,為啥還是404呢?
if (beanType != null && this.isHandler(beanType)) {this.detectHandlerMethods(beanName); }原來通過isHandler條件判斷之后,還需要通過detectHandlerMethods檢測bean上的方法,注冊url和對象method的映射關系。
但是這里有個坑~
我們知道,不管是JDK動態(tài)代理還是CGLIB動態(tài)代理,此時的bean都是代理對象。檢測bean上的方法,一定得檢測真實的目標對象才有意義。
Spring也正是這樣做的,它通過ClassUtils.getUserClass(handlerType);來獲取真實對象。
然后看到這段代碼的時候,才發(fā)現(xiàn):
這里只處理了CGLIB代理的情況。。換言之,如果是JDK的代理對象,這里返回的還是代理對象。
那么在外層,拿著這個代理對象去selectMethods查找方法,當然一無所獲。最后的結果就是,沒有把這個url和對象method映射起來,當我們訪問/list的時候,會報出404。
這里的SpringMVC版本為5.1.7.RELEASE,不知道其他版本是不是也是這樣處理的。歡迎探討~
總結
以前老聽一些人說,在Controller里面不要用私有方法,也知道可能會產(chǎn)生問題。
但具體會產(chǎn)生哪些問題?產(chǎn)生問題的根源在哪里?卻一直很朦朧,通過本文也許你對這個問題就有了更新的認識。
作者:清幽之地
原文鏈接:https://juejin.im/post/5d01e088f265da1b7f2978c3
總結
以上是生活随笔為你收集整理的SpringBoot的AOP是默认开启的,不需要加注解@EnableAspectJAutoProxy____听说SpringAOP 有坑?那就来踩一踩的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 编译器原理笔录(2)-符号表示法的形式化
- 下一篇: 工业用微型计算机笔记(14)-指令系统(