Spring AOP的介紹與實現
1. AOP的介紹
AOP(Aspect OrientedProgramming)稱為面向切面程式設計,在程式開發中主要用來解決一些系統層面上的問題,比如日誌,事務,許可權等待,Struts2的攔截器設計就是基於AOP的思想,是個比較經典的例子。
在不改變原有的邏輯的基礎上,增加一些額外的功能。代理也是這個功能,讀寫分離也能用aop來做。 AOP可以說是OOP(Object Oriented Programming,面向物件程式設計)的補充和完善。OOP引入封裝、繼承、多型等概念來建立一種物件層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌程式碼往往橫向地散佈在所有物件層次中,而與它對應的物件的核心功能毫無關係對於其他型別的程式碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的程式碼被稱為橫切(crosscutting),在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。
AOP技術恰恰相反,它利用一種稱為"橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。
使用"橫切"技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
2. AOP的相關概念
-
橫切關注點:對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之為橫切關注點
-
Aspect(切面):通常是一個類,裡面可以定義切入點和通知
-
JointPoint(連線點):程式執行過程中明確的點,一般是方法的呼叫。被攔截到的點,因為Spring只支援方法型別的連線點,所以在Spring中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器
-
Advice(通知):AOP在特定的切入點上執行的增強處理,有before(前置),after(後置),afterReturning(最終),afterThrowing(異常),around(環繞)
-
Pointcut(切入點):就是帶有通知的連線點,在程式中主要體現為書寫切入點表示式
-
weave(織入):將切面應用到目標物件並導致代理物件建立的過程
-
introduction(引入):在不修改程式碼的前提下,引入可以在執行期為類動態地新增一些方法或欄位
-
AOP代理(AOPProxy):AOP框架建立的物件,代理就是目標物件的加強。Spring中的AOP代理可以使JDK動態代理,也可以是CGLIB代理,前者基於介面,後者基於子類
-
目標物件(Target Object): 包含連線點的物件。也被稱作被通知或被代理物件。POJO
3. Advice通知型別:
-
(1)Before:
在目標方法被呼叫之前做增強處理,
@Before只需要指定切入點表示式即可 -
(2)AfterReturning:
在目標方法正常完成後做增強,
@AfterReturning除了指定切入點表示式後,還可以指定一個返回值形參名returning,代表目標方法的返回值 -
(3)AfterThrowing:
主要用來處理程式中未處理的異常,
@AfterThrowing除了指定切入點表示式後,還可以指定一個throwing的返回值形參名,可以通過該形參名來訪問目標方法中所丟擲的異常物件 -
(4)After:
在目標方法完成之後做增強,無論目標方法時候成功完成。
@After可以指定一個切入點表示式 -
(5)Around:
環繞通知,在目標方法完成前後做增強處理,環繞通知是最重要的通知型別,像事務,日誌等都是環繞通知,注意程式設計中核心是ProceedingJoinPoint
@Around可以指定一個切入點表示式
4. AOP的實現方式
通過介面來實現
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterAdvice;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import java.lang.reflect.Method;
/**
* 前置通知:執行目標方法前時,執行的通知;
* 後置通知:執行目標方法後時,執行的通知;
* 異常通知:執行目標方法發生異常時,執行的通知;
* 環繞通知:在環繞通知中可以定義,前置通知、後置通知、異常通知和最終通知,比較全面;
* 最終通知:執行方法後,都會執行的通知;
* <p>
* 通過介面實現通知
* 前置通知:繼承MethodBeforeAdvice介面,並重寫before()方法;
* 後置通知:繼承AfterReturningAdvice介面,並重寫afterReturning()方法;
* 異常通知:繼承ThrowsAdvice介面,無重寫方法;
* 環繞通知:繼承MethodInterceptor介面,並重寫invoke()方法;
* 最終通知;
*/
public class AspectImpl implements MethodBeforeAdvice, AfterReturningAdvice, AfterAdvice, MethodInterceptor, ThrowsAdvice {
/**
* 前置通知
* @param method 目標方法物件
* @param args 目標方法引數
* @param target 目標方法的類物件
* @throws Throwable
*/
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.printf("前置通知:%s%n引數:%s%n", method.getName(), (Integer) args[0]);
}
/**
* 後置通知
* @param returnValue 目標方法的返回值
* @param method 目標方法物件
* @param args 目標方法引數
* @param target 目標方法的類物件
* @throws Throwable
*/
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.printf("後置通知:%s%n返回值:%s%n%n", method.getName(), returnValue);
}
/**
* 異常通知
* @param method 目標方法物件
* @param args 目標方法引數
* @param target 目標方法的類物件
* @param ex 異常型別
*/
public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
System.out.printf("異常通知:%s%n%n", ex.toString());
}
/**
* 環繞通知 攔截器
* @param invocation 連線點資訊
* @return 目標方法執行後的結果
* @throws Throwable
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = null;
try {
System.out.printf("環繞通知-前置通知:%s%n", invocation.getMethod().getName());
result = invocation.proceed();
System.out.printf("環繞通知-後置通知:%s%n", invocation.getMethod().getName());
} catch (Exception ex) {
System.out.printf("環繞通知-異常通知:%s%n", invocation.getMethod().getName());
} finally {
System.out.printf("環繞通知-最終通知:%s%n", invocation.getMethod().getName());
}
return result;
}
}
配置檔案xml
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="cn.sivan.aop.impl.UserServiceImpl"/>
<bean id="aspectImpl" class="cn.sivan.aop.aspect.AspectImpl"/>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* cn.sivan.aop.impl.*.*(..))"/>
<aop:advisor advice-ref="aspectImpl" pointcut-ref="pointcut"/>
</aop:config>
</beans>
測試
public class UserServiceTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.updateUser(1);
}
}
環繞通知-前置通知:updateUser
前置通知:updateUser
引數:1
updateUser:更新-使用者
後置通知:updateUser
返回值:6059
環繞通知-後置通知:updateUser
環繞通知-最終通知:updateUser
通過配置實現通知
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 通過配置實現通知
* before 前置通知
* afterReturning 後置通知
* after 最終通知
* afterThrowing 異常通知
* invoke 環繞通知
* <p>
* JoinPoint物件封裝了SpringAop中切面方法的資訊,在切面方法中新增JoinPoint引數,就可以獲取到封裝了該方法資訊的JoinPoint物件
* Signature getSignature(); 獲取封裝了署名資訊的物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊
* Object[] getArgs(); 獲取傳入目標方法的引數物件
* Object getTarget(); 獲取被代理的物件
* Object getThis(); 獲取代理物件
*/
public class AspectConfig {
public void before(JoinPoint jp) {
System.out.println("目標方法名為:" + jp.getSignature().getName());
System.out.println("目標方法所屬類的簡單類名:" + jp.getSignature().getDeclaringType().getSimpleName());
System.out.println("目標方法所屬類的類名:" + jp.getSignature().getDeclaringTypeName());
//獲取傳入目標方法的引數
Object[] args = jp.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i + 1) + "個引數為:" + args[i]);
}
System.out.println("被代理的物件:" + jp.getTarget());
System.out.println("代理物件自己:" + jp.getThis());
}
public void after(JoinPoint jp) {
System.out.println("最終通知");
}
public void afterReturning(JoinPoint jp, Object returnValue) {
System.out.println("後置通知:" + returnValue);
}
public void afterThrowing(JoinPoint jp, NullPointerException ex) {
System.out.println("異常通知:" + ex.toString());
}
public Object invoke(ProceedingJoinPoint pjp) {
System.out.println("環繞通知");
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
配置檔案xml
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="cn.sivan.aop.impl.UserServiceImpl"/>
<bean id="aspectConfig" class="cn.sivan.aop.aspect.AspectConfig"/>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* cn.sivan.aop.impl.UserServiceImpl.*(..))"/>
<aop:aspect ref="aspectConfig">
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>
<aop:after-returning method="afterReturning" returning="returnValue" pointcut-ref="pointcut"/>
<aop:after-throwing method="afterThrowing" throwing="ex" pointcut-ref="pointcut"/>
<aop:around method="invoke" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
通過註解實現通知
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 通過註解實現通知
* 前置通知: 註解為 @Before
* 後置通知: 註解為 @AfterReturning
* 異常通知: 註解為 @AfterThrowing
* 環繞通知: 註解為 @Around
* 最終通知: 註解為 @After
* 在applicationContext.xml中開啟註解對AOP的支援
* <p>
* 兩個注意點:
* @Aspect 宣告該類是一個通知;
* @Component("annotationAdvice") 將AnnotationAdvice納入到SpringIoc容器中。
*/
@Aspect
@Component("aspectAnnotation")
public class AspectAnnotation {
@Before("execution(* cn.sivan.aop.impl.*.*(..))")
public void myBefore(JoinPoint jp) {
System.out.println("前置通知:" + jp.getSignature().getName());
}
@After("execution(* cn.sivan.aop.impl.*.*(..))")
public void myAfter(JoinPoint jp) {
System.out.println("最終通知");
}
@AfterReturning(pointcut = "execution(* cn.sivan.aop.impl.*.*(..))", returning = "returnValue")
public void myAfterReturning(JoinPoint jp, Object returnValue) {
System.out.println("後置通知:" + returnValue);
}
@AfterThrowing(pointcut = "execution(* cn.sivan.aop.impl.*.*(..))", throwing = "ex")
public void myThrows(JoinPoint jp, NullPointerException ex) {
System.out.println("異常通知:" + ex.toString());
}
@Around("execution(* cn.sivan.aop.impl.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) {
System.out.println("環繞通知");
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
配置檔案xml
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--開啟註解對aop的支援-->
<aop:aspectj-autoproxy/>
<!-- 裡面放包的名字,可以放多個包。放在裡面之後執行會在相關包中找相關的註解,找到了就將他們納入到SpringIoc容器中 -->
<context:component-scan base-package="cn.sivan.aop"/>
</beans>
常見使用場景
Advice | |
---|---|
環繞通知 | 控制事務 許可權控制 |
後置通知 | 記錄日誌(方法已經成功呼叫) |
異常通知 | 異常處理 控制事務 |
最終通知 | 記錄日誌(方法已經呼叫,但不一定成功) |
指定每個 aspect 的執行順序
- 實現org.springframework.core.Ordered介面,實現它的getOrder()方法
- 給aspect新增@Order註解,該註解全稱為:org.springframework.core.annotation.Order
不管採用上面的哪種方法,都是值越小的 aspect 越先執行。
比如,我們為 apsect1 和 aspect2 分別新增 @Order 註解,如下:
@Order(5)
@Component
@Aspect
public class Aspect1 {
// ...
}
@Order(6)
@Component
@Aspect
public class Aspect2 {
// ...
}
這樣修改之後,可保證不管在任何情況下, aspect1 中的 advice 總是比 aspect2 中的 advice 先執行.