Spring面向切面編程AOP詳情
1. 面向切面編程
- 定義:面向切面編程(AOP,Aspect Oriented Programming)是通過預(yù)編譯方式和運行期間動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)。
- 作用:利用AOP可以對業(yè)務(wù)邏輯的各個部分進行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。
- 主要功能:日志記錄、性能統(tǒng)計、安全控制、事務(wù)處理、異常處理等。
- 總結(jié):面向切面編程是希望能夠?qū)⑼ㄓ眯枨蠊δ軓牟幌嚓P(guān)的類當(dāng)中分離出來,能夠使得很多類共享一個行為,一旦發(fā)生變化,不必修改很多類,而只是修改這個行為即可。
AOP通過提供另一種思考程序結(jié)構(gòu)的方式來補充了面向?qū)ο缶幊蹋∣OP)。OOP中模塊化的基本單元是類(class),而AOP中模塊化的基本單元是切面(aspect)。可以這么理解,OOP是解決了縱向的代碼復(fù)用問題,AOP是解決了橫向的代碼復(fù)用問題。
Spring的關(guān)鍵組件之一是AOP框架。雖然Spring IOC容器不依賴于AOP,意味著如果你不想使用AOP,則可以不使用AOP,但AOP補充了Spring IOC以提供一個非常強大的中間件解決方案。
2. AOP核心概念
- 切面(aspect):在AOP中,切面一般使用@Aspect注解來標(biāo)識。
- 連接點(Join Point):在Spring AOP,一個連接點總是代表一次方法的執(zhí)行。
- 增強(Advice):在連接點執(zhí)行的動作。
- 切入點(Pointcout):說明如何匹配到連接點。引
- 介(Introduction):為現(xiàn)有類型聲明額外的方法和屬性。
- 目標(biāo)對象(Target Object):由一個或者多個切面建議的對象,也被稱為“建議對象”,由于Spring AOP是通過動態(tài)代理來實現(xiàn)的,這個對象永遠(yuǎn)是一個代理對象。
- AOP代理(AOP proxy):一個被AOP框架創(chuàng)建的對象,用于實現(xiàn)切面約定(增強方法的執(zhí)行等)。在Spring Framework中,一個AOP代理是一個JDK動態(tài)代理或者CGLIB代理。
- 織入(Weaving):連接切面和目標(biāo)對象或類型創(chuàng)建代理對象的過程。它能在編譯時(例如使用AspectJ編譯器)、加載時或者運行時完成。Spring AOP與其他的純Java AOP框架一樣是在運行時進行織入的。
Spring AOP包括以下類型的增強:
- 前置增強(Before advice):在連接點之前運行,但不能阻止到連接點的流程繼續(xù)執(zhí)行(除非拋出異常)
- 返回增強(After returning advice):在連接點正常完成后運行的增強(例如,方法返回沒有拋出異常)
- 異常增強(After thorwing advice):如果方法拋出異常退出需要執(zhí)行的增強
- 后置增強(After (finally) Advice):無論連接點是正常或者異常退出,都會執(zhí)行該增強
- 環(huán)繞增強(Around advice):圍繞連接點的增強,例如方法的調(diào)用。環(huán)繞增強能在方法的調(diào)用之前和調(diào)用之后自定義行為。它還可以選擇方法是繼續(xù)執(zhí)行或者去縮短方法的執(zhí)行通過返回自己的值或者拋出異常。
3. AOP的實現(xiàn)
AOP的兩種實現(xiàn)方式:靜態(tài)織入(以AspectJ為代表)和動態(tài)代理(Spring AOP實現(xiàn))
AspectJ是一個采用Java實現(xiàn)的AOP框架,它能夠?qū)Υa進行編譯(在編譯期進行),讓代碼具有AspectJ的AOP功能,當(dāng)然它也可支持動態(tài)代理的方式;
Spring AOP實現(xiàn):通過動態(tài)代理技術(shù)來實現(xiàn),Spring2.0集成了AspectJ,主要用于PointCut的解析和匹配,底層的技術(shù)還是使用的Spring1.x中的動態(tài)代理來實現(xiàn)。
Spring AOP采用了兩種混合的實現(xiàn)方式:JDK動態(tài)代理和CGLib動態(tài)代理。
JDK動態(tài)代理:Spring AOP的首選方法。每當(dāng)目標(biāo)對象實現(xiàn)一個接口時,就會使用JDK動態(tài)代理。目標(biāo)對象必須實現(xiàn)接口。
CGLIB:如果目標(biāo)對象沒有實現(xiàn)接口,則可以使用CGLIB代理。
4. Spring 對AOP支持
Spring可以使用兩種方式來實現(xiàn)AOP:基于注解式配置和基于XML配置
下面介紹基于注解配置的形式
4.1 支持@Aspect
如果是Spring Framework,需要使用aspectjweaver.jar包,然后創(chuàng)建我們自己的AppConfig,如下,并加上@EnableAspectJAutoProxy注解開啟AOP代理自動配置(Spring Boot默認(rèn)是開啟的,則不需要增加配置),
如下:
@Configuration @EnableAspectJAutoProxy public class AppConfig { }
4.2 聲明一個切面
@Aspect //告訴Spring 這是一個切面 @Component //交給Spring容器管理 public class MyAspect { }
可以使用@Aspect來定義一個類作為切面,但是這樣,該類并不會自動被Spring加載,還是需要加上@Component注解
4.3 聲明一個切入點
一個切入點的生命包含兩個部分:一個包含名稱和任何參數(shù)的簽名和一個切入點的表達式,這個表達式確定了我們對哪些方法的執(zhí)行感興趣。
我們以攔截Controller層中的MyController中的test方法為例子,代碼如下:
@RestController @RequestMapping("/my") public class MyController { @GetMapping("/test") public void test() { System.out.println("test 方法"); } }
下面定義一個名為controller的切入點,該切入點與上述的test方法相匹配,切入點需要用@Pointcut注解來標(biāo)注,如下:
//表達式 @Pointcut("execution (public * com.yc.springboot.controller.MyController.test())") public void controller(){}; //簽名
切入點表達式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) execution(方法修飾符(可選) 返回類型 類路徑 方法名 參數(shù) 異常模式(可選))
AspectJ描述符如下:
AspectJ描述符 | 描述 |
arg() | 限制連接點匹配參數(shù)為指定類型的執(zhí)行方法 |
@args() | 限制連接點匹配參數(shù)由指定注解標(biāo)注的執(zhí)行方法 |
execution() | 用于匹配是連接點的執(zhí)行方法 |
this() | 限制連接點匹配的AOP代理的bean引用為指定類型的類 |
target | 限制連接點匹配目標(biāo)對象為指定類型的類 |
@target() | 限制連接點匹配特定的執(zhí)行對象,這些對象對應(yīng)的類要具有指定類型的注解 |
within() | 限制連接點匹配指定的類型 |
@within() | 限制連接點匹配指定注解所標(biāo)注的類型 |
@annotationn | 限定匹配帶有指定注解的連接點 |
常用的主要是:execution()
AspectJ類型匹配的通配符:
within() | 限制連接點匹配指定的類型 |
@within() | 限制連接點匹配指定注解所標(biāo)注的類型 |
@annotationn | 限定匹配帶有指定注解的連接點 |
常用的匹配規(guī)則:
表達式 | 內(nèi)容 |
execution(public * *(..)) | 匹配所有public方法 |
execution(* set*(..)) | 匹配所有方法名開頭為set的方法 |
execution(* com.xyz.service.AccountService.*(..)) | 匹配AccountService下的所有方 |
execution(* com.xyz.service.*.*(..)) | 匹配service包下的所有方法 |
execution(* com.xyz.service..*.*(..)) | 匹配service包或其子包下的所有方法 |
@annotation(org.springframework.transaction.annotation.Transactional) | 匹配所有打了@Transactional注解的方法 |
bean(*Service) | 匹配命名后綴為Service的類的方法 |
4.4 聲明增強
增強與切點表達式相關(guān)聯(lián),并且在與切點匹配的方法之前、之后或者前后執(zhí)行。
在3當(dāng)中已經(jīng)對各類增強做了紹,這里就不詳細(xì)展開了,下面直接羅列了各種增強的聲明,用于攔截MyController中的各個方法
@Aspect //告訴Spring 這是一個切面 @Component //告訴Spring容器需要管理該對象 public class MyAspect { //通過規(guī)則確定哪些方法是需要增強的 @Pointcut("execution (public * com.yc.springboot.controller.MyController.*(..))") public void controller() { } //前置增強 @Before("controller()") public void before(JoinPoint joinPoint) { System.out.println("before advice"); } //返回增強 @AfterReturning( pointcut = "controller()", returning = "retVal" ) public void afterReturning(Object retVal) { System.out.println("after returning advice, 返回結(jié)果 retVal:" + retVal); } //異常增強 @AfterThrowing( pointcut = "controller()", throwing = "ex" ) public void afterThrowing(Exception ex) { System.out.println("after throwing advice, 異常 ex:" + ex.getMessage()); } //后置增強 @After("controller()") public void after(JoinPoint joinPoint) { System.out.println("after advice"); } //環(huán)繞增強 @Around("controller()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("before advice"); //相當(dāng)于是before advice Object reVal = null; try { reVal = joinPoint.proceed(); } catch (Exception e) { //相當(dāng)于afterthrowing advice System.out.println("afterthrowing advice"); } //相當(dāng)于是after advice System.out.println("after advice"); return reVal; } }
需要注意的是:
- 在返回增強中,我們需要給@AfterReturing設(shè)置returning的值,且需要與方法的參數(shù)名一致,用于表示業(yè)務(wù)方法的返回值
- 在異常增強中,需要給@AfterThrowing設(shè)置throwing的值,且需要與方法的參數(shù)名一致,用于表示業(yè)務(wù)方法產(chǎn)生的異常在環(huán)繞增強中,參數(shù)為ProceedingJoinPoint類型,它是JoinPoint的子接口,我們需要在這個方法中手動調(diào)用其proceed方法來觸發(fā)業(yè)務(wù)方法
- 在所有的增強方法中都可以申明第一個參數(shù)為JoinPoint(注意的是,環(huán)繞增強是使用ProceedingJoinPoint來進行申明,它實現(xiàn)了JoinPoint接口)
- JoinPoint接口提供了幾個有用的方法 :
- getArgs():返回這個方法的參數(shù)
- getThis():返回這個代理對象
- getTarget():返回目標(biāo)對象(被代理的對象)
- getSignature():返回被增強方法的描述
- toString():打印被增強方法的有用描述
下面為Mycontroller測試類:
@RestController @RequestMapping("/my") public class MyController { @GetMapping("/testBefore") public void testBefore() { System.out.println("testBefore 業(yè)務(wù)方法"); } @GetMapping("/testAfterReturning") public String testAfterReturning() { System.out.println("testAfterReturning 業(yè)務(wù)方法"); return "我是一個返回值"; } @GetMapping("/testAfterThrowing") public void testAfterThrowing() { System.out.println("testAfterThrowing 業(yè)務(wù)方法"); int a = 0; int b = 1 / a; } @GetMapping("/testAfter") public void testAfter() { System.out.println("testAfter 業(yè)務(wù)方法"); } @GetMapping("/around") public void around() { System.out.println("around 業(yè)務(wù)方法"); } }
5. 用AOP實現(xiàn)日志攔截
5.1 一般的實現(xiàn)
打印日志是AOP的一個常見應(yīng)用場景,我們可以對Controller層向外提供的接口做統(tǒng)一的日志攔截,用日志記錄請求參數(shù)、返回參數(shù)、請求時長以及異常信息,方便我們線上排查問題,下面是核心類LogAspect的實現(xiàn)
/** * 日志的切面 */ @Aspect @Component public class LogAspect { @Resource private IdWorker idWorker; @Pointcut("execution (public * com.yc.core.controller.*.*(..))") public void log(){} /** * 使用環(huán)繞增強實現(xiàn)日志打印 * @param joinPoint * @return * @throws Throwable */ @Around("log()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { //獲得執(zhí)行方法的類和名稱 String className = joinPoint.getTarget().getClass().getName(); //獲得方法名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getMethod().getName(); //獲得參數(shù) Object[] args = joinPoint.getArgs(); long requestId = idWorker.nextId(); //打印參數(shù) LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",params:" + JSONObject.toJSONString(args)); long startTime = System.currentTimeMillis(); //執(zhí)行業(yè)務(wù)方法 Object result = null; try { result = joinPoint.proceed(); } catch (Exception e) { LogHelper.writeErrLog(className, methodName, "requestId:" + requestId + ",異常啦:" + LogAspect.getStackTrace(e)); } long endTime = System.currentTimeMillis(); //打印結(jié)果 LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",耗時:" + (endTime - startTime) + "ms,result:" + JSONObject.toJSONString(result)); //返回 return result; } /** * 獲取異常的堆棧信息 * @param throwable * @return */ public static String getStackTrace(Throwable throwable) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); try { throwable.printStackTrace(pw); return sw.toString(); } finally { pw.close(); } } }
- 在proceed()方法之前,相當(dāng)于前置增強,收集類名、方法名、參數(shù),記錄開始時間,生成requestId
- 在proceed()方法之后,相當(dāng)于后置增強,并能獲取到返回值,計算耗時
- 在前置增強時,生成requestId,用于串聯(lián)多條日志
- 使用try、catch包裹proceed()方法,在catch中記錄異常日志
- 提供了getStackTrace方法獲取異常的堆棧信息,便于排查報錯詳細(xì)情況
5.2 僅攔截需要的方法
但是上面的日志是針對所有controller層中的方法進行了日志攔截,如果我們有些方法不想進行日志輸出,比如文件上傳的接口、大量數(shù)據(jù)返回的接口,這個時候定義切入點的時候可以使用@annotation描述符來匹配加了特定注解的方法,步驟如下:
1. 先定義一個日志注解Log
/** * 自定義日志注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log { }
2.用@annotation定義切入點
@Pointcut("@annotation(com.yc.core.annotation.Log)") public void logAnnotation(){}
3.在想做日志輸出的方法上使用注解Log
@Log @PostMapping(value = "testannotation") public AOPTestVO testannotation(@RequestBody AOPTestDTO aopTestDTO) { AOPTestVO aopTestVO = new AOPTestVO(); aopTestVO.setCode(1); aopTestVO.setMsg("哈哈哈"); return aopTestVO; }
這樣,我們就可以自定義哪些方法需要日志輸出了
5.3 requestId傳遞
后來有同事提到,如果這是針對Controller層的攔截,但是Service層也有自定義的日志輸出,怎么在Service層獲取到上述的requestId呢?
其實就是我們攔截之后,是否可以針對方法的參數(shù)進行修改呢?其實注意是看
result = joinPoint.proceed();
我們發(fā)現(xiàn)ProceedingJoinPoint還有另外一個帶有參數(shù)的proceed方法,定義如下:
public Object proceed(Object[] args) throws Throwable;
我們可以利用這個方法,在環(huán)繞增強中去增加requestId,這樣后面的增強方法或業(yè)務(wù)方法中就能獲取到這個requestId了。
首先,我們先定義一個基類AOPBaseDTO,只有一個屬性requestId
@Data @ApiModel("aop參數(shù)基類") public class AOPBaseDTO { @ApiModelProperty(value = "請求id", hidden = true) private long requestId; }
然后我們讓Controller層接口的參數(shù)AOPTestDTO繼承上述AOPBaseDTO,如下:
@Data @ApiModel("aop測試類") public class AOPTestDTO extends AOPBaseDTO{ @ApiModelProperty(value = "姓名") private String name; @ApiModelProperty(value = "年齡") private int age; }
最后在環(huán)繞的增強中添加上requestId,如下:
/** * 使用環(huán)繞增強實現(xiàn)日志打印 * @param joinPoint * @return * @throws Throwable */ @Around("logAnnotation()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { //獲得執(zhí)行方法的類和名稱 String className = joinPoint.getTarget().getClass().getName(); //獲得方法名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getMethod().getName(); //獲得參數(shù) Object[] args = joinPoint.getArgs(); long requestId = idWorker.nextId(); for(int i = 0; i < args.length; i++) { if (args[i] instanceof AOPBaseDTO) { //增加requestId ((AOPBaseDTO) args[i]).setRequestId(requestId); } } //打印參數(shù) LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",params:" + JSONObject.toJSONString(args)); long startTime = System.currentTimeMillis(); //執(zhí)行業(yè)務(wù)方法 Object result = null; try { result = joinPoint.proceed(args); } catch (Exception e) { LogHelper.writeErrLog(className, methodName, "requestId:" + requestId + ",異常啦:" + LogAspect.getStackTrace(e)); } long endTime = System.currentTimeMillis(); //打印結(jié)果 LogHelper.writeInfoLog(className, methodName, "requestId:" + requestId + ",耗時:" + (endTime - startTime) + "ms,result:" + JSONObject.toJSONString(result)); //返回 return result; }
我們運行起代碼,訪問一下,下面是運行結(jié)果:
可以看到,我們的業(yè)務(wù)方法中已經(jīng)能獲取到requestId,如果Service層需要,可以通過傳遞AOPTestDTO,從中獲取。
5.4 關(guān)于增強執(zhí)行的順序
- 1. 針對不同類型的增強,順序固定的,比如before在after前面
- 2. 針對同一切面的相同類型的增強,根據(jù)定義先后順序依次執(zhí)行
- 3. 針對不同切面的相同增強,可以通過使我們的切面實現(xiàn)Ordered接口,重寫getOrder方法,返回值最小,優(yōu)先級越高。注意的是before和after的相反的,before的優(yōu)先級越高越早執(zhí)行,after的優(yōu)先級越高,越晚執(zhí)行
6. 思考
- 1. 代理對象是什么時候創(chuàng)建的?
- 2. 當(dāng)存在多個不同類型增強時,執(zhí)行順序是怎么保證的?
- 3. 真正的業(yè)務(wù)方法是什么時候調(diào)用的,怎么做到只調(diào)用一次?
到此這篇關(guān)于Spring面向切面編程AOP詳情的文章就介紹到這了,更多相關(guān)Spring面向切面編程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
maven中springboot-maven-plugin的5種打包方式
本文主要介紹了maven中springboot-maven-plugin的5種打包方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-09-09logback FixedWindowRollingPolicy固定窗口算法重命名文件滾動策略
這篇文章主要介紹了FixedWindowRollingPolicy根據(jù)logback 固定窗口算法重命名文件滾動策略源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11UniApp?+?SpringBoot?實現(xiàn)支付寶支付和退款功能
這篇文章主要介紹了UniApp?+?SpringBoot?實現(xiàn)支付寶支付和退款功能,基本的?SpringBoot?的腳手架,可以去IDEA?自帶的快速生成腳手架插件,本文通過實例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-06-06一文搞清楚Java中Comparable和Comparator的區(qū)別
Java中的Comparable和Comparator都是用于集合排序的接口,但它們有明顯的區(qū)別,文中通過一些實例代碼詳細(xì)介紹了Java中Comparable和Comparator的區(qū)別,感興趣的同學(xué)跟著小編一起學(xué)習(xí)吧2023-05-05