文章目錄
使用AOP可以實現在不侵入到目標方法的情況下,動態的將某一模塊加入IoC容器中。AOP的應用場景一般在日誌、權限驗證、安全檢查、事務控制。
一、IoC容器中保存的是組件的代理對象
假設使用動態代理後,從容器中獲取bean的類型,可以發現,獲得的是一個代理類型,說明AOP的底層就是動態代理,Ioc容器中保存的是組件的代理對象。
@ContextConfiguration(locations = "classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class AOPTest {
@Autowired
Calculator ioc;
@Test
public void test() {
System.out.println(ioc.getClass());
}
}
補充:如果從IoC容器中使用類型來獲取到目標對象,一定要使用它的接口類型來獲取。如果沒有接口,Spring的cglib會自動爲我們創建代理對象。
二、切入點表達式的寫法
固定格式:execution(訪問權限符
返回值類型
方法全類名(參數表)
)
通配符:
(1)*
:
- 匹配一個或多個字符。如:
"execution(public int com.gql.impl.myMathCalculator.*.Math*(int, int))"
- 匹配任意一個參數。如:
"execution(public int com.gql.impl.myMathCalculator.*(int, *))"
- 如果
*
放在路徑裏,只能匹配一層路徑。如:"execution(public int com.gql.impl.myMathCalculator.*(int, int))"
- 權限位置不能寫
*
默認就是public的。
(2)..
:
- 匹配任意任意個任意類型參數。如:
"execution(public int com.gql.impl.myMathCalculator.*(..)"
- 匹配任意多層路徑。如:
"execution(public int com.gql..myMath*(..)"
記住兩種切入點表達式:
最模糊的:execution(* *.*(..))
。代表任意包下任意類的任意方法,
最精確的:execution(public int com.gql.impl.myMathCalculator.add(int,int))
另外:切入點表達式還支持"&&"、"||"、"!"
三、通知方法的執行順序
正常執行:
- ① @Before(前置通知)
- ② @After(後置通知)
- ③ @AfterReturning(正常返回)
異常執行:
- ①@Before(前置通知)
- ② @After(後置通知)
- ③ @AfterThrowing(方法異常)
四、JoinPoint獲取目標方法的信息
在通知方法運行的時候,拿到目標方法的詳細信息。只需要在通知方法的參數列表上寫一個參數。
JoinPoint joinPoint:封裝了當前目標方法的詳細信息。
- 獲得方法名:joinPoint.getSignature().getName()
- 獲得參數列表:Arrays.asList(joinPoint.getArgs())
五、throwing、returning指定哪個參數用來接收異常、返回值
使用returning獲得方法返回的結果:
@AfterReturning(value = "execution(public int com.gql.impl.myMathCalculator.*(int, int))", returning = "result")
public static void logReturn(JoinPoint joinPoint, Object result) {
System.out.println("[" + joinPoint.getSignature().getName() + "]方法執行完成,計算結果是[" + result + "]");
}
使用throwing獲得異常信息:
@AfterThrowing(value = "execution(public int com.gql.impl.myMathCalculator.*(int, int))", throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e) {
System.out.println("[" + joinPoint.getSignature().getName() + "方法出現異常,異常信息是:");
}
這種寫法和Ajax接收服務器數據特別像,只不過要告訴Spring這個變量的作用是接收返回值還是拋出異常。
六、Spring對通知方法的約束
Spring對通知方法的要求不嚴格,唯一有要求的是方法的參數列表一定不能亂寫。 通知方法是Spring利用反射調用的,每次方法調用需要確定這個方法的參數表的值。即參數列表上的每一個參數,Spring都得知道它是什麼。
- Spring認識的參數:JoinPoint joinPoint。
- Spring不認識的參數:使用直接中的屬性告訴Spring。
補充:在進行接收異常和接收返回值的時候,最好在參數中將異常的範圍、返回值的類型寫大一點。
七、抽取可重用的切入點表達式
在切面(Aspect)中聲明一個無返回類型的空方法,併爲方法標註@Pointcut註解。
@Pointcut("execution(public int com.gql.impl.myMathCalculator.*(int, int))")
public void myPoint() {
}
後來的方法需要寫表達式時只需要將value的值填寫爲方法名即可。
八、環繞通知實際上就是動態代理
@Around環繞通知是Spring中最強大的通知,環繞通知實際上就是動態代理。
* try{
* //前置通知
* method.invoke(obj,args);
* //返回通知
* }catch(e){
* //異常通知
* }finally{
* //後置通知
* }
環繞通知,就是上面代碼中的四個通知合起來的效果。下面演示環繞通知:
@Around("myPoint()")
public Object myAround(ProceedingJoinPoint pjp) {
Object proceed = null;
String methodName = pjp.getSignature().getName();
try {
System.out.println("環繞通知前:" + methodName + "方法開始執行");
proceed = pjp.proceed(pjp.getArgs());// 相當於method.invoke()
System.out.println("環繞通知後:" + methodName + "方法返回,返回值是" + proceed);
} catch (Throwable e) {
System.out.println(methodName + "方法出現異常,異常信息:" + e);
throw new RuntimeException(e);
} finally {
System.out.println("環繞後置通知:" + methodName + "方法結束");
}
return proceed;
}
上面的環繞通知中:
- pjp.proceed(pjp.getArgs())實際上就是method.invoke(),即通過反射調用了方法。
如果程序正常執行:
如果程序出現異常:
九、同時聲明環繞通知和普通通知時的執行順序
環繞通知優先於普通通知執行,下面說明兩者都聲明時的執行順序:
* [普通前置]
* {
* try{
* 環繞:前置
* 目標方法執行
* 環繞:返回
* }catch(){
* 環繞:出現異常
* }finally{
* 環繞:後置
* }
* [普通後置]
* [普通返回/異常]
真正的執行順序是:
- ①環繞:前置 ②普通前置。(這兩個的執行順序是隨機的,不必關注)
- ③目標方法執行
- ④環繞:返回/環繞:出現異常
- ⑤環繞:後置
- ⑥普通後置
- ⑦普通返回/異常
注意:如果環繞通知拿到異常不處理,普通通知就不會拿到異常,則普通通知感受到的方法是正常的,這是由於環繞通知把異常catch掉了。因此,爲了讓外界能夠知道這個異常,環繞通知中的異常一定要拋出去。
十、多切面運行順序
下圖的結果是LogUtils切面
,VaApsect切面
,LogUtils切面中的環繞通知
,同時作用與一個方法時的執行順序:
- 默認情況下是以開頭字母順序決定哪個切面先執行。
- 可以通過@Order(num)指定切面執行順序,數值越小優先級越高。
- 牢記一句話:
環繞是在哪個切面內,就只作用於哪個切面內!