導言
AOP(Aspect Orient Programming),作為面向對象程式設計的一種補充,廣泛應用于處理一些具有橫切性質的系統級服務,如日志收集、事務管理、安全檢查、緩存、對象池管理等。AOP實作的關鍵就在于AOP架構自動建立的AOP代理,AOP代理則可分為靜态代理和動态代理兩大類,其中靜态代理是指使用AOP架構提供的指令進行編譯,進而在編譯階段就可生成 AOP 代理類,是以也稱為編譯時增強;而動态代理則在運作時借助于
JDK動态代理
、
CGLIB
等在記憶體中“臨時”生成AOP動态代理類,是以也被稱為運作時增強。
面向切面的程式設計(AOP) 是一種程式設計範式,旨在通過允許橫切關注點的分離,提高子產品化。AOP提供切面來将跨越對象關注點子產品化。雖然現在可以獲得許多AOP架構,但在這裡我們要區分的隻有兩個流行的架構:Spring AOP和AspectJ。
關鍵概念
Aspect
Aspect被翻譯方面或者切面,相當于OOP中的類,就是封裝用于橫插入系統的功能。例如日志、事務、安全驗證等。
JoinPoint
JoinPoint(連接配接點)是AOP中的一個重要的關鍵概念。JoinPoint可以看做是程式運作時的一個執行點。打個比方,比如執行System.out.println("Hello")這個函數,println()就是一個joinpoint;再如給一個變量指派也是一個joinpoint;還有最常用的for循環,也是一個joinpoint。
理論上說,一個程式中很多地方都可以被看做是JoinPoint,但是AspectJ中,隻有下面所示的幾種執行點被認為是JoinPoint:
表1 JoinPoint的類型
說明 | 示例 | |
---|---|---|
method call | 函數調用 | 比如調用Logger.info(),這是一處JoinPoint |
method execution | 函數執行 | 比如Logger.info()的執行内部,是一處JoinPoint。注意它和method call的差別。method call是調用某個函數的地方。而execution是某個函數執行的内部。 |
constructor call | 構造函數調用 | 和method call類似 |
constructor execution | 構造函數執行 | 和method execution類似 |
field get | 擷取某個變量 | 比如讀取User.name成員 |
field set | 設定某個變量 | 比如設定User.name成員 |
pre-initialization | Object在構造函數中做得一些工作。 | |
initialization | Object在構造函數中做得工作 | |
static initialization | 類初始化 | 比如類的static{} |
handler | 異常處理 | 比如try catch(xxx)中,對應catch内的執行 |
advice execution | 這個是AspectJ的内容 |
這裡列出了AspectJ所認可的JoinPoint的類型。實際上,連接配接點也就是你想把新的代碼插在程式的哪個地方,是插在構造方法中,還是插在某個方法調用前,或者是插在某個方法中,這個地方就是JoinPoint,當然,不是所有地方都能給你插的,隻有能插的地方,才叫JoinPoint。
PointCut
PointCut通俗地翻譯為切入點,一個程式會有多個Join Point,即使同一個函數,也還分為call和execution類型的Join Point,但并不是所有的Join Point都是我們關心的,Pointcut就是提供一種使得開發者能夠選擇自己需要的JoinPoint的方法。PointCut分為
call
execution
target
this
within
等關鍵字。與joinPoint相比,pointcut就是一個具體的切點。
Advice
Advice翻譯為通知或者增強(Advisor),就是我們插入的代碼以何種方式插入,相當于OOP中的方法,有Before、After以及Around。
-
Before
前置通知用于将切面代碼插入方法之前,也就是說,在方法執行之前,會首先執行前置通知裡的代碼.包含前置通知代碼的類就是切面。
-
After
後置通知的代碼在調用被攔截的方法後調用。
-
Around
環繞通知能力最強,可以在方法調用前執行通知代碼,可以決定是否還調用目标方法。也就是說它可以控制被攔截的方法的執行,還可以控制被攔截方法的傳回值。
Target
Target指的是需要切入的目标類或者目标接口。
Proxy
Proxy是代理,AOP工作時是通過代理對象來通路目标對象。其實AOP的實作是通過動态代理,離不開代理模式,是以必須要有一個代理對象。
Weaving
Weaving即織入,在目标對象中插入切面代碼的過程就叫做織入。
AspectJ
AspectJ的介紹
AspectJ是一個面向切面的架構,他定義了AOP的一些文法,有一個專門的位元組碼生成器來生成遵守java規範的 class檔案。
AspectJ的通知類型不僅包括我們之前了解過的三種通知:前置通知、後置通知、環繞通知,在Aspect中還有異常通知以及一種最終通知即無論程式是否正常執行,最終通知的代碼會得到執行。
AspectJ提供了一套自己的表達式語言即切點表達式,切入點表達式可以辨別切面織入到哪些類的哪些方法當中。隻要把切面的實作配置好,再把這個切入點表達式寫好就可以了,不需要一些額外的xml配置。
切點表達式文法:
execution(
modifiers-pattern? //通路權限比對 如public、protected
ret-type-pattern //傳回值類型比對
declaring-type-pattern? //全限定性類名
name-pattern(param-pattern) //方法名(參數名)
throws-pattern? //抛出異常類型
)
注意:
1. 中間以空格隔開,有問号的屬性表示可以省略。
2. 表達式中特殊符号說明:
- a:
代表0到多個任意字元,通常用作某個包下面的某些類以及某些方法。*
- b:
放在方法參數中,代表任意個參數,放在包名後面表示目前包及其所有子包路徑。..
- c:
放在類名後,表示目前類及其子類,放在接口後,表示目前接口及其實作類。+
表2 方法表達式
表達式 | 含義 |
---|---|
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結尾的類型 |
java.lang.Number+ | 比對java.lang包下的任何Number的自類型,如比對java.lang.Integer,也比對java.math.BigInteger |
表3 參數表達式
參數 | |
---|---|
() | 表示方法沒有任何參數 |
(..) | 表示比對接受任意個參數的方法 |
(..,java.lang.String) | 表示比對接受java.lang.String類型的參數結束,且其前邊可以接受有任意個參數的方法 |
(java.lang.String,..) | 表示比對接受java.lang.String類型的參數開始,且其後邊可以接受任意個參數的方法 |
(*,java.lang.String) | 表示比對接受java.lang.String類型的參數結束,且其前邊接受有一個任意類型參數的方法 |
舉個栗子:
execution(public * com.zhoujunwen.service.*.*(..))
,該表達式表示com.zhoujunwen.service包下的public通路權限的任意類的任意方法。
AspectJ的安裝以及常用指令
AspectJ下載下傳位址(
http://www.eclipse.org/aspectj/downloads.php),在下載下傳頁面選擇合适的版本下載下傳,目前最新穩定版是1.9.1。下載下傳完之後雙加jar包安裝,安裝界面如下:
安裝目錄用tree指令可以看到如下結構(省去doc目錄):
├── LICENSE-AspectJ.html
├── README-AspectJ.html
├── bin
│ ├── aj
│ ├── aj5
│ ├── ajbrowser
│ ├── ajc
│ └── ajdoc
└── lib
├── aspectjrt.jar
├── aspectjtools.jar
├── aspectjweaver.jar
└── org.aspectj.matcher.jar
42 directories, 440 files
- bin:存放aj、aj5、ajc、ajdoc、ajbrowser等指令,其中ajc指令最常用,它的作用類似于javac。
- doc:存放了AspectJ的使用說明、參考手冊、API文檔等文檔。
- lib:該路徑下的4個JAR檔案是AspectJ的核心類庫。
注意安裝完成後,需要配置将
aspectjrt.jar
配置到CLASSPATH中,并且将
bin
目錄配置到PATH中。下面以MacOs配置為例:
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:/Users/yourname/Documents/software/aspectj1.9.1/lib/aspectjrt.jar
M2_HOME=/Users/yourname/Documents/software/apache-maven-3.5.0
PATH=$JAVA_HOME/bin:$M2_HOME/bin:/usr/local/bin:/Users/yourname/Documents/software/aspectj1.9.1/bin:$PATH
注意:其中
/Users/yourname/Documents/software/aspectj1.9.1/lib/aspectjrt.jar
替換為自己安裝AspectJ的路徑的lib,
/Users/yourname/Documents/software/aspectj1.9.1/bin
替換為安裝AspectJ的bin目錄
AspectJ的demo
驗證AspectJ的切面功能,寫個單純的AspectJ的demo,實作方法日志埋點,在方法後增強。
業務代碼(AuthorizeService.java):
package com.zhoujunwen.aop;
/**
* 不用太過于較真業務邏輯的處理,大概意思大家懂就好。
* @author zhoujunwen
* @version 1.0.0
*/
public class AuthorizeService {
private static final String USERNAME = "zhoujunwen";
private static final String PASSWORD = "123456";
public void login(String username, String password) {
if (username == null || username.length() == 0) {
System.out.print("使用者名不能為空");
return;
}
if (password == null || password.length() == 0) {
System.out.print("使用者名不能為空");
return;
}
if (!USERNAME.equals(username) || !PASSWORD.equals(password)) {
System.out.print("使用者名或者密碼不對");
return;
}
System.out.print("登入成功");
}
public static void main(String[] args) {
AuthorizeService as = new AuthorizeService();
as.login("zhoujunwen", "123456");
}
}
日志埋點切面邏輯(LogAspect.java):
package com.zhoujunwen.aop;
public aspect LogAspect {
pointcut logPointcut():execution(void AuthorizeService.login(..));
after():logPointcut(){
System.out.println("****處理日志****");
}
}
将上述兩個檔案檔案放置在同一個目錄,在目前目錄下執行acj編譯和織入指令:
ajc -d . AuthorizeService.java LogAspect.java
如果配置一切OK的話,不會出現異常或者錯誤,并在目前目錄生成
com/zhoujunwen/aop/AuthorizeService.class
和
com/zhoujunwen/aop/LogAspect.class
兩個位元組碼檔案,執行tree(自己編寫的類似Linux的tree指令)指令檢視目錄結構:
zhoujunwendeMacBook-Air:aop zhoujunwen$ tree
.
├── AuthorizeService.java
├── LogAspect.java
└── com
└── zhoujunwen
└── aop
├── AuthorizeService.class
└── LogAspect.class
3 directories, 4 files
最後執行java執行指令:
java com/zhoujunwen/aop/AuthorizeService
輸出日志内容:
登入成功處理日志
ajc可以了解為javac指令,都用于編譯Java程式,差別是ajc指令可識别AspectJ的文法;我們可以将ajc當成一個增強版的javac指令。執行ajc指令後的AuthorizeService.class 檔案不是由原來的AuthorizeService.java檔案編譯得到的,該AuthorizeService.class裡新增了列印日志的内容——這表明AspectJ在編譯時“自動”編譯得到了一個新類,這個新類增強了原有的AuthorizeService.java類的功能,是以AspectJ通常被稱為編譯時增強的AOP架構。
為了驗證上述的結論,我們用javap指令反編譯AuthorizeService.class檔案。javap是Java class檔案分解器,可以反編譯(即對javac編譯的檔案進行反編譯),也可以檢視java編譯器生成的位元組碼。用于分解class檔案。
javap -p -c com/zhoujunwen/aop/AuthorizeService.class
輸出内容如下,在login方法的code為0、3以及91、94的地方,會發現
invokestatic
com/zhoujunwen/aop/LogAspect
的代碼,這說明上面的結論是正确的。
Compiled from "AuthorizeService.java"
public class com.zhoujunwen.aop.AuthorizeService {
private static final java.lang.String USERNAME;
private static final java.lang.String PASSWORD;
public com.zhoujunwen.aop.AuthorizeService();
Code:
0: aload_0
1: invokespecial #16 // Method java/lang/Object."<init>":()V
4: return
public void login(java.lang.String, java.lang.String);
Code:
0: invokestatic #70 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
3: invokevirtual #76 // Method com/zhoujunwen/aop/LogAspect.ajc$before$com_zhoujunwen_aop_LogAspect$2$9fd5dd97:()V
6: aload_1
7: ifnull 17
10: aload_1
11: invokevirtual #25 // Method java/lang/String.length:()I
14: ifne 28
17: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #37 // String 使用者名不能為空
22: invokevirtual #39 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
25: goto 99
28: aload_2
29: ifnull 39
32: aload_2
33: invokevirtual #25 // Method java/lang/String.length:()I
36: ifne 50
39: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
42: ldc #37 // String 使用者名不能為空
44: invokevirtual #39 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
47: goto 99
50: ldc #8 // String zhoujunwen
52: aload_1
53: invokevirtual #45 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq 68
59: ldc #11 // String 123456
61: aload_2
62: invokevirtual #45 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
65: ifne 79
68: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
71: ldc #49 // String 使用者名或者密碼不對
73: invokevirtual #39 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
76: goto 99
79: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
82: ldc #51 // String 登入成功
84: invokevirtual #39 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
87: goto 99
90: astore_3
91: invokestatic #70 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
94: invokevirtual #73 // Method com/zhoujunwen/aop/LogAspect.ajc$after$com_zhoujunwen_aop_LogAspect$1$9fd5dd97:()V
97: aload_3
98: athrow
99: invokestatic #70 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
102: invokevirtual #73 // Method com/zhoujunwen/aop/LogAspect.ajc$after$com_zhoujunwen_aop_LogAspect$1$9fd5dd97:()V
105: return
Exception table:
from to target type
6 90 90 Class java/lang/Throwable
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/zhoujunwen/aop/AuthorizeService
3: dup
4: invokespecial #57 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #8 // String zhoujunwen
11: ldc #11 // String 123456
13: invokevirtual #58 // Method login:(Ljava/lang/String;Ljava/lang/String;)V
16: return
}
SpringAOP
Spring AOP介紹
Spring AOP也是對目标類增強,生成代理類。但是與AspectJ的最大差別在于——Spring AOP的運作時增強,而AspectJ是編譯時增強。
dolphin叔叔文章中寫道自己曾經誤以為AspectJ是Spring AOP的一部分,我想大多數人都沒有弄清楚AspectJ和Spring AOP的關系。
Spring AOP與Aspect無關性
當你不用Spring AOP提供的注解時,Spring AOP和AspectJ沒半毛錢的關系,前者是JDK動态代理,用到了CGLIB(Code Generation Library),CGLIB是一個代碼生成類庫,可以在運作時候動态是生成某個類的子類。代理模式為要通路的目标對象提供了一種途徑,當通路對象時,它引入了一個間接的層。後者是靜态代理,在編譯階段就已經編譯到位元組碼檔案中。Spring中提供了前置通知
org.springframework.aop.MethodBeforeAdvice
、後置通知
org.springframework.aop.AfterReturningAdvice
,環繞通知
org.aopalliance.intercept.MethodInvocation
(通過反射實作,invoke(org.aopalliance.intercept.MethodInvocation mi)中的MethodInvocation擷取目标方法,目标類,目标字段等資訊),異常通知
org.springframework.aop.ThrowsAdvice
。這些通知能夠切入目标對象,Spring AOP的核心是代理Proxy,其主要實作類是
org.springframework.aop.framework.ProxyFactoryBean
,ProxyFactoryBean中
proxyInterfaces
為代理指向的目标接口,Spring AOP無法截獲未在該屬性指定的接口中的方法,
interceptorNames
是攔截清單,
target
是目标接口實作類,一個代理隻能有一個target。
Spring AOP的核心類
org.springframework.aop.framework.ProxyFactoryBean
雖然能實作AOP的行為,但是這種方式具有局限性,需要在代碼中顯式的調用ProxyFactoryBean代理工廠類,舉例:UserService是一個接口,UserServiceImpl是UserService的實作類,ApplicationContext context為Spring上下文,調用方式為
UserService userService = (UserService)context.getBean("userProxy");
。
完整的配置如下:
<bean id="userService" class="com.zhoujunwen.UserServiceImpl"></bean>
<!-- 定義前置通知,com.zhoujunwen.BeforeLogAdvice實作了org.springframework.aop.MethodBeforeAdvice -->
<bean id="beforeLogAdvice" class="com.zhoujunwen.BeforeLogAdvice"></bean>
<!-- 定義後置通知,com.zhoujunwen.AfterLogAdvice實作了org.springframework.aop.AfterReturningAdvice -->
<bean id="afterLogAdvice" class="com.zhoujunwen.AfterLogAdvice"></bean>
<!-- 定義異常通知, com.zhoujunwen.ThrowsLogAdvice實作了org.springframework.aop.ThrowsAdvice-->
<bean id="throwsLogAdvice" class="com.zhoujunwen.ThrowsLogAdvice"></bean>
<!-- 定義環繞通知,com.zhoujunwen.LogAroundAdvice實作了org.aopalliance.intercept.MethodInvocation -->
<bean id="logAroundAdvice" class="com.zhoujunwen.LogAroundAdvice"></bean>
<!-- 定義代理類,名 稱為userProxy,将通過userProxy通路業務類中的方法 -->
<bean id="userProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.zhoujunwen.UserService</value>
</property>
<property name="interceptorNames">
<list>
<value>beforeLogAdvice</value>
<!-- 織入後置通知 -->
<value>afterLogAdvice</value>
<!-- 織入異常通知 -->
<value>throwsLogAdvice</value>
<!-- 織入環繞通知 -->
<value>logAroundAdvice</value>
</list>
</property>
<property name="target" ref="userService"></property>
</bean>
當然,上述的局限性spring官方也給出了解決方案,讓AOP的通知在服務調用方毫不知情的下就進行織入,可以通過
org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator
自動代理。
<bean id="myServiceAutoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>logAroundAdvice</value>
</list>
</property>
<property name="beanNames">
<value>*Service</value>
</property>
</bean>
這個BeanNameAutoProxyCreator的bean中指明上下文中所有調用以Service結尾的服務類都會被攔截,執行logAroundAdvice的invoke方法。同時它會自動生成Service的代理,這樣在使用的時候就可以直接取服務類的bean,而不用再像上面那樣還用取代理類的bean。
對于BeanNameAutoProxyCreator建立的代理,可以這樣調用:
UserService userService = (UserService) context.getBean("userService");
,context為spring上下文。
Spring AOP與AspectJ有關性
當你用到Spring AOP提供的注入@Before、@After等注解時,Spring AOP和AspectJ就有了關系。在開發中引入了
org.aspectj:aspectjrt:1.6.11
org.aspectj:aspectjweaver:1.6.11
兩個包,這是因為Spring AOP使用了AspectJ的Annotation,使用了Aspect來定義切面,使用Pointcut來定義切入點,使用Advice來定義增強處理。雖然Spring AOP使用了Aspect的Annotation,但是并沒有使用它的編譯器和織入器。
Spring AOP其實作原理是JDK動态代理,在運作時生成代理類。為了啟用Spring對
@AspectJ
切面配置的支援,并保證Spring容器中的目标Bean被一個或多個切面自動增強,必須在Spring配置檔案中添加如下配置
<aop:aspectj-autoproxy/>
當啟動了@AspectJ支援後,在Spring容器中配置一個帶
@Aspect
注釋的Bean,Spring将會自動識别該 Bean,并将該Bean作為切面Bean處理。切面Bean與普通Bean沒有任何差別,一樣使用
<bean.../>
元素進行配置,一樣支援使用依賴注入來配置屬性值。
Spring AOP注解使用demo
全注解實作
業務邏輯代碼(AuthorizeService.java):
package com.zhoujunwen.engine.service;
import org.springframework.stereotype.Service;
/**
* Created with IntelliJ IDEA.
* Date: 2018/10/25
* Time: 12:47 PM
* Description:
*
* @author zhoujunwen
* @version 1.0
*/
@Service
public class AuthorizeService {
private static final String USERNAME = "zhoujunwen";
private static final String PASSWORD = "123456";
public void login(String username, String password) {
if (username == null || username.length() == 0) {
System.out.print("使用者名不能為空");
return;
}
if (password == null || password.length() == 0) {
System.out.print("使用者名不能為空");
return;
}
if (!USERNAME.equals(username) || !PASSWORD.equals(password)) {
System.out.print("使用者名或者密碼不對");
return;
}
System.out.print("登入成功");
}
}
切面邏輯代碼(LogAspect.java)
package com.zhoujunwen.engine.service;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* Created with IntelliJ IDEA.
* Date: 2018/10/25
* Time: 1:04 PM
* Description:
*
* @author zhoujunwen
* @version 1.0
*/
@Aspect
@Component
public class LogAspect {
@After("execution(* com.zhoujunwen.engine.service.AuthorizeService.login(..))")
public void logPointcut(){
System.out.println("***處理日志***");
}
}
這樣是實作了對AuthorizeService.login()方法的後置通知。不需要在xml中其他配置,當然前提是開啟
<aop:aspectj-autoproxy/>
aspectj的自動代理。
測試調用代碼:
AuthorizeService authorizeService = SpringContextHolder.getBean(AuthorizeService.class);
authorizeService.login("zhangsan", "zs2018");
xml配置實作
業務代碼,日志埋點(MeasurementService.java):
package com.zhoujunwen.engine.measurement;
import com.zhoujunwen.common.base.AccountInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* metrics 切面接口
* @create 2018-08-16-上午10:13
*/
@Service
public class MeasurementService {
private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementService.class);
public String gainZhimaLog(AccountInfo accountInfo) {
if (NumberUtils.isNumber(accountInfo.getZhimaPoint())) {
return "正常";
} else if (StringUtils.contains(accountInfo.getZhimaPoint(), "*")) {
return "未授權";
} else {
return "未爬到";
}
}
public String gainJiebeiLog(AccountInfo accountInfo) {
if (NumberUtils.isNumber(accountInfo.getJiebeiQuota())) {
return "正常";
}
return "未爬到";
}
public String gainHuabeiLog(AccountInfo accountInfo) {
if (accountInfo.getCreditQuota() != null) {
return "正常";
} else {
return "未爬到";
}
}
}
切面邏輯,統計日志中個字段的總和(KeywordMeasurement.java):
package com.zhoujunwen.engine.measurement;
import com.zhoujunwen.common.base.AccountInfo;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
/**
* 關鍵字段監控統計 <br>
*
* @create 2018-08-15-下午5:41
*/
public class KeywordMeasurement {
private String invokeCountFieldName = "";
/**
* 調用次數
*/
public void summary(JoinPoint joinPoint, Object result) {
try {
String msg;
String resultStr = "";
if (result instanceof String) {
resultStr = (String) result;
}
if (StringUtils.isBlank(resultStr)) {
return;
}
if ("正常".equals(resultStr)) {
msg = "_ok";
} else if ("未爬到".equals(resultStr)) {
msg = "_empty";
} else {
msg = "_star";
}
String methodName = joinPoint.getSignature().getName();
Object args[] = joinPoint.getArgs();
AccountInfo accountInfo = null;
for (Object arg : args) {
if (arg.getClass().getName().contains("AccountInfo")) {
accountInfo = (accountInfo) arg;
}
}
if (methodName.contains("Zhima")) {
invokeCountFieldName = "zhima" + msg;
} else if (methodName.contains("Jiebei")) {
invokeCountFieldName = "jiebei" + msg;
} else if (methodName.contains("Huabei")) {
invokeCountFieldName = "huabei" + msg;
} else {
return;
}
// TODO 寫入到influxDB
} catch (Exception e) {
//skip
}
}
}
完整的配置(後置通知,并需要傳回結果):
<bean id="keywordMeasurement" class="com.zhoujunwen.engine.measurement.KeywordMeasurement"/>
<aop:config proxy-target-class="true">
<aop:aspect id="keywordMeasurementAspect" ref="keywordMeasurement">
<aop:pointcut id="keywordMeasurementPointcut"
expression="execution(* com.zhoujunwen.engine.measurement.SdkMeasurementService.gain*(..))"/>
<!-- 統計summary,summary方法有兩個參數JoinPoint和Object-->
<aop:after-returning method="summary" returning="result" pointcut-ref="keywordMeasurementPointcut"/>
</aop:aspect>
</aop:config>
其他可用的配置(省略了rt、count、qps的aspect):
<!-- 統計RT,rt方法隻有一個參數ProceedingJoinPoint-->
<aop:around method="rt" pointcut-ref="keywordMeasurementPointcut"/>
<!--統計調用次數,count方法隻有一個參數JoinPoint-->
<aop:after method="count" pointcut-ref="keywordMeasurementPointcut"/>
<!--統計QPS,qps方法隻有一個參數JoinPoint-->
<aop:after method="qps" pointcut-ref="keywordMeasurementPointcut"/>
注意:關于Spring AOP中,切面代理類一定是由Spirng容器管理,是以委托類也需要交由Spring管理,不可以将委托類執行個體交由自己建立的容器管理(比如放入自己建立的Map中),如果這麼做了,當調用委托類執行個體的時候,切面是不生效的。
原因:(1)實作實作和目标類相同的接口,spring會使用JDK的java.lang.reflect.Proxy類,它允許Spring動态生成一個新類來實作必要的接口,織入通知,并且把這些接口的任何調用都轉發到目标類。
(2)生成子類調用,spring使用CGLIB庫生成目标類的一個子類,在建立這個子類的時候,spring織入通知,并且把對這個子類的調用委托到目标類。
AspectJ和Spring AOP的差別和選擇
兩者的聯系和差別
AspectJ和Spring AOP都是對目标類增強,生成代理類。
AspectJ是在編譯期間将切面代碼編譯到目标代碼的,屬于靜态代理;Spring AOP是在運作期間通過代理生成目标類,屬于動态代理。
AspectJ是靜态代理,故而能夠切入final修飾的類,abstract修飾的類;Spring AOP是動态代理,其實作原理是通過CGLIB生成一個繼承了目标類(委托類)的代理類,是以,final修飾的類不能被代理,同樣static和final修飾的方法也不會代理,因為static和final方法是不能被覆寫的。在CGLIB底層,其實是借助了ASM這個非常強大的Java位元組碼生成架構。關于CGLB和ASM的讨論将會新開一個篇幅探讨。
Spring AOP支援注解,在使用@Aspect注解建立和配置切面時将更加友善。而使用AspectJ,需要通過.aj檔案來建立切面,并且需要使用ajc(Aspect編譯器)來編譯代碼。
選擇對比
首先需要考慮,Spring AOP緻力于提供一種能夠與Spring IoC緊密內建的面向切面架構的實作,以便于解決在開發企業級項目時面臨的常見問題。明确你在應用橫切關注點(cross-cutting concern)時(例如事物管理、日志或性能評估),需要處理的是Spring beans還是POJO。如果正在開發新的應用,則選擇Spring AOP就沒有什麼阻力。但是如果你正在維護一個現有的應用(該應用并沒有使用Spring架構),AspectJ就将是一個自然的選擇了。為了詳細說明這一點,假如你正在使用Spring AOP,當你想将日志功能作為一個通知(advice)加入到你的應用中,用于追蹤程式流程,那麼該通知(Advice)就隻能應用在Spring beans的連接配接點(Joinpoint)之上。
另一個需要考慮的因素是,你是希望在編譯期間進行織入(weaving),還是編譯後(post-compile)或是運作時(run-time)。Spring隻支援運作時織入。如果你有多個團隊分别開發多個使用Spring編寫的子產品(導緻生成多個jar檔案,例如每個子產品一個jar檔案),并且其中一個團隊想要在整個項目中的所有Spring bean(例如,包括已經被其他團隊打包了的jar檔案)上應用日志通知(在這裡日志隻是用于加入橫切關注點的舉例),那麼通過配置該團隊自己的Spring配置檔案就可以輕松做到這一點。之是以可以這樣做,就是因為Spring使用的是運作時織入。
還有一點,因為Spring基于代理模式(使用CGLIB),它有一個使用限制,即無法在使用final修飾的bean上應用橫切關注點。因為代理需要對Java類進行繼承,一旦使用了關鍵字final,這将是無法做到的。在這種情況下,你也許會考慮使用AspectJ,其支援編譯期織入且不需要生成代理。于此相似,在static和final方法上應用橫切關注點也是無法做到的。因為Spring基于代理模式。如果你在這些方法上配置通知,将導緻運作時異常,因為static和final方法是不能被覆寫的。在這種情況下,你也會考慮使用AspectJ,因為其支援編譯期織入且不需要生成代理。
如果你希望使用一種易于實作的方式,就選擇Spring AOP吧,因為Spring AOP支援注解,在使用@Aspect注解建立和配置切面時将更加友善。而使用AspectJ,你就需要通過.aj檔案來建立切面,并且需要使用ajc(Aspect編譯器)來編譯代碼。是以如果你确定之前提到的限制不會成為你的項目的障礙時,使用Spring AOP。AspectJ的一個間接局限是,因為AspectJ通知可以應用于POJO之上,它有可能将通知應用于一個已配置的通知之上。對于一個你沒有注意到這切面問題的大範圍應用的通知,這有可能導緻一個無限循環。在下面這種情況下,當proceed即将被調用時,日志通知會被再次應用,這樣就導緻了嵌套循環。
public aspectLogging {
Object around() : execution(public * * (..))
Sysytem.out.println(thisJoinPoint.getSignature());
return proceed();
}
參考文章
誠摯感謝以下文章及作者,也是讓我在參考實踐以及理論總結的過程中學習到了很多東西。不做無頭無腦的抄襲者,要做閱讀他人的文章,汲取精粹,親自實踐得出結論。尊重原創,尊重作者!
AspectJ(一) 一些該了解的概念 AspectJ 架構,比用 spring 實作 AOP 好用很多喲! 比較分析 Spring AOP 和 AspectJ 之間的差别 AspectJ基本用法 應用Spring AOP(一) AspectJ官方doc文檔 Spring AOP,AspectJ, CGLIB 有點暈該文首發
《虛懷若谷》個人部落格,轉載前請務必署名,轉載請标明出處。
古之善為道者,微妙玄通,深不可識。夫唯不可識,故強為之容:
豫兮若冬涉川,猶兮若畏四鄰,俨兮其若客,渙兮若冰之釋,敦兮其若樸,曠兮其若谷,混兮其若濁。
孰能濁以靜之徐清?孰能安以動之徐生?
保此道不欲盈。夫唯不盈,故能敝而新成。
請關注我的微信公衆号:下雨就像彈鋼琴,Thanks(・ω・)ノ