面向切面編程(Aspect Oriented Programming, AOP)是面向對象編程(Object Oriented Programming,OOP)的強大補充,通過橫切面注入的方式引入其他額外功能,比如日志記錄,事務處理等,用戶無需修改源代碼就可以"優雅"的實現額外功能的補充,對於Programmer來說,AOP是個非常強大的工具。
AOP中的切面處理邏輯會被應用到我們所定義的切點(Point Cut)上,切面邏輯定義可以使用 around, before,after等Aspect注解實現,切點可以使用Aspect注解中的參數指定或者通過xml配置文件聲明。在編寫代碼的過程中,切點控制往往不夠靈活,需要我們在xml或者Aspect注解參數中指定方法的path,當切點較多,需要顆粒度更加細致的切點控制時,通常我們需要添加大量的切點定義代碼,這樣比較麻煩。通常呢我們我們可以通過結合自定義注解來解決這個問題,實現更加靈活的切點控制。
自定義注解實現AOP切點定義的背后原理說起來其實很簡單,通過掃描項目所有的類,然后過濾出標注點的位置,將切面自定義邏輯應用到標注點上,就實現了我們的業務需求。但是,技術上如何去實現呢?本文就這一問題,結合Java Spring AOP框架給出解答。
目標
我們的自定義注解需要具備以下功能:
- 類中方法添加注解,則這個方法成為切點
- 類添加注解,則這個類中所有的方法成為切點
- 抽象類方法添加注解,則抽閑類中的這個方法成為切點
- 抽象類添加注解,則抽象類中的所有方法成為切點
- 接口添加注解,則接口中定義的所有方法成為切點
- 接口中方法添加注解,則接口中的這個方法成為切點
JDK提供的關鍵類和方法
Class
Java最基本的元素稱為"類",類中可以包涵方法和屬性的定義。Class對象提供了很多有用的方法,可以幫助我們切點位置定位,比如:
- public native Class<? super T> getSuperclass();獲取當前類的父類
- public Class<?>[] getInterfaces();獲取當前類實現的接口
- public Method[] getDeclaredMethods() throws SecurityException 獲取當前類中聲明的方法
- public < A extends Annotation > A getAnnotation(Class < A > annotationClass)獲取當前類指定標簽的對象,若為空,表明當前類沒有標簽annotationClass。
Method
我們還使用到Method類的一些方法:
- public String getName(); 獲取方法的名字
- public < T extends Annotation > T getAnnotation(Class
annotationClass);取當前類指定標簽的對象,若為空,表明當前類沒有標簽annotationClass
ProceedingJoinPoint
ProceedingJoinPoint接口提供了很多實用的函數,便於用戶獲取應用切面點函數具體的信息。下面四個接口是我們用的比較多的:
- Object proceed() throws Throwable; 調用要攔截的方法
- Object proceed(Object[] var1) throws Throwable;調用要攔截的方法,可以自定義傳入參數
- Object[] getArgs();獲取攔截方法的傳入參數
- Signature getSignature();獲取攔截方法的方法名
實現
XML配置
配置xml文件,使能AOP和Spring Bean自動裝配
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 使能AOP-->
<aop:aspectj-autoproxy/>
<!-- 自動裝載bean使能-->
<context:component-scan base-package="com.mj.spring.aop"/>
<context:annotation-config/>
</beans>
定義自定義標簽
我們的自定義標簽可以作用於類,方法上,運行時工作。這兒需要說一下兩個標簽@Target和Retention,@Target用來設置標簽的作用范圍:
- @Target(ElementType. FIELD)表示標簽只能用來修飾字段、枚舉的常量
- @Target(ElementType.METHOD)表示標簽只能用來修飾方法
- @Target(ElementType.TYPE) 標簽可用來修飾接口、類、枚舉、注解
- ...
@Retention用來修飾注解的生存范圍
-
@Retention(RetentionPolicy.SOURCE) 注解僅存在於源碼中,在class字節碼文件中不包含
-
@Retention(RetentionPolicy.CLASS) 默認的保留策略,注解會在class字節碼文件中存在,但運行時無法獲得,
-
@Retention(RetentionPolicy.RUNTIME) 注解會在class字節碼文件中存在,在運行時可以通過反射獲取到
@Retention(RetentionPolicy.RUNTIME) public @interface AOPLog4jAnnotation { }
注意到我們沒有添加Target標簽,不指定標簽的作用范圍,那么標簽適用於所有范圍。
定義切面類
完整的切面類代碼如下所示,類中實現了切面邏輯的定義和切點判斷的邏輯代碼。
@Component
@Aspect
public class APIProxy{
private final static Log LOGGER = LogFactory.getLog(APIProxy.class);
//切面應用范圍是在com.mj.spring.aop包下面所有函數
@Around("execution(* com.mj.spring.aop..*.*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
String signatureName = joinPoint.getSignature().getName();
Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
if (isTagged(invokeClass, signatureName)) {
LOGGER.info(signatureName + " is tagged");
joinPoint.proceed();
return;
}
joinPoint.proceed();
}
//掃描父類是否被打上標簽,或者父類中的這個方法是否被打傷標簽
private boolean isTagged(Class invokeClass, String signatureName) {
if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
return true;
}
if (!invokeClass.equals(Object.class)) {
return isTaggedInClassOf(invokeClass, signatureName) ? true :
isTagged(invokeClass.getSuperclass(), signatureName);
}
return false;
}
//掃描當前類的接口
private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
Class[] interfaces = invokeClass.getInterfaces();
for (Class cas : interfaces) {
return isTaggedInClassOf(cas, signatureName) ? true :
isTaggedInInterfaceOf(cas, signatureName);
}
return false;
}
//方法名為signatureName的方法tagged有兩種情況:方法本身被taged或者方法所在的類被taged
private boolean isTaggedInClassOf(Class cas, String signatureName) {
return Lists.newArrayList(cas.getDeclaredMethods())
.stream().anyMatch(method ->
isMethodWithName(method, signatureName) && isMethodTagged(method)
|| isMethodWithName(method, signatureName) && isClassTagged(cas));
}
private boolean isClassTagged(Class invokeClass) {
return invokeClass.getAnnotation(AOPLog4jAnnotation.class) != null;
}
private boolean isMethodTagged(Method method) {
return method.getAnnotation(AOPLog4jAnnotation.class) != null;
}
private boolean isMethodWithName(Method method, String name) {
return method.getName().equals(name);
}
}
下面代碼實現了一個around切面advice定義,切面邏輯的應用范圍是com.mj.spring.aop包下的所有的方法,判斷當前執行方法是否被打上標簽,如果打上標簽,那么執行我們額外添加的業務邏輯代碼,這里為簡單起見在方法運行前打了一個log,然后執行方法,返回。否則直接調用方法,不做任何額外處理。
//切面應用范圍是在com.mj.spring.aop包下面所有函數
@Around("execution(* com.mj.spring.aop..*.*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
String signatureName = joinPoint.getSignature().getName();
Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
if (isTagged(invokeClass, signatureName)) {
LOGGER.info(signatureName + " is tagged");
joinPoint.proceed();
return;
}
joinPoint.proceed();
}
方法被打上自定義標簽有以下幾種可能:
- 該方法方法體被打上標簽
- 該方法所在類被打上標簽
- 該方法所在的API接口函數對應被打上標簽
- 該方法所在的API接口被打上標簽
- 該方法所在的抽象類被打上標簽
- 該方法所在的抽象類函數定義被打上標簽
對於接口函數來說,接口之間可以多重嵌套,搜尋接口中指定函數的標簽,需要采用遞歸的方式向上尋找,對於父類繼承也同樣如此。下面的代碼實現涵蓋了上面6種能性的所有判讀。
//掃描父類是否被打上標簽,或者父類中的這個方法是否被打傷標簽
private boolean isTagged(Class invokeClass, String signatureName)
{
if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
return true;
}
if (!invokeClass.equals(Object.class)) {
return isTaggedInClassOf(invokeClass, signatureName) ? true :
isTagged(invokeClass.getSuperclass(), signatureName);
}
return false;
}
函數開始:
- 判斷當前名為signatureName的方法是否在invokeClass類所實現的API接口中被Tag。(實現3和4的判斷)
- 判斷當前類是否為Object.class,若不是則執行第三步,否則執行第四步
- 判斷當前名為signatureName的方法是否在類invokeClass中被tag(實現1和2的判斷)
- 上面三項沒有為真,則調用當前類的父類繼續遞歸(實現5和6的判斷)
判斷當前名為signatureName的方法是否在invokeClass類所實現的API接口中被Tag的代碼如下所示,首先獲取當前類所有接口,分別對每個接口類進行方法檢查,若檢查成功,則返回true,否則繼續向上遞歸。
//掃描當前類的接口
private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
Class[] interfaces = invokeClass.getInterfaces();
for (Class cas : interfaces) {
return isTaggedInClassOf(cas, signatureName) ? true :
isTaggedInInterfaceOf(cas, signatureName);
}
return false;
}
判斷一個名為signatureName的方法在類cas中是否被tag的代碼如下所示:
private boolean isTaggedInClassOf(Class cas, String signatureName) {
return Lists.newArrayList(cas.getDeclaredMethods())
.stream().anyMatch(method ->
isMethodWithName(method, signatureName) && isMethodTagged(method)
|| isMethodWithName(method, signatureName)&& isClassTagged(cas));
代碼邏輯實現很簡單,判斷方法被tag條件為:該方法是在該類中同時(該方法體是被打上標簽或者類被打上標簽)
Conclusion
本文和大家分享了如何通過自定義注解實現AOP切點定義,希望能夠對大家有所幫助。本文完整的源碼,單元測試位於:< https://github.com/jma19/spring/tree/master/spring-aop >, 歡迎大家下載,批評指正。