AOP簡介
利用物件導向的方法可以很好的組織程式碼,也可以繼承的方式實現程式碼重用。但是專案中總是會出現一些重複的程式碼,並且不太方便使用繼承的方式把他們重用管理起來,比如說通用日誌列印,事務處理和安全檢查等。我們可以將這些程式碼封裝起來,做成通用模組,但是還是需要在程式碼中每處需要的地方進行顯示呼叫,使用起來不方便。這是時候就是利用AOP的時候。
AOP是一種程式設計正規化,用來解決特定的問題,不能解決所有問題,可以看做是OOP的補充,常見的程式設計正規化還有:
- 程式導向程式設計;
- 物件導向程式設計;
- 面向函式程式設計(函數語言程式設計);
- 事件驅動程式設計(GUI開發中比較常見);
- 面向切面程式設計
AOP的常見使用場景
- 效能監控,在方法呼叫前後記錄呼叫時間,方法執行太長或超時報警;
- 快取代理,快取某方法的返回值,下次執行該方法時,直接從快取裡獲取;
- 軟體破解,使用AOP修改軟體的驗證類的判斷邏輯;
- 記錄日誌,在方法執行前後記錄系統日誌;
- 工作流系統,工作流系統需要將業務程式碼和流程引擎程式碼混合在一起執行,那麼我們可以使用AOP將其分離,並動態掛接業務;
- 許可權驗證,方法執行前驗證是否有許可權執行當前方法,沒有則丟擲沒有許可權執行異常,由業務程式碼捕捉;
- 事務處理 。
Spring AOP相關概念
- AOP:這種在執行時(或者編譯時或者載入時),動態地將某些公共程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計;
- 切面(Aspect):A modularization of a concern that cuts across multiple classes。在Spring中切面就是一個標註@AspectJ的類,不要想得太複雜;
- 連線點(Joinpoint):方法執行過程中的某個點,是在應用執行過程中能夠插入切面的一個點。這個點可以是呼叫方法時、丟擲異常時、甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增新的行為;
- 通知(advice):描述切面要完成什麼工作,以及在什麼時間點進行工作;
- Pointcut:用來匹配一組連線點,並且pointcut會關聯advice,在pointcut匹配的連線點執行的時候,advice程式碼會被執行;
- Introduction
- Target object:被織入切面的物件;
- AOP proxy : 包裝了切面程式碼和target程式碼的物件,Spring中支援JDK動態代理和
CGLIB,預設使用JDK動態代理,但是如果被代理的類沒有實現介面,或者使用者強制使用CGLIB,那麼Spring會使用CGLIB代理; - Weaving:將切面程式碼新增到目的碼的過程,織入的型別有編譯時織入,載入時織入和執行時織入(Spring是執行時織入)
SpringAOP可以應用5種型別的通知:
- 前置通知(Before):在目標方法被呼叫之前呼叫通知功能。
- 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼。(不管執行是否成功都執行都執行)
- 返回通知(After-returning):在目標方法成功執行之後呼叫通知。
- 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知。
- 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為。
Spring AOP相關
開啟Aop
//自動選擇合適的AOP代理
//傳統xml這樣配置:<aop:aspectj-autoproxy/>
//exposeProxy = true屬性設定成true,意思是將動態生成的代理類expose到AopContext的ThreadLocal執行緒
//可以通過AopContext.currentProxy();獲取到生成的動態代理類。
//proxyTargetClass屬性設定動態代理使用JDK動態代理還是使用CGlib代理,設定成true是使用CGlib代理,false的話是使用JDK動態代理
//注意:如果使用Spring Boot的話,下面的配置可以不需要。AopAutoConfiguration這個自動配置類中已經自動開啟了AOP
//預設使用CGLIB動態代理,Spring Boot配置的優先順序高於下面的配置
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = false)
public class AopConfig {
}
如果使用傳統的配置方式的話,可按如下配置開啟AOP功能。
<aop:aspectj-autoproxy/>
定義一個Aspect
Aspects (classes annotated with @Aspect) can have methods and fields, the same as any other class. They can also contain pointcut, advice, and introduction (inter-type) declarations.
可以使用普通Bean的定義方式,或者加@Aspect註解的方式定義。一旦一個類被標註成切面類,它就不會成為其他切面的代理物件。
定義一個PointCut
切面表示式可以由指示器,萬用字元和運算子組成。
- 指示器(Designators)
- 匹配方法 execution() (重點掌握...)
- 匹配註解 @target() @args() @within() @annotation()
- 匹配包/型別 within()
- 匹配物件 this() bean() target()
- 匹配引數 args()
- Wildcards(萬用字元)
- *匹配任意數量的字元
- +匹配指定類及其子類
- .. 一般用於匹配任意引數的子包或引數
- Operators(運算子)
- && 與操作符
- || 或操作符
- ! 非操作符
下面給出一個定義PointCut的例子
package com.csx.demo.spring.boot.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
//PointCut匹配的方法必須是Spring中bean的方法
//Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合.
//下面定義的這些切入點就可以通過&& ||組合
private static Logger logger = LoggerFactory.getLogger(MyAspect.class);
//*:代表方法的返回值可以是任何型別
//整個表示式匹配controller包下面任何的的echo方法,方法入參樂意是任意
@Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(..))")
public void pointCut1(){}
//代表echo方法必須有一個引數 引數的型別可以是任意型別
@Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(*))")
public void pointCut2(){}
//代表echo方法必須有兩個引數,第一個型別任意,第二個型別必須是String
@Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(*,String))")
public void pointCut3(){}
//contrller包及其子包下面的任意類的任意方法
//需要注意的是with和@with都是正對包級別的
@Pointcut("within(com.csx.demo.spring.boot.controller..*)")
public void pointCut4(){}
//使用RestController這個註解標註任意類的任意方法
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
public void pointCut5(){}
//用法和@Within類似
@Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
public void pointCut10(){}
//MyService這個介面實現類的任何方法
//如果MyService是一個類的話,那匹配這個類內部的所有方法
@Pointcut("this(com.csx.demo.spring.boot.service.MyService)")
public void pointCut6(){}
@Pointcut("this(com.csx.demo.spring.boot.service.MyServiceImpl)")
public void pointCut7(){}
//某個bean內部的所有方法
@Pointcut("bean(myServiceImpl)")
public void pointCut8(){}
//@within和@target針對類的註解,@annotation是針對方法的註解
//匹配任何標註GetMaping註解的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void pointCut9(){}
//匹配只有一個引數,引數型別是String的方法
@Pointcut("args(String)")
public void pointCut11(){}
@Before("pointCut1()")
public void befor(){
logger.info("前置通知vvvv...");
logger.info("我要做些事情...");
}
@After("pointCut1()")
public void after(){
logger.info("後置通知");
}
@AfterReturning("pointCut1()")
public void afterReturn(){
logger.info("後置返回");
}
//目標方法丟擲相關異常後通知
@AfterThrowing("pointCut1()")
public void afterThrowing(){
logger.info("後置異常");
}
@Around("pointCut1()")
public void around(ProceedingJoinPoint point) throws Throwable {
logger.info("環繞通知...");
logger.info("我要做些事情...");
point.proceed();
logger.info("結束環繞通知");
}
}