詳解Spring中的AOP及AspectJ五大通知注解
AOP 基本概念
AOP(Aspect-Oriented Programming, 面向切面編程): 是一種新的方法論,是對(duì)傳統(tǒng) -OOP(Object-Oriented Programming, 面向?qū)ο缶幊? 的補(bǔ)充。
AOP 的主要編程對(duì)象是切面(aspect), 切面模塊化橫切關(guān)注點(diǎn)。
在應(yīng)用 AOP 編程時(shí), 仍然需要定義公共功能, 但可以明確的定義這個(gè)功能用在哪里,以什么方式應(yīng)用,并且不必修改受影響的類,這樣一來橫切關(guān)注點(diǎn)就被模塊化到特殊的對(duì)象(切面)里。
AOP 的好處:
- 每個(gè)事物邏輯位于一個(gè)位置,代碼不分散,便于維護(hù)和升級(jí)。
- 業(yè)務(wù)模塊更簡(jiǎn)潔,只包含核心業(yè)務(wù)代碼。
AOP術(shù)語
- 切面(Aspect): 橫切關(guān)注點(diǎn)(跨越應(yīng)用程序多個(gè)模塊的功能)被模塊化的特殊對(duì)象。
- 通知(Advice): 切面必須要完成的工作。
- 目標(biāo)(Target): 被通知的對(duì)象。
- 代理(Proxy): 向目標(biāo)對(duì)象應(yīng)用通知之后創(chuàng)建的對(duì)象。
- 連接點(diǎn)(Joinpoint):程序執(zhí)行的某個(gè)特定位置:如類某個(gè)方法調(diào)用前、調(diào)用后、方法拋出異常后等。連接點(diǎn)由兩個(gè)信息確定:方法表示的程序執(zhí)行點(diǎn);相對(duì)點(diǎn)表示的方位。方位為方法執(zhí)行前的位置。
- 切點(diǎn)(pointcut):每個(gè)類都擁有多個(gè)連接點(diǎn):即連接點(diǎn)是程序類中客觀存在的事務(wù)。AOP 通過切點(diǎn)定位到特定的連接點(diǎn)。類比:連接點(diǎn)相當(dāng)于數(shù)據(jù)庫中的記錄,切點(diǎn)相當(dāng)于查詢條件。切點(diǎn)和連接點(diǎn)不是一對(duì)一的關(guān)系,一個(gè)切點(diǎn)匹配多個(gè)連接點(diǎn),切點(diǎn)通過
org.springframework.aop.Pointcut
接口進(jìn)行描述,它使用類和方法作為連接點(diǎn)的查詢條件。
AspectJ
- AspectJ:Java 社區(qū)里最完整最流行的 AOP 框架.
- 在 Spring2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP
應(yīng)用
本文以一個(gè)簡(jiǎn)單計(jì)算器為代碼例子
1、在Spring中啟用AspectJ 注解支持
- 要在 Spring 應(yīng)用中使用 AspectJ 注解, 必須在 classpath 下包含 AspectJ 類庫: aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar
- 將aopSchema添加到 beans根元素中
- 要在 Spring IOC 容器中啟用 AspectJ 注解支持,只要在Bean配置文件中定義一個(gè)空的XML元素<aop:aspectj-autoproxy>
- 當(dāng) Spring IOC 容器偵測(cè)到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素時(shí),會(huì)自動(dòng)為與 AspectJ 切面匹配的 Bean 創(chuàng)建代理
2、用 AspectJ 注解聲明切面
- 要在Spring中聲明AspectJ切面只需要在IOC容器中將切面聲明為Bean實(shí)例。當(dāng)在 Spring IOC 容器中初始化 AspectJ 切面之后,Spring IOC 容器就會(huì)為那些與 AspectJ 切面相匹配的 Bean 創(chuàng)建代理。
- 在AspectJ注解中, 切面只是一個(gè)帶有
@Aspect
注解的Java類。 - 通知是標(biāo)注有某種注解的簡(jiǎn)單的Java方法。
- AspectJ 支持 5 種類型的通知注解:
- @Before: 前置通知,在方法執(zhí)行之前執(zhí)行
- @After: 后置通知,在方法執(zhí)行之后執(zhí)行
- @AfterRunning:返回通知,在方法返回結(jié)果之后執(zhí)行
- @AfterThrowing: 異常通知,在方法拋出異常之后
- @Around: 環(huán)繞通知,圍繞著方法執(zhí)行
前置通知
- 前置通知:在方法執(zhí)行之前執(zhí)行的通知。
- 前置通知使用
@Before
注解, 并將切入點(diǎn)表達(dá)式的值作為注解值。
后置通知
- 后置通知是在連接點(diǎn)完成之后執(zhí)行的,即連接點(diǎn)返回結(jié)果或者拋出異常的時(shí)候,下面的后置通知記錄了方法的終止。
- 一個(gè)切面可以包括一個(gè)或者多個(gè)通知。
返回通知
- 無論連接點(diǎn)是正常返回還是拋出異常,后置通知都會(huì)執(zhí)行。如果只想在連接點(diǎn)返回的時(shí)候記錄日志, 應(yīng)使用返回通知代替后置通知。
- 在返回通知中, 只要將 returning 屬性添加到 @AfterReturning 注解中, 就可以訪問連接點(diǎn)的返回值。 該屬性的值即為用來傳入返回值的參數(shù)名稱。
- 必須在通知方法的簽名中添加一個(gè)同名參數(shù)。在運(yùn)行時(shí),Spring AOP 會(huì)通過這個(gè)參數(shù)傳遞返回值。
- 原始的切點(diǎn)表達(dá)式需要出現(xiàn)在pointcut屬性中。
異常通知
- 只在連接點(diǎn)拋出異常時(shí)才執(zhí)行異常通知。
- 將Throwing屬性添加到
@AfterThrowing
注解中, 也可以訪問連接點(diǎn)拋出的異常. Throwable 是所有錯(cuò)誤和異常類的超類,所以在異常通知方法可以捕獲到任何錯(cuò)誤和異常。 - 如果只對(duì)某種特殊的異常類型感興趣, 可以將參數(shù)聲明為其他異常的參數(shù)類型, 然后通知就只在拋出這個(gè)類型及其子類的異常時(shí)才被執(zhí)行。
環(huán)繞通知
- 環(huán)繞通知是所有通知類型中功能最為強(qiáng)大的, 能夠全面地控制連接點(diǎn), 甚至可以控制是否執(zhí)行連接點(diǎn)。
- 對(duì)于環(huán)繞通知來說,連接點(diǎn)的參數(shù)類型必須是ProceedingJoinPoint ,它是 JoinPoint 的子接口,允許控制何時(shí)執(zhí)行,是否執(zhí)行連接點(diǎn)。
- 在環(huán)繞通知中需要明確調(diào)用ProceedingJoinPoint的proceed()方法來執(zhí)行被代理的方法.如果忘記這樣做就會(huì)導(dǎo)致通知被執(zhí)行了,但目標(biāo)方法沒有被執(zhí)行。
- 注意: 環(huán)繞通知的方法需要返回目標(biāo)方法執(zhí)行之后的結(jié)果,即調(diào)用 joinPoint.proceed()的返回值,否則會(huì)出現(xiàn)空指針異常。
實(shí)現(xiàn)代碼
1、Calc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.ycfxhsw.aop"></context:component-scan> <!-- 使用AspactJ,注解起作用:自動(dòng)為匹配的類生成代理對(duì)象 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
2、Calc接口
public interface Calc { // 加法 int Addition(int i, int j); // 減法 int Subtraction(int i, int j); // 乘法 int Multiplication(int i, int j); // 除法 int Division(int i, int j); }
3、接口實(shí)現(xiàn)CalcImp
import org.springframework.stereotype.Component; @Component public class CalcImp implements Calc { public int Addition(int i, int j) { int result = i + j; return result; } public int Subtraction(int i, int j) { int result = i + j; return result; } public int Multiplication(int i, int j) { int result = i + j; return result; } public int Division(int i, int j) { int result = i + j; return result; } }
4、LoggingAspect
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; // @Order(1) 指定切面優(yōu)先級(jí) @Component @Aspect public class LoggingAspect { /** * 申明切入點(diǎn)表達(dá)式,一般該方法內(nèi)不需要添加其他方法 * 使用 @Pointcut 申明切入點(diǎn)表達(dá)式 * 后面的切入點(diǎn)直接使用方法名 */ @Pointcut("execution(public int com.ycfxhsw.aop.Calc.*(..))") public void DeclareJoinPointExpression() { } /** * 前置通知 * @param joinPoint */ @Before("execution(public int com.ycfxhsw.aop.Calc.*(int, int))") public void BeforeMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); List<Object> list = Arrays.asList(joinPoint.getArgs()); System.out.println("Method Starts..." + MethodName + " with " + list); } /** * 后置通知(無論有無異常) * 不能訪問目標(biāo)方法的執(zhí)行結(jié)果 * @param joinPoint * com.ycfxhsw.aop.Calc.*(..) * 重用 DeclareJoinPointExpression() */ @After("DeclareJoinPointExpression()") public void AfterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); System.out.println("Method Ends..." + MethodName); } /** * 返回通知 * 在方法正常結(jié)束后執(zhí)行 * 可以訪問目標(biāo)方法的執(zhí)行結(jié)果 * @param joinPoint,result */ @AfterReturning(value = "execution(public int com.ycfxhsw.aop.Calc.*(..))", returning = "result") public void AfterReturningMethod(JoinPoint joinPoint, Object result) { String MethodName = joinPoint.getSignature().getName(); System.out.println("Method AfterReturning..." + MethodName + " --> " + result); } /** * 異常通知 * @param joinPoint,exception */ @AfterThrowing(value = "execution(public int com.ycfxhsw.aop.Calc.*(..))", throwing = "exception") public void AfterThrowingMethod(JoinPoint joinPoint, Exception exception) { String MethodName = joinPoint.getSignature().getName(); System.out.println("Method AfterThrowing..." + MethodName + " --> " + exception); } /** * 環(huán)繞通知,需要攜帶 ProceedingJoinPoint 類型的參數(shù) * 類似于動(dòng)態(tài)代理全過程 * 必須有返回值(目標(biāo)方法返回值) * @param proceedingJoinPoint @Around("execution(public int com.ycfxhsw.aop.Calc.*(..))") public Object AroundMethod(ProceedingJoinPoint proceedingJoinPoint) { Object result = null; String methodName = proceedingJoinPoint.getSignature().getName(); try { // 前置通知 System.out.println("The method " + methodName + " begins with " + Arrays.asList(proceedingJoinPoint.getArgs())); // 執(zhí)行目標(biāo)方法 result = proceedingJoinPoint.proceed(); // 返回通知 System.out.println("The method " + methodName + " ends with " + result); } catch (Throwable e) { // 異常通知 System.out.println("The method " + methodName + " occurs exception:" + e); throw new RuntimeException(e); } // 后置通知 System.out.println("The method " + methodName + " ends"); return result; } */ }
5、測(cè)試Main
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * 測(cè)試 */ public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Calc.xml"); Calc calc = applicationContext.getBean(Calc.class); System.out.println(calc.getClass().getName()); int resultAdd = calc.Addition(5, 6); System.out.println("result-->" + resultAdd + "\n"); int resultSub = calc.Subtraction(9, 3); System.out.println("result-->" + resultSub + "\n"); int resultMul = calc.Multiplication(8, 6); System.out.println("result-->" + resultMul + "\n"); int resultDiv = calc.Division(99, 3); System.out.println("result-->" + resultDiv + "\n"); } }
3、指定切面的優(yōu)先級(jí)
- 在同一個(gè)連接點(diǎn)上應(yīng)用不止一個(gè)切面時(shí),除非明確指定,否則它們的優(yōu)先級(jí)是不確定的.
- 切面的優(yōu)先級(jí)可以通過實(shí)現(xiàn) Ordered 接口或利用
@Order
注解指定. - 實(shí)現(xiàn) Ordered 接口,
getOrder()
方法的返回值越小, 優(yōu)先級(jí)越高. - 若使用
@Order
注解,序號(hào)出現(xiàn)在注解中
4、重用切入點(diǎn)定義
- 在編寫 AspectJ 切面時(shí),可以直接在通知注解中書寫切入點(diǎn)表達(dá)式,但同一個(gè)切點(diǎn)表達(dá)式可能會(huì)在多個(gè)通知中重復(fù)出現(xiàn)。
- 在 AspectJ 切面中, 可以通過 @Pointcut注解將一個(gè)切入點(diǎn)聲明成簡(jiǎn)單的方法,切入點(diǎn)的方法體通常是空的,因?yàn)閷⑶腥朦c(diǎn)定義與應(yīng)用程序邏輯混在一起是不合理的。
- 切入點(diǎn)方法的訪問控制符同時(shí)也控制著這個(gè)切入點(diǎn)的可見性,如果切入點(diǎn)要在多個(gè)切面中共用, 最好將它們集中在一個(gè)公共的類中,在這種情況下, 它們必須被聲明為 public。在引入這個(gè)切入點(diǎn)時(shí),必須將類名也包括在內(nèi),如果類沒有與這個(gè)切面放在同一個(gè)包中, 還必須包含包名。
- 其他通知可以通過方法名稱引入該切入點(diǎn)。
到此這篇關(guān)于詳解Spring中的AOP及AspectJ五大通知注解的文章就介紹到這了,更多相關(guān)Spring的AOP及AspectJ內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
你應(yīng)該知道的21個(gè)Java核心技術(shù)
Java的21個(gè)核心技術(shù)點(diǎn),你知道嗎?這篇文章主要為大家詳細(xì)介紹了Java核心技術(shù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08手把手帶你實(shí)現(xiàn)一個(gè)萌芽版的Spring容器
大家好,我是老三,Spring是我們最常用的開源框架,經(jīng)過多年發(fā)展,Spring已經(jīng)發(fā)展成枝繁葉茂的大樹,讓我們難以窺其全貌,這節(jié),我們回歸Spring的本質(zhì),五分鐘手?jǐn)]一個(gè)Spring容器,揭開Spring神秘的面紗2022-03-03Spring MVC+MyBatis+MySQL實(shí)現(xiàn)分頁功能實(shí)例
分頁功能是我們?nèi)粘i_發(fā)中經(jīng)常會(huì)遇到的,下面這篇文章主要給大家介紹了Spring MVC+MyBatis+MySQL實(shí)現(xiàn)分頁功能的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-06-06Java調(diào)用IK分詞器進(jìn)行分詞方式,封裝工具類
這篇文章主要介紹了Java調(diào)用IK分詞器進(jìn)行分詞方式,封裝工具類,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08springboot如何開啟一個(gè)監(jiān)聽線程執(zhí)行任務(wù)
這篇文章主要介紹了springboot如何開啟一個(gè)監(jiān)聽線程執(zhí)行任務(wù)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02mybatis的dtd約束文件及配置文件xml自動(dòng)提示操作
這篇文章主要介紹了mybatis的dtd約束文件及配置文件xml自動(dòng)提示操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12Java基礎(chǔ)之多線程方法狀態(tài)和創(chuàng)建方法
Java中可以通過Thread類和Runnable接口來創(chuàng)建多個(gè)線程,下面這篇文章主要給大家介紹了關(guān)于Java基礎(chǔ)之多線程方法狀態(tài)和創(chuàng)建方法的相關(guān)資料,需要的朋友可以參考下2021-09-09