Java中的SpringAOP、代理模式、常用AspectJ注解詳解
一、AOP簡述
回到主題,何為AOP?AOP即面向切面編程——Spring提供了面向切面編程的豐富支持,允許通過分離應(yīng)用的業(yè)務(wù)邏輯與系統(tǒng)級服務(wù)(例如審計(auditing)和事務(wù)(transaction)管理)進(jìn)行內(nèi)聚性的開發(fā)。
應(yīng)用對象只實現(xiàn)它們應(yīng)該做的——完成業(yè)務(wù)邏輯——僅此而已。
它們并不負(fù)責(zé)(甚至是意識)其它的系統(tǒng)級關(guān)注點,例如日志或事務(wù)支持。
如下圖,可以很直接明了的展示整個AOP的過程:
1.1 一些基本概念
通知(Adivce)
通知有5種類型:
我們可能會問,那通知對應(yīng)系統(tǒng)中的代碼是一個方法、對象、類、還是接口什么的呢?
我想說一點,其實都不是,你可以理解通知就是對應(yīng)我們?nèi)粘I钪兴f的通知,比如‘某某人,你2019年9月1號來學(xué)校報個到’,通知更多地體現(xiàn)一種告訴我們(告訴系統(tǒng)何)何時執(zhí)行,規(guī)定一個時間,在系統(tǒng)運行中的某個時間點(比如拋異常啦!方法執(zhí)行前啦!), 并非對應(yīng)代碼中的方法!并非對應(yīng)代碼中的方法!并非對應(yīng)代碼中的方法!
- Before 在方法被調(diào)用之前調(diào)用
- After 在方法完成后調(diào)用通知,無論方法是否執(zhí)行成功
- After-returning 在方法成功執(zhí)行之后調(diào)用通知
- After-throwing 在方法拋出異常后調(diào)用通知
- Around 通知了好、包含了被通知的方法,在被通知的方法調(diào)用之前后調(diào)用之后執(zhí)行自定義的行為
- 切點(Pointcut)
- 切點在Spring AOP中確實是對應(yīng)系統(tǒng)中的方法。但是這個方法是定義在切面中的方法,一般和通知一起使用,一起組成了切面。
- 連接點(Join point)
- 比如:方法調(diào)用、方法執(zhí)行、字段設(shè)置/獲取、異常處理執(zhí)行、類初始化、甚至是 for 循環(huán)中的某個點 理論上, 程序執(zhí)行過程中的任何時點都可以作為作為織入點, 而所有這些執(zhí)行時點都是 Joint point 但 Spring AOP 目前僅支持方法執(zhí)行 (method execution) 也可以這樣理解,連接點就是你準(zhǔn)備在系統(tǒng)中執(zhí)行切點和切入通知的地方(一般是一個方法,一個字段)
- 切面(Aspect)
- 切面是切點和通知的集合,一般單獨作為一個類。通知和切點共同定義了關(guān)于切面的全部內(nèi)容,它是什么時候,在何時和何處完成功能。
- 引入(Introduction)
- 引用允許我們向現(xiàn)有的類添加新的方法或者屬性
- 織入(Weaving)
- 組裝方面來創(chuàng)建一個被通知對象。這可以在編譯時完成(例如使用AspectJ編譯器),也可以在運行時完成。Spring和其他純Java AOP框架一樣,在運行時完成織入。
二、代理模式
首先AOP思想的實現(xiàn)一般都是基于代理模式,在JAVA中一般采用JDK動態(tài)代理模式,但是我們都知道,JDK動態(tài)代理模式只能代理接口,如果要代理類那么就不行了。
因此,Spring AOP 會這樣子來進(jìn)行切換,因為Spring AOP 同時支持 CGLIB、ASPECTJ、JDK動態(tài)代理,當(dāng)你的真實對象有實現(xiàn)接口時,Spring AOP會默認(rèn)采用JDK動態(tài)代理,否則采用cglib代理。
- 如果目標(biāo)對象的實現(xiàn)類實現(xiàn)了接口,Spring AOP 將會采用 JDK 動態(tài)代理來生成 AOP 代理類;
- 如果目標(biāo)對象的實現(xiàn)類沒有實現(xiàn)接口,Spring AOP 將會采用 CGLIB 來生成 AOP 代理類——不過這個選擇過程對開發(fā)者完全透明、開發(fā)者也無需關(guān)心。
這里簡單說說代理模式,代理模式的UML類圖如下:
2.1 靜態(tài)代理
//接口類: interface Person { void speak(); } //真實實體類: class Actor implements Person { private String content; public Actor(String content) { this.content = content; } @Override public void speak() { System.out.println(this.content); } } //代理類: class Agent implements Person { private Actor actor; private String before; private String after; public Agent(Actor actor, String before, String after) { this.actor = actor; this.before = before; this.after = after; } @Override public void speak() { //before speak System.out.println("Before actor speak, Agent say: " + before); //real speak this.actor.speak(); //after speak System.out.println("After actor speak, Agent say: " + after); } } //測試方法: public class StaticProxy { public static void main(String[] args) { Actor actor = new Actor("I am a famous actor!"); Agent agent = new Agent(actor, "Hello I am an agent.", "That's all!"); agent.speak(); } }
2.2 動態(tài)代理
在講JDK的動態(tài)代理方法之前,不妨先想想如果讓你來實現(xiàn)一個可以任意類的任意方法的代理類,該怎么實現(xiàn)?有個很naive的做法,通過反射獲得Class和Method,再調(diào)用該方法,并且實現(xiàn)一些代理的方法。我嘗試了一下,很快就發(fā)現(xiàn)問題所在了。于是乎,還是使用JDK的動態(tài)代理接口吧。
JDK自帶方法
首先介紹一下最核心的一個接口和一個方法:
首先是java.lang.reflect包里的InvocationHandler接口:
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
我們對于被代理的類的操作都會由該接口中的invoke方法實現(xiàn),其中的參數(shù)的含義分別是:
- proxy:被代理的類的實例
- method:調(diào)用被代理的類的方法
- args:該方法需要的參數(shù)
使用方法首先是需要實現(xiàn)該接口,并且我們可以在invoke方法中調(diào)用被代理類的方法并獲得返回值,自然也可以在調(diào)用該方法的前后去做一些額外的事情,從而實現(xiàn)動態(tài)代理,下面的例子會詳細(xì)寫到。
另外一個很重要的靜態(tài)方法是java.lang.reflect包中的Proxy類的newProxyInstance方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
其中的參數(shù)含義如下:
- loader:被代理的類的類加載器
- interfaces:被代理類的接口數(shù)組
- invocationHandler:就是剛剛介紹的調(diào)用處理器類的對象實例
該方法會返回一個被修改過的類的實例,從而可以自由的調(diào)用該實例的方法。下面是一個實際例子。
Fruit接口: public interface Fruit { public void show(); } Apple實現(xiàn)Fruit接口: public class Apple implements Fruit{ @Override public void show() { System.out.println("<<<); } } 代理類Agent.java: public class DynamicAgent { //實現(xiàn)InvocationHandler接口,并且可以初始化被代理類的對象 static class MyHandler implements InvocationHandler { private Object proxy; public MyHandler(Object proxy) { this.proxy = proxy; } //自定義invoke方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(">>>>before invoking"); //真正調(diào)用方法的地方 Object ret = method.invoke(this.proxy, args); System.out.println(">>>>after invoking"); return ret; } } //返回一個被修改過的對象 public static Object agent(Class interfaceClazz, Object proxy) { return Proxy.newProxyInstance(interfaceClazz.getClassLoader(), new Class[]{interfaceClazz}, new MyHandler(proxy)); } } 測試類: public class ReflectTest { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException { //注意一定要返回接口,不能返回實現(xiàn)類否則會報錯 Fruit fruit = (Fruit) DynamicAgent.agent(Fruit.class, new Apple()); fruit.show(); } }
結(jié)果:
可以看到對于不同的實現(xiàn)類來說,可以用同一個動態(tài)代理類來進(jìn)行代理,實現(xiàn)了“一次編寫到處代理”的效果。
但是這種方法有個缺點,就是被代理的類一定要是實現(xiàn)了某個接口的,這很大程度限制了本方法的使用場景。下面還有另外一個使用了CGlib增強庫的方法。
2.3 CGLIB庫的方法
CGlib是一個字節(jié)碼增強庫,為AOP等提供了底層支持。下面看看它是怎么實現(xiàn)動態(tài)代理的。
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CGlibAgent implements MethodInterceptor { private Object proxy; public Object getInstance(Object proxy) { this.proxy = proxy; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(this.proxy.getClass()); // 回調(diào)方法 enhancer.setCallback(this); // 創(chuàng)建代理對象 return enhancer.create(); } //回調(diào)方法 @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println(">>>>before invoking"); //真正調(diào)用 Object ret = methodProxy.invokeSuper(o, objects); System.out.println(">>>>after invoking"); return ret; } public static void main(String[] args) { CGlibAgent cGlibAgent = new CGlibAgent(); Apple apple = (Apple) cGlibAgent.getInstance(new Apple()); apple.show(); } }
三、Spring中的AOP: @AspectJ
3.1 @AspectJ 由來
AspectJ是一個AOP框架,它能夠?qū)ava代碼進(jìn)行AOP編譯(一般在編譯期進(jìn)行),讓java代碼具有AspectJ的AOP功能(當(dāng)然需要特殊的編譯器),可以這樣說AspectJ是目前實現(xiàn)AOP框架中最成熟,功能最豐富的語言,更幸運的是,AspectJ與java程序完全兼容,幾乎是無縫關(guān)聯(lián),因此對于有java編程基礎(chǔ)的工程師,上手和使用都非常容易。
其實AspectJ單獨就是一門語言,它需要專門的編譯器(ajc編譯器). Spring AOP 與ApectJ的目的一致,都是為了統(tǒng)一處理橫切業(yè)務(wù),但與AspectJ不同的是,Spring AOP并不嘗試提供完整的AOP功能(即使它完全可以實現(xiàn)),Spring AOP 更注重的是與Spring IOC容器的結(jié)合,并結(jié)合該優(yōu)勢來解決橫切業(yè)務(wù)的問題,因此在AOP的功能完善方面,相對來說AspectJ具有更大的優(yōu)勢,同時,Spring注意到AspectJ在AOP的實現(xiàn)方式上依賴于特殊編譯器(ajc編譯器),因此Spring很機智回避了這點,轉(zhuǎn)向采用動態(tài)代理技術(shù)的實現(xiàn)原理來構(gòu)建Spring AOP的內(nèi)部機制(動態(tài)織入),這是與AspectJ(靜態(tài)織入)最根本的區(qū)別。在AspectJ 1.5后,引入@Aspect形式的注解風(fēng)格的開發(fā),Spring也非常快地跟進(jìn)了這種方式,因此Spring 2.0后便使用了與AspectJ一樣的注解。請注意,Spring 只是使用了與 AspectJ 5 一樣的注解,但仍然沒有使用 AspectJ 的編譯器,底層依是動態(tài)代理技術(shù)的實現(xiàn),因此并不依賴于 AspectJ 的編譯器。
所以,Spring AOP雖然是使用了AspectJ那一套注解,其實實現(xiàn)AOP的底層是使用了動態(tài)代理(JDK或者CGLib)來動態(tài)植入。
3.2 舉個栗子
小狗類,會說話:
? public class Dog { private String name; public void say(){ System.out.println(name + "在汪汪叫!..."); } public String getName() { return name; } public void setName(String name) { this.name = name; } } 切面類: @Aspect //聲明自己是一個切面類 public class MyAspect { /** * 前置通知 */ //@Before是增強中的方位 // @Before括號中的就是切入點了 //before()就是傳說的增強(建言):說白了,就是要干啥事. @Before("execution(* com.zdy..*(..))") public void before(){ System.out.println("前置通知...."); } } ?
這個類是重點,先用@Aspect聲明自己是切面類,然后before()為增強,@Before(方位)+切入點可以具體定位到具體某個類的某個方法的方位. Spring配置文件:
//開啟AspectJ功能. <aop:aspectj-autoproxy /> <bean id="dog" class="com.zdy.Dog" /> <bean name="myAspect" class="com.zdy.MyAspect"/> 然后Main方法: ApplicationContext ac =new ClassPathXmlApplicationContext("applicationContext.xml"); Dog dog =(Dog) ac.getBean("dog"); System.out.println(dog.getClass()); dog.say();
輸出結(jié)果:
class com.zdy.Dog$$EnhancerBySpringCGLIB$$80a9ee5f
前置通知....
null在汪汪叫!...
說白了,就是把切面類丟到容器,開啟一個AdpectJ的功能,Spring AOP就會根據(jù)切面類中的(@Before+切入點)定位好具體的類的某個方法(我這里定義的是com.zdy包下的所有類的所有方法),然后把增強before()切入進(jìn)去.
3.3 舉個Spring Boot中的栗子
這個栗子很實用,關(guān)于Aop做切面去統(tǒng)一處理Web請求的日志:
@Aspect @Component public class WebLogAspect { private Logger logger = Logger.getLogger(getClass()); @Pointcut("execution(public * com.didispace.web..*.*(..))") public void webLog(){} @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 接收到請求,記錄請求內(nèi)容 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 記錄下請求內(nèi)容 logger.info("URL : " + request.getRequestURL().toString()); logger.info("HTTP_METHOD : " + request.getMethod()); logger.info("IP : " + request.getRemoteAddr()); logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs())); } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) throws Throwable { // 處理完請求,返回內(nèi)容 logger.info("RESPONSE : " + ret); } }
可以看上面的例子,通過 @Pointcut 定義的切入點為 com.didispace.web 包下的所有函數(shù)(對web層所有請求處理做切入點),然后通過 @Before 實現(xiàn),對請求內(nèi)容的日志記錄(本文只是說明過程,可以根據(jù)需要調(diào)整內(nèi)容),最后通過 @AfterReturning 記錄請求返回的對象。
通過運行程序并訪問: //localhost:8080/hello?name=didi ,可以獲得下面的日志輸出
2016-05-19 13:42:13,156 INFO WebLogAspect:41 - URL : http://localhost:8080/hello
2016-05-19 13:42:13,156 INFO WebLogAspect:42 - HTTP_METHOD : http://localhost:8080/hello
2016-05-19 13:42:13,157 INFO WebLogAspect:43 - IP : 0:0:0:0:0:0:0:1
2016-05-19 13:42:13,160 INFO WebLogAspect:44 - CLASS_METHOD : com.didispace.web.HelloController.hello
2016-05-19 13:42:13,160 INFO WebLogAspect:45 - ARGS : [didi]
2016-05-19 13:42:13,170 INFO WebLogAspect:52 - RESPONSE:Hello didi
3.4 Spring AOP支持的幾種AspectJ注解
- 前置通知@Before: 前置通知通過@Before注解進(jìn)行標(biāo)注,并可直接傳入切點表達(dá)式的值,該通知在目標(biāo)函數(shù)執(zhí)行前執(zhí)行,注意JoinPoint,是Spring提供的靜態(tài)變量,通過joinPoint 參數(shù),可以獲取目標(biāo)對象的信息,如類名稱,方法參數(shù),方法名稱等,該參數(shù)是可選的。
@Before("execution(...)") public void before(JoinPoint joinPoint){ System.out.println("..."); }
- 后置通知@AfterReturning: 通過@AfterReturning注解進(jìn)行標(biāo)注,該函數(shù)在目標(biāo)函數(shù)執(zhí)行完成后執(zhí)行,并可以獲取到目標(biāo)函數(shù)最終的返回值returnVal,當(dāng)目標(biāo)函數(shù)沒有返回值時,returnVal將返回null,必須通過returning = “returnVal”注明參數(shù)的名稱而且必須與通知函數(shù)的參數(shù)名稱相同。請注意,在任何通知中這些參數(shù)都是可選的,需要使用時直接填寫即可,不需要使用時,可以完成不用聲明出來。
@AfterReturning(value="execution(...)",returning = "returnVal") public void AfterReturning(JoinPoint joinPoint,Object returnVal){ System.out.println("我是后置通知...returnVal+"+returnVal); }
- 異常通知 @AfterThrowing:該通知只有在異常時才會被觸發(fā),并由throwing來聲明一個接收異常信息的變量,同樣異常通知也用于Joinpoint參數(shù),需要時加上即可.
@AfterThrowing(value="execution(....)",throwing = "e") public void afterThrowable(Throwable e){ System.out.println("出現(xiàn)異常:msg="+e.getMessage()); }
- 最終通知 @After:該通知有點類似于finally代碼塊,只要應(yīng)用了無論什么情況下都會執(zhí)行.
@After("execution(...)") public void after(JoinPoint joinPoint) { System.out.println("最終通知...."); }
- 環(huán)繞通知 @Around: 環(huán)繞通知既可以在目標(biāo)方法前執(zhí)行也可在目標(biāo)方法之后執(zhí)行,更重要的是環(huán)繞通知可以控制目標(biāo)方法是否指向執(zhí)行,但即使如此,我們應(yīng)該盡量以最簡單的方式滿足需求,在僅需在目標(biāo)方法前執(zhí)行時,應(yīng)該采用前置通知而非環(huán)繞通知。案例代碼如下第一個參數(shù)必須是ProceedingJoinPoint,通過該對象的proceed()方法來執(zhí)行目標(biāo)函數(shù),proceed()的返回值就是環(huán)繞通知的返回值。同樣的,ProceedingJoinPoint對象也是可以獲取目標(biāo)對象的信息,如類名稱,方法參數(shù),方法名稱等等
@Around("execution(...)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("我是環(huán)繞通知前...."); //執(zhí)行目標(biāo)函數(shù) Object obj= (Object) joinPoint.proceed(); System.out.println("我是環(huán)繞通知后...."); return obj; }
然后說下一直用"…"忽略掉的切入點表達(dá)式,這個表達(dá)式可以不是exection(…),還有其他的一些,我就不說了,說最常用的execution:
? //scope :方法作用域,如public,private,protect //returnt-type:方法返回值類型 //fully-qualified-class-name:方法所在類的完全限定名稱 //parameters 方法參數(shù) execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters)) <fully-qualified-class-name>.*(parameters) ?
注意這一塊,如果沒有精確到class-name,而是到包名就停止了,要用兩個"…"來表示包下的任意類:
- execution(* com.zdy…*(…)):com.zdy包下所有類的所有方法.
- execution(* com.zdy.Dog.*(…)): Dog類下的所有方法.
具體詳細(xì)語法,大家如果有需求自行g(shù)oogle了,我最常用的就是這倆了。要么按照包來定位,要么按照具體類來定位.
在使用切入點時,還可以抽出來一個@Pointcut來供使用:
/** * 使用Pointcut定義切點 */ @Pointcut("execution(...)") private void myPointcut(){} /** * 應(yīng)用切入點函數(shù) */ @After(value="myPointcut()") public void afterDemo(){ System.out.println("最終通知...."); }
可以避免重復(fù)的execution在不同的注解里寫很多遍…
3.5 AOP切面的優(yōu)先級
由于通過AOP實現(xiàn),程序得到了很好的解耦,但是也會帶來一些問題,比如:我們可能會對Web層做多個切面,校驗用戶,校驗頭信息等等,這個時候經(jīng)常會碰到切面的處理順序問題。
所以,我們需要定義每個切面的優(yōu)先級,我們需要@Order(i)注解來標(biāo)識切面的優(yōu)先級。i的值越小,優(yōu)先級越高。假設(shè)我們還有一個切面是CheckNameAspect用來校驗name必須為derry,我們?yōu)槠湓O(shè)置@Order(10),而上文中WebLogAspect設(shè)置為@Order(5),所以WebLogAspect有更高的優(yōu)先級,這個時候執(zhí)行順序是這樣的:
- 在@Before中優(yōu)先執(zhí)行@Order(5)的內(nèi)容,再執(zhí)行@Order(10)的內(nèi)容
- 在@After和@AfterReturning中優(yōu)先執(zhí)行@Order(10)的內(nèi)容,再執(zhí)行@Order(5)的內(nèi)容
所以我們可以這樣子總結(jié):
- 在切入點前的操作,按order的值由小到大執(zhí)行
- 在切入點后的操作,按order的值由大到小執(zhí)行
到此這篇關(guān)于Java中的SpringAOP、代理模式、常用AspectJ注解詳解的文章就介紹到這了,更多相關(guān)AOP、代理模式與AspectJ內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot深入理解之內(nèi)置web容器及配置的總結(jié)
今天小編就為大家分享一篇關(guān)于SpringBoot深入理解之內(nèi)置web容器及配置的總結(jié),小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03Scala可變參數(shù)列表,命名參數(shù)和參數(shù)缺省詳解
這篇文章主要介紹了Scala可變參數(shù)列表,命名參數(shù)和參數(shù)缺省詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06SpringCloud Feign 服務(wù)調(diào)用的實現(xiàn)
Feign是一個聲明性web服務(wù)客戶端。本文記錄多個服務(wù)之間使用Feign調(diào)用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01