1.什麼是AOP? AOP(Aspect-Oriented Programming, 面向切麵編程): 是一種新的方法論, 是對傳統 OOP(Object-Oriented Programming, 面向對象編程) 的補充,它的主要編程對象是切麵(aspect), 而切麵模塊化橫切關註點.在應用 A ...
1.什麼是AOP?
AOP(Aspect-Oriented Programming, 面向切麵編程): 是一種新的方法論, 是對傳統 OOP(Object-Oriented Programming, 面向對象編程) 的補充,它的主要編程對象是切麵(aspect), 而切麵模塊化橫切關註點.在應用 AOP 編程時, 仍然需要定義公共功能, 但可以明確的定義這個功能在哪裡, 以什麼方式應用, 並且不必修改受影響的類. 這樣一來橫切關註點就被模塊化到特殊的對象(切麵)里。
2.為什麼需要AOP?
越來越多的非業務需求(日誌和驗證等)加入後, 原有的業務方法急劇膨脹. 每個方法在處理核心邏輯的同時還必須兼顧其他多個關註點. 以日誌需求為例, 只是為了滿足這個單一需求, 就不得不在多個模塊(方法)里多次重覆相同的日誌代碼. 如果日誌需求發生變化, 必須修改所有模塊。
上述問題解決的方法就是使用動態代理,代理設計模式的原理是使用一個代理將對象包裝起來, 然後用該代理對象取代原始對象. 任何對原始對象的調用都要通過代理. 代理對象決定是否以及何時將方法調用轉到原始對象上。
使用AOP的好處是:
- 每個事物邏輯位於一個位置, 代碼不分散, 便於維護和升級
- 業務模塊更簡潔, 只包含核心業務代碼.
3.AOP術語
- 切麵(Aspect): 橫切關註點(跨越應用程式多個模塊的功能)被模塊化的特殊對象;
- 通知(Advice): 切麵必須要完成的工作;
- 目標(Target): 被通知的對象;
- 代理(Proxy): 向目標對象應用通知之後創建的對象;
- 連接點(Joinpoint):程式執行的某個特定位置:如類某個方法調用前、調用後、方法拋出異常後等。連接點由兩個信息確定:方法表示的程式執行點;相對點表示的方位。例如 ArithmethicCalculator#add() 方法執行前的連接點,執行點為 ArithmethicCalculator#add(); 方位為該方法執行前的位置;
- 切點(pointcut):每個類都擁有多個連接點:例如 ArithmethicCalculator 的所有方法實際上都是連接點,即連接點是程式類中客觀存在的事務。AOP 通過切點定位到特定的連接點。類比:連接點相當於資料庫中的記錄,切點相當於查詢條件。切點和連接點不是一對一的關係,一個切點匹配多個連接點,切點通過 org.springframework.aop.Pointcut 介面進行描述,它使用類和方法作為連接點的查詢條件。
4.如何使用AOP?
AspectJ:Java 社區里最完整最流行的 AOP 框架.在 Spring2.0 以上版本中, 可以使用基於 AspectJ 註解或基於 XML 配置的 AOP。
4.1 在Spring中啟用AspectJ註解支持
(1)在classpath下添加jar包
要在Spring應用中使用AspectJ註解,需要添加的jar包有(包含Spring的基礎jar包):
- com.springsource.org.aopalliance-1.0.0.jar
- com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
- commons-logging-1.1.3.jar
- spring-aop-4.0.0.RELEASE.jar
- spring-aspects-4.0.0.RELEASE.jar
- spring-beans-4.0.0.RELEASE.jar
- spring-context-4.0.0.RELEASE.jar
- spring-core-4.0.0.RELEASE.jar
- spring-expression-4.0.0.RELEASE.jar
(2)在配置文件中加入AOP的命名空間
(3)要在 Spring IOC 容器中啟用 AspectJ 註解支持, 只要在 Bean 配置文件中定義一個空的 XML 元素 <aop:aspectj-autoproxy>,當 Spring IOC 容器偵測到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素時, 會自動為與 AspectJ 切麵匹配的 Bean 創建代理.
4.2 用AspectJ註解聲明切麵
(1)要在Spring中聲明AspectJ切麵,需要在IOC容器中將切麵聲明為Bean實例,即加入@Component註解;
(2)在AspectJ註解中,切麵是一個帶有@Aspect註解的Java類,即加入@Aspect註解;
4.3 在類中聲明各種通知
(1)聲明一個方法;
(2)在方法前加入通知註解。
5.AspectJ 支持 5 種類型的通知註解
- @Before: 前置通知, 在方法執行之前執行
- @After: 後置通知, 在方法執行之後執行
- @AfterRunning: 返回通知, 在方法返回結果之後執行
- @AfterThrowing: 異常通知, 在方法拋出異常之後
- @Around: 環繞通知, 圍繞著方法執行
5.1 前置通知
在方法執行之前執行的通知。前置通知使用 @Before 註解, 並將切入點表達式的值作為註解值。
示例代碼:
定義介面:ArithmeticCalculator.java
package com.java.spring.aop.impl; public interface ArithmeticCalculator { int add(int i,int j); int sub(int i,int j); int mul(int i,int j); int div(int i,int j); }
介面的實現類:ArithmeticCalculatorImpl.java
package com.java.spring.aop.impl; import org.springframework.stereotype.Component; @Component("arithmetiCalculator") public class ArithmeticCalculatorImpl implements ArithmeticCalculator { @Override public int add(int i, int j) { int result = i+j; return result; } @Override public int sub(int i, int j) { int result = i-j; return result; } @Override public int mul(int i, int j) { int result = i*j; return result; } @Override public int div(int i, int j) { int result = i/j; return result; } }
日誌LoggingAspect.java
package com.java.spring.aop.impl; import java.util.Arrays; import java.util.List; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))") public void beforeMethod(JoinPoint joinpoint){ String methodName=joinpoint.getSignature().getName(); List<Object> args=Arrays.asList(joinpoint.getArgs()); System.out.println("The method "+methodName+" begins with args "+args); } }
在applicationContext.xml中進行配置:
<context:component-scan base-package="com.java.spring.aop.impl"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
測試:
Main.java
package com.java.spring.aop.impl; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args){ ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml"); ArithmeticCalculator ac=(ArithmeticCalculator) ctx.getBean("arithmetiCalculator"); int result1=ac.add(123, 10); System.out.println(result1); int result2=ac.sub(123, 10); System.out.println(result2); } }
運行後輸出:
The method add begins with args [123, 10] 133 The method sub begins with args [123, 10] 113
(1)LoggingAspect.java中,
@Before("execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))")
public void beforeMethod(JoinPoint joinpoint){...}
標示beforeMethod()方法是前置通知,切點表達式表示執行ArithmeticCalculator介面中參數為兩個int類型,並且方法修飾符為public和返回值為int類型的所有方法。
最典型的切入點表達式時根據方法的簽名來匹配各種方法:
- execution * com.java.spring.aop.impl.ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中聲明的所有方法,第一個 * 代表任意修飾符及任意返回值. 第二個 * 代表任意方法. .. 匹配任意數量的參數. 若目標類與介面與該切麵在同一個包中, 可以省略包名.
- execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 介面的所有公有方法.
- execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中返回 double 類型數值的方法
- execution public double ArithmeticCalculator.*(double, ..): 匹配第一個參數為 double 類型的方法, .. 匹配任意數量任意類型的參數
- execution public double ArithmeticCalculator.*(double, double): 匹配參數類型為 double, double 類型的方法.
(2)讓通知訪問當前連接點的細節。可以在通知方法中聲明一個類型為 JoinPoint 的參數. 然後就能訪問鏈接細節. 如方法名稱和參數值.
String methodName=joinpoint.getSignature().getName(); List<Object> args=Arrays.asList(joinpoint.getArgs());
5.2 後置通知
後置通知是在連接點完成之後執行的, 即連接點返回結果或者拋出異常的時候. 一個切麵可以包括一個或者多個通知。
@After("execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))") public void afterMethod(JoinPoint joinpoint){ String methodName=joinpoint.getSignature().getName(); List<Object> args=Arrays.asList(joinpoint.getArgs()); System.out.println("The method "+methodName+" ends with args "+args); }
5.3 返回通知
無論連接點是正常返回還是拋出異常, 後置通知都會執行. 如果只想在連接點返回的時候記錄日誌, 應使用返回通知代替後置通知,返回通知是可以訪問到方法的返回值的。
在返回通知中, 只要將 returning 屬性添加到 @AfterReturning 註解中, 就可以訪問連接點的返回值. 該屬性的值即為用來傳入返回值的參數名稱. 而且必須在通知方法的簽名中添加一個同名參數. 在運行時, Spring AOP 會通過這個參數傳遞返回值.
@AfterReturning(value="execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))", returning="result") public void reutrnMethod(JoinPoint joinpoint,Object result){ String methodName=joinpoint.getSignature().getName(); List<Object> args=Arrays.asList(joinpoint.getArgs()); System.out.println("The method "+methodName+" ends with args "+args+"and the result is "+result); }
5.4 異常通知
只在連接點拋出異常時才執行異常通知。將 throwing 屬性添加到 @AfterThrowing 註解中, 也可以訪問連接點拋出的異常. Throwable 是所有錯誤和異常類的超類. 所以在異常通知方法可以捕獲到任何錯誤和異常.如果只對某種特殊的異常類型感興趣, 可以將參數聲明為其他異常的參數類型. 然後通知就只在拋出這個類型及其子類的異常時才被執行。
@AfterThrowing(value="execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))", throwing="e") public void afterThrowing(JoinPoint joinpoint,Exception e){ String methodName=joinpoint.getSignature().getName(); List<Object> args=Arrays.asList(joinpoint.getArgs()); System.out.println("The method "+methodName+" occurs "+args+e); }
5.5 環繞通知
環繞通知是所有通知類型中功能最為強大的, 能夠全面地控制連接點. 甚至可以控制是否執行連接點。對於環繞通知來說, 連接點的參數類型必須是 ProceedingJoinPoint ,可以決定是否執行目標方法。它是 JoinPoint 的子介面, 允許控制何時執行, 是否執行連接點。在環繞通知中需要明確調用 ProceedingJoinPoint 的 proceed() 方法來執行被代理的方法. 如果忘記這樣做就會導致通知被執行了, 但目標方法沒有被執行。
@Around("execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))") public Object aroundMethod(ProceedingJoinPoint pjd){ Object result=null; String methodName=pjd.getSignature().getName(); try { //前置通知 System.out.println("The method "+methodName+" begins with args "+Arrays.asList(pjd.getArgs())); //執行目標方法 result=pjd.proceed(); //返回通知 System.out.println("The method "+"ends with "+result); } catch (Throwable e) { //異常通知 System.out.println("The method occurs Exception "+e); } //後置通知 System.out.println("The method ends"); return result; }
6.切麵的優先順序
在同一個連接點上應用不止一個切麵時, 除非明確指定, 否則它們的優先順序是不確定的.切麵的優先順序可以通過實現 Ordered 介面或利用 @Order 註解指定.實現 Ordered 介面, getOrder() 方法的返回值越小, 優先順序越高;若使用 @Order 註解, 序號出現在註解中。
@Aspect @Order(0) @Component public class LoggingAspect {}
7.重用切入點表達式
在 AspectJ 切麵中, 可以通過 @Pointcut 註解將一個切入點聲明成簡單的方法. 切入點的方法體通常是空的。後面的其他通知直接使用方法名來引用當前的接入點表達式。
切入點方法的訪問控制符同時也控制著這個切入點的可見性. 如果切入點要在多個切麵中共用, 最好將它們集中在一個公共的類中. 在這種情況下, 它們必須被聲明為 public. 在引入這個切入點時, 必須將類名也包括在內. 如果類沒有與這個切麵放在同一個包中, 還必須包含包名.
//定義一個方法,用於聲明一個切入點表達式 @Pointcut("execution(public int com.java.spring.aop.impl.ArithmeticCalculator.*(int, int))") public void declareJointPointExpression(){} @Before("declareJointPointExpression()") public void beforeMethod(JoinPoint joinpoint){ String methodName=joinpoint.getSignature().getName(); List<Object> args=Arrays.asList(joinpoint.getArgs()); System.out.println("The method "+methodName+" begins with args "+args); }