一优幸、Aop關(guān)鍵術(shù)語個人理解
1.1 Joinpoint(連接點)
所謂連接點是指那些被攔截到的點吨拍。在spring中,這些點指的是方法,因為spring只支持方法類型的連接點。(通俗理解:業(yè)務(wù)層接口的所有方法都叫連接點)
1.2 Pointcut(切入點)
所謂切入點是指我們要對哪些Joinpoint進行攔截的定義网杆。 (通俗理解:被增強的業(yè)務(wù)層接口的方法叫切入點)
這樣看來羹饰,連接點不一定是切入點,但切入點一定是連接點跛璧。
1.3 Advice(通知/增強)
所謂通知是指攔截到Joinpoint之后所要做的事情就是通知严里。
通知的類型:前置通知、后置通知追城、異常通知刹碾、最終通知、環(huán)繞通知座柱。
通知的查找方法:找到invoke方法中明確調(diào)用業(yè)務(wù)層那行代碼迷帜,在其之前執(zhí)行的就是前置通知,在其之后執(zhí)行的就是后置通知色洞,在catch中的就是異常通知戏锹,在finally中的就是最終通知。整個的invoke方法執(zhí)行就是環(huán)繞通知火诸。
注意:返回通知和異常通知只能有一個會被執(zhí)行锦针,因為發(fā)生異常執(zhí)行異常通知,然后就不會繼續(xù)向下執(zhí)行置蜀,自然后置通知也就不會被執(zhí)行奈搜,反之亦然。
1.4 Introduction(引介)
引介是一種特殊的通知在不修改類代碼的前提下, Introduction可以在運行期為類動態(tài)地添加一些方法或Field盯荤。
1.5 Target(目標(biāo)對象)
代理的目標(biāo)對象馋吗。(被代理的對象)
1.6 Weaving(織入):
織入是指把增強應(yīng)用到目標(biāo)對象來創(chuàng)建新的代理對象的過程。 spring采用動態(tài)代理織入秋秤,而AspectJ采用編譯期織入和類裝載期織入宏粤。
1.7 Proxy(代理)
一個類被AOP織入增強后,就產(chǎn)生一個結(jié)果代理類灼卢。(代理對象)
1.8 Aspect(切面):
切面是切入點和通知(引介)的結(jié)合绍哎。 (通俗理解:建立切入點方法和通知方法在執(zhí)行調(diào)用的對應(yīng)關(guān)系就是切面)
二、Pointcut表達(dá)式
2.1 Pointcut表達(dá)式類型
標(biāo)準(zhǔn)的AspectJ Aop的pointcut的表達(dá)式類型是很豐富的鞋真,但是Spring Aop只支持其中的9種蛇摸,外加Spring Aop自己擴充的一種一共是10種類型的表達(dá)式,分別如下灿巧。
- execution:一般用于指定方法的執(zhí)行赶袄,用的最多揽涮。
- within:指定某些類型的全部方法執(zhí)行,也可用來指定一個包饿肺。
- this:Spring Aop是基于動態(tài)代理的蒋困,生成的bean也是一個代理對象,this就是這個代理對象敬辣,當(dāng)這個對象可以轉(zhuǎn)換為指定的類型時雪标,對應(yīng)的切入點就是它了,Spring Aop將生效溉跃。
- target:當(dāng)被代理的對象可以轉(zhuǎn)換為指定的類型時村刨,對應(yīng)的切入點就是它了,Spring Aop將生效撰茎。
- args:當(dāng)執(zhí)行的方法的參數(shù)是指定類型時生效嵌牺。
- @target:當(dāng)代理的目標(biāo)對象上擁有指定的注解時生效。
- @args:當(dāng)執(zhí)行的方法參數(shù)類型上擁有指定的注解時生效龄糊。
- @within:與@target類似逆粹,看官方文檔和網(wǎng)上的說法都是@within只需要目標(biāo)對象的類或者父類上有指定的注解,則@within會生效炫惩,而@target則是必須是目標(biāo)對象的類上有指定的注解僻弹。而根據(jù)筆者的測試這兩者都是只要目標(biāo)類或父類上有指定的注解即可。
- @annotation:當(dāng)執(zhí)行的方法上擁有指定的注解時生效他嚷。
- bean:當(dāng)調(diào)用的方法是指定的bean的方法時生效蹋绽。
Pointcut定義時,還可以使用&&筋蓖、||卸耘、! 這三個運算。進行邏輯運算扭勉。可以把各種條件組合起來使用苛聘。
2.2 Pointcut表達(dá)式使用示例
2.2.1 execution
execution是使用的最多的一種Pointcut表達(dá)式涂炎,表示某個方法的執(zhí)行,其標(biāo)準(zhǔn)語法如下设哗。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- 修飾符匹配(modifier-pattern?)
- 返回值匹配(ret-type-pattern)可以為*表示任何返回值,全路徑的類名等
- 類路徑匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以set開頭的所有方法
- 參數(shù)匹配((param-pattern))可以指定具體的參數(shù)類型唱捣,多個參數(shù)間用“,”隔開,各個參數(shù)也可以用“”來表示匹配任意類型的參數(shù)网梢,如(String)表示匹配一個String參數(shù)的方法震缭;(,String) 表示匹配有兩個參數(shù)的方法,第一個參數(shù)可以是任意類型战虏,而第二個參數(shù)是String類型拣宰;可以用(…)表示零個或多個任意參數(shù)
- 異常類型匹配(throws-pattern?)
- 其中后面跟著“?”的是可選項
下面看幾個例子
//表示匹配所有方法
1)execution(* *(..))
//表示匹配com.fsx.run.UserService中所有的公有方法
2)execution(public * com.fsx.run.UserService.*(..))
//表示匹配com.fsx.run包及其子包下的所有方法
3)execution(* com.fsx.run..*.*(..))
Pointcut定義時党涕,還可以使用&&、||巡社、! 這三個運算膛堤。進行邏輯運算
// 簽名:消息發(fā)送切面
@Pointcut("execution(* com.fsx.run.MessageSender.*(..))")
private void logSender(){}
// 簽名:消息接收切面
@Pointcut("execution(* com.fsx.run.MessageReceiver.*(..))")
private void logReceiver(){}
// 只有滿足發(fā)送 或者 接收 這個切面都會切進去
@Pointcut("logSender() || logReceiver()")
private void logMessage(){}
這個例子中,logMessage()將匹配任何MessageSender和MessageReceiver中的任何方法晌该。
當(dāng)我們的切面很多的時候肥荔,我們可以把所有的切面放到單獨的一個類去,進行統(tǒng)一管理朝群,比如下面:
//集中管理所有的切入點表達(dá)式
public class Pointcuts {
@Pointcut("execution(* *Message(..))")
public void logMessage(){}
@Pointcut("execution(* *Attachment(..))")
public void logAttachment(){}
@Pointcut("execution(* *Service.*(..))")
public void auth(){}
}
這樣別的使用時燕耿,采用全類名+方法名的方式
@Before("com.fsx.run.Pointcuts.logMessage()")
public void before(JoinPoint joinPoint) {
System.out.println("Logging before " + joinPoint.getSignature().getName());
}
2.2.2 within
within是用來指定類型的,指定類型中的所有方法將被攔截姜胖。
// AService下面所有外部調(diào)用方法誉帅,都會攔截。備注:只能是AService的方法谭期,子類不會攔截的
@Pointcut("within(com.fsx.run.service.AService)")
public void pointCut() {
}
所以此處需要注意:上面寫的是AService接口堵第,是達(dá)不到攔截效果的,只能寫實現(xiàn)類:
//此處只能寫實現(xiàn)類
@Pointcut("within(com.fsx.run.service.impl.AServiceImpl)")
public void pointCut() {
}
匹配包以及子包內(nèi)的所有類:
@Pointcut("within(com.fsx.run.service..*)")
public void pointCut() {
}
2.2.3 this
Spring Aop是基于代理的隧出,this就表示代理對象踏志。this類型的Pointcut表達(dá)式的語法是this(type),當(dāng)生成的代理對象可以轉(zhuǎn)換為type指定的類型時則表示匹配胀瞪≌胗啵基于JDK接口的代理和基于CGLIB的代理生成的代理對象是不一樣的。(注意和上面within的區(qū)別)
// 這樣子凄诞,就可以攔截到AService所有的子類的所有外部調(diào)用方法
@Pointcut("this(com.fsx.run.service.AService*)")
public void pointCut() {
}
2.2.4 target
Spring Aop是基于代理的圆雁,target則表示被代理的目標(biāo)對象。當(dāng)被代理的目標(biāo)對象可以被轉(zhuǎn)換為指定的類型時則表示匹配帆谍。
注意:和上面不一樣伪朽,這里是target,因此如果要切入汛蝙,只能寫實現(xiàn)類了
@Pointcut("target(com.fsx.run.service.impl.AServiceImpl)")
public void pointCut() {
}
2.2.5 args
args用來匹配方法參數(shù)的烈涮。
- 1、“args()”匹配任何不帶參數(shù)的方法窖剑。
- 2坚洽、“args(java.lang.String)”匹配任何只帶一個參數(shù),而且這個參數(shù)的類型是String的方法。
- 3、“args(…)”帶任意參數(shù)的方法剪侮。
- 4跷究、“args(java.lang.String,…)”匹配帶任意個參數(shù)跳昼,但是第一個參數(shù)的類型是String的方法般甲。
- 5、“args(…,java.lang.String)”匹配帶任意個參數(shù)庐舟,但是最后一個參數(shù)的類型是String的方法欣除。
@Pointcut("args()")
public void pointCut() {
}
這個匹配的范圍非常廣,所以一般和別的表達(dá)式結(jié)合起來使用
2.2.6 @target
@target匹配當(dāng)被代理的目標(biāo)對象對應(yīng)的類型及其父類型上擁有指定的注解時挪略。
//能夠切入類上(非方法上)標(biāo)準(zhǔn)了MyAnno注解的所有外部調(diào)用方法
@Pointcut("@target(com.fsx.run.anno.MyAnno)")
public void pointCut() {
}
2.2.7 @args
@args匹配被調(diào)用的方法上含有參數(shù)历帚,且對應(yīng)的參數(shù)類型上擁有指定的注解的情況。
例如:
// 匹配**方法參數(shù)類型上**擁有MyAnno注解的方法調(diào)用杠娱。如我們有一個方法add(MyParam param)接收一個MyParam類型的參數(shù)挽牢,而MyParam這個類是擁有注解MyAnno的,則它可以被Pointcut表達(dá)式匹配上
@Pointcut("@args(com.fsx.run.anno.MyAnno)")
public void pointCut() {
}
2.2.8 @within:
@within用于匹配被代理的目標(biāo)對象對應(yīng)的類型或其父類型擁有指定的注解的情況摊求,但只有在調(diào)用擁有指定注解的類上的方法時才匹配禽拔。
“@within(com.fsx.run.anno.MyAnno)”匹配被調(diào)用的方法聲明的類上擁有MyAnno注解的情況。比如有一個ClassA上使用了注解MyAnno標(biāo)注室叉,并且定義了一個方法a()睹栖,那么在調(diào)用ClassA.a()方法時將匹配該Pointcut;如果有一個ClassB上沒有MyAnno注解茧痕,但是它繼承自ClassA野来,同時它上面定義了一個方法b(),那么在調(diào)用ClassB().b()方法時不會匹配該Pointcut踪旷,但是在調(diào)用ClassB().a()時將匹配該方法調(diào)用曼氛,因為a()是定義在父類型ClassA上的,且ClassA上使用了MyAnno注解令野。但是如果子類ClassB覆寫了父類ClassA的a()方法舀患,則調(diào)用ClassB.a()方法時也不匹配該Pointcut。
2.2.9 @annotation:使用得也比較多
@annotation用于匹配方法上擁有指定注解的情況气破。
// 可以匹配所有方法上標(biāo)有此注解的方法
@Pointcut("@annotation(com.fsx.run.anno.MyAnno)")
public void pointCut() {
}
我們還可以這么寫聊浅,非常方便的獲取到方法上面的注解
@Before("@annotation(myAnno)")
public void doBefore(JoinPoint joinPoint, MyAnno myAnno) {
System.out.println(myAnno); //@com.fsx.run.anno.MyAnno()
System.out.println("AOP Before Advice...");
}
2.2.10 bean
這是Spring增加的一種方法,spring獨有现使,bean用于匹配當(dāng)調(diào)用的是指定的Spring的某個bean的方法時低匙。
- 1、“bean(abc)”匹配Spring Bean容器中id或name為abc的bean的方法調(diào)用朴下。
- 2努咐、“bean(user*)”匹配所有id或name為以user開頭的bean的方法調(diào)用苦蒿。
// 這個就能切入到AServiceImpl類的素有的外部調(diào)用的方法里
@Pointcut("bean(AServiceImpl)")
public void pointCut() {
}
2.3 類型匹配語法
-
*
:匹配任何數(shù)量字符殴胧; -
...
:匹配任何數(shù)量字符的重復(fù),如在類型模式中匹配任何數(shù)量子包;而在方法參數(shù)模式中匹配任何數(shù)量參數(shù)团滥。
+
:匹配指定類型的子類型竿屹;僅能作為后綴放在類型模式后邊。
java.lang.String 匹配String類型灸姊;
java.*.String 匹配java包下的任何“一級子包”下的String類型拱燃; 如匹配java.lang.String,但不匹配java.lang.ss.String
java..* 匹配java包及任何子包下的任何類型力惯。如匹配java.lang.String碗誉、java.lang.annotation.Annotation
java.lang.*ing 匹配任何java.lang包下的以ing結(jié)尾的類型;
java.lang.Number+ 匹配java.lang包下的任何Number的子類型父晶; 如匹配java.lang.Integer哮缺,也匹配java.math.BigInteger
2.4 表達(dá)式的組合
表達(dá)式的組合其實就是對應(yīng)的表達(dá)式的邏輯運算,與甲喝、或尝苇、非〔号郑可以通過它們把多個表達(dá)式組合在一起糠溜。
- 1、“bean(userService) && args()”匹配id或name為userService的bean的所有無參方法直撤。
- 2非竿、“bean(userService) || @annotation(MyAnnotation)”匹配id或name為userService的bean的方法調(diào)用,或者是方法上使用了MyAnnotation注解的方法調(diào)用谊惭。
- 3汽馋、“bean(userService) && !args()”匹配id或name為userService的bean的所有有參方法調(diào)用。
三使用案例
3.1 引入aop相關(guān)的依賴
<!--aop相關(guān)的依賴引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 環(huán)繞通知使用
@Component
@Aspect
@Slf4j
public class FeignCallDetailAspect {
// 定義切點Pointcut
@Pointcut("execution(* com.yibo.order.service..api.open.*.*(..))")
public void excludeService() {
}
@Around("excludeService()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String url = request.getRequestURL().toString();
String method = request.getMethod();
String uri = request.getRequestURI();
String queryString = request.getQueryString();
//log.info("請求開始, 各個參數(shù), url: {}, method: {}, uri: {}, params: {}", url, method, uri, queryString);
Object[] args = joinPoint.getArgs();
String params = "";
//獲取請求參數(shù)集合并進行遍歷拼接
if(args.length>0){
if("POST".equals(method)){
Object object = args[0];
//請求參數(shù)
Map<String, Object> map = getKeyAndValue(object);
}else if("GET".equals(method)){
params = queryString;
}
}
// result的值就是被攔截方法的返回值
Object result = joinPoint.proceed();
log.info("請求結(jié)束圈盔,controller的返回值是 " + JSON.toJSONString(result));
return result;
}
/**
* 獲取接口參數(shù)
* @param obj
* @return
*/
public static Map<String, Object> getKeyAndValue(Object obj) {
Map<String, Object> map = new HashMap<>();
// 得到類對象
Class userCla = (Class) obj.getClass();
/* 得到類中的所有屬性集合 */
Field[] fs = userCla.getDeclaredFields();
for (int i = 0; i < fs.length; i++) {
Field f = fs[i];
f.setAccessible(true); // 設(shè)置些屬性是可以訪問的
Object val = new Object();
try {
val = f.get(obj);
// 得到此屬性的值
map.put(f.getName(), val);// 設(shè)置鍵值
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
}
3.3 內(nèi)網(wǎng)調(diào)用接口自動賦值app security的AOP方案
/**
* app授權(quán)配置組件
*
**/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Component
public class AppSecurityConfigComponent {
/**
應(yīng)用id
*/
@Value("${xundu.app-id}")
private String appId;
/**
版本號
*/
@Value("${xundu.app.version}")
private String version;
/**
密鑰
*/
@Value("${xundu.app.security-key}")
private String securityKey;
public void setSecurityProperties(SecurityBaseForm securityForm) {
securityForm.setVersion(this.version);
securityForm.setAppId(this.appId);
securityForm.setSecurityKey(this.securityKey);
}
}
/**
* 解決調(diào)用內(nèi)網(wǎng)接口自動賦值app security的AOP方案
*
*/
@Component
@Aspect
public class SecurityBaseFormAspect {
@Resource
private AppSecurityConfigComponent securityKeyConfigComponent;
/**
* 切面的聲明
* 影響到的方法:
* 1: 被{@link org.springframework.cloud.openfeign.FeignClient}標(biāo)記的類
* 2: 參數(shù)為{@link SecurityBaseForm}的所有方法
*/
@Pointcut("@within(org.springframework.cloud.openfeign.FeignClient) && args(xundu.app.api.validate.SecurityBaseForm)")
public void pointCut() {
}
/**
* 定義Advice
* 前置處理豹芯, 對請求的參數(shù)進行賦值:將配置的App配置賦值到SecurityBaseForm中
*
* @param joinPoint 連接點對象
*/
@Before("pointCut()")
public void handle(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
Arrays.stream(args).filter(arg -> arg instanceof SecurityBaseForm)
.forEach(arg -> securityKeyConfigComponent
.setSecurityProperties((SecurityBaseForm) arg));
}
}
四、理解AOP
4.1 什么是AOP
AOP(Aspect Oriented Programming)驱敲,面向切面思想铁蹈,是Spring的三大核心思想之一(兩外兩個:IOC-控制反轉(zhuǎn)、DI-依賴注入)众眨。
那么AOP為何那么重要呢握牧?在我們的程序中,經(jīng)常存在一些系統(tǒng)性的需求娩梨,比如權(quán)限校驗沿腰、日志記錄、統(tǒng)計等狈定,這些代碼會散落穿插在各個業(yè)務(wù)邏輯中颂龙,非常冗余且不利于維護习蓬。例如下面這個示意圖:
有多少業(yè)務(wù)操作,就要寫多少重復(fù)的校驗和日志記錄代碼措嵌,這顯然是無法接受的躲叼。當(dāng)然,用面向?qū)ο蟮乃枷肫蟪玻覀兛梢园堰@些重復(fù)的代碼抽離出來枫慷,寫成公共方法,就是下面這樣:
這樣浪规,代碼冗余和可維護性的問題得到了解決或听,但每個業(yè)務(wù)方法中依然要依次手動調(diào)用這些公共方法,也是略顯繁瑣笋婿。有沒有更好的方式呢神帅?有的,那就是AOP萌抵,AOP將權(quán)限校驗找御、日志記錄等非業(yè)務(wù)代碼完全提取出來,與業(yè)務(wù)代碼分離绍填,并尋找節(jié)點切入業(yè)務(wù)代碼中:
4.2 AOP體系與概念
簡單地去理解霎桅,其實AOP要做三類事:
- 在哪里切入,也就是權(quán)限校驗等非業(yè)務(wù)操作在哪些業(yè)務(wù)代碼中執(zhí)行讨永。
- 在什么時候切入滔驶,是業(yè)務(wù)代碼執(zhí)行前還是執(zhí)行后。
- 切入后做什么事卿闹,比如做權(quán)限校驗揭糕、日志記錄等。
因此锻霎,AOP的體系可以梳理為下圖:
一些概念詳解:
Pointcut:切點著角,決定處理如權(quán)限校驗、日志記錄等在何處切入業(yè)務(wù)代碼中(即織入切面)旋恼。切點分為execution方式和annotation方式吏口。前者可以用路徑表達(dá)式指定哪些類織入切面,后者可以指定被哪些注解修飾的代碼織入切面冰更。
Advice:處理产徊,包括處理時機和處理內(nèi)容。處理內(nèi)容就是要做什么事蜀细,比如校驗權(quán)限和記錄日志舟铜。處理時機就是在什么時機執(zhí)行處理內(nèi)容,分為前置處理(即業(yè)務(wù)代碼執(zhí)行前)奠衔、后置處理(業(yè)務(wù)代碼執(zhí)行后)等谆刨。
Aspect:切面奕谭,即Pointcut和Advice。
Joint point:連接點痴荐,是程序執(zhí)行的一個點。例如官册,一個方法的執(zhí)行或者一個異常的處理生兆。在 Spring AOP 中,一個連接點總是代表一個方法執(zhí)行膝宁。
Weaving:織入鸦难,就是通過動態(tài)代理,在目標(biāo)對象方法中執(zhí)行處理內(nèi)容的過程员淫。
參考:
https://blog.csdn.net/weixin_44830331/article/details/119169791
https://www.codeleading.com/article/718121000/
https://www.cnblogs.com/satire/p/14874827.html
https://blog.csdn.net/weixin_46228112/article/details/123930413