Spring的面向切面編程(AOP)
1,Java動態代理Demo
-
有這樣一個業務場景,設計一個計算器類,裏面有加減乘除四則運算,在裏面添加日誌功能,日誌記錄輸入的參數以及輸出的結果。如果不使用動態代理,我們會這樣寫這個功能。
寫一個計算器的接口,裏面有四則運算
public interface ArithmeticCalculator {
//加
int add (int i , int j);
//減
int sub (int i , int j);
//除
int div (int i , int j);
//乘
int mul (int i , int j);
}
實現這個接口,實現具體的計算步驟和日誌記錄
/**
* 計算器接口的實現
*
* 實現計算器的日誌功能, 日誌記錄計算的數字, 日誌記錄計算後的結果
* @author
* @version 1.00
* @time 2020/3/5 0005 下午 4:43
*/
public class ArithmeticCalculatorLoggingImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
//打印日誌
System.out.println("執行方法 add 計算的數字爲" + Arrays.asList(i,j));
//計算
int result = i + j;
//打印日誌
System.out.println("方法 add 計算的結果爲" + result);
return result ;
}
@Override
public int sub(int i, int j) {
//打印日誌
System.out.println("執行方法 sub 計算的數字爲" + Arrays.asList(i,j));
//計算
int result = i - j;
//打印日誌
System.out.println("方法 sub 計算的結果爲" + result);
return result ;
}
@Override
public int div(int i, int j) {
//打印日誌
System.out.println("執行方法 div 計算的數字爲" + Arrays.asList(i,j));
//計算
int result = i / j;
//打印日誌
System.out.println("方法 div 計算的結果爲" + result);
return result ;
}
@Override
public int mul(int i, int j) {
//打印日誌
System.out.println("執行方法 mul 計算的數字爲" + Arrays.asList(i, j));
//計算
int result = i * j;
//打印日誌
System.out.println("方法 mul 計算的結果爲" + result);
return result;
}
}
我們完成了一開始制定的目標,實現了計算和日誌功能,但是這個計算器存在着很多致命的缺陷:
①代碼混亂:越來越多的非業務需求(日誌和驗證等)加入後,原有的業務方法急劇膨脹。每個方法在處理核心邏輯的同時還必須兼顧其他多個關注點。
②代碼分散: 以日誌需求爲例,只是爲了滿足這個單一需求,就不得不在多個模塊(方法)裏多次重複相同的日誌代碼。如果日誌需求發生變化,必須修改所有模塊。
- 我們可以使用動態代理實現上述功能,代理模式原理:使用一個代理將原本對象包裝起來,然後用該代理對象”取代”原始對象。任何對原始對象的調用都要通過代理。代理對象決定是否以及何時將方法調用轉到原始對象上。
-
- 下面的demo爲使用動態 代理實現的日誌功能
/**
* jdk動態代理demo,
* @author
* @version 1.00
* @time 2020/3/7 0007 下午 1:54
*/
public class ArithmeticCalculatorProxy2 implements InvocationHandler {
//被代理對象,,
private Object target;
//有參構造器 對被代理對象進行實例化
public ArithmeticCalculatorProxy2(Object target) {
this.target = target;
}
//invoke方法,,調用此方法,實際上是調用的被代理對象的代理方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//日誌
System.out.println("計算器開始運行["+ method.getName()+"]方法,參數爲"+ Arrays.asList(args) +"." );
//實際調用,獲取結果
Object result = method.invoke(target, args);
//日誌
System.out.println("計算器計算結束,計算方法爲["+ method.getName()+"],結果爲["+ result +"]." );
//返回結果
return result;
}
//產生代理對象
public static Object getProxyInstance(Object target){
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
//jkd自帶的生成動態代理的接口
//三個參數 被代理對象的類加載器,被代理對象的接口,InvocationHandler接口的實現類
Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, new ArithmeticCalculatorProxy2(target));
//返回代理對象
return proxyInstance;
}
}
測試
public class ArithmeticCalculatorTest {
public static void main(String[] args) {
ArithmeticCalculatorImpl arithmeticCalculator = new ArithmeticCalculatorImpl();
ArithmeticCalculator proxyInstance =(ArithmeticCalculator)ArithmeticCalculatorProxy2.getProxyInstance(arithmeticCalculator);
proxyInstance.add(1,2);
}
}
打印臺輸出:
計算器開始運行[add]方法,參數爲[1, 2].
計算器計算結束,計算方法爲[add],結果爲[3].
2,AOP概念理解
- AOP概述:
- AOP(Aspect-Oriented Programming,面向切面編程):是一種新的方法論,是對傳
統 OOP(Object-Oriented Programming,面向對象編程)的補充。
面向對象 縱向繼承機制
面向切面 橫向抽取機制 - AOP編程操作的主要對象是切面(aspect),而切面模塊化橫切關注點(公共功能)。
- 在應用AOP編程時,仍然需要定義公共功能,但可以明確的定義這個功能應用在哪裏,以什麼方式應用,並且不必修改受影響的類。這樣一來橫切關注點就被模塊化到特殊的類裏——這樣的類我們通常稱之爲“切面”。
- AOP的好處:
① 每個事物邏輯位於一個位置,代碼不分散,便於維護和升級
② 業務模塊更簡潔,只包含核心業務代碼 - AOP的底層原理是動態代理,由Spring框架生成代理對象,對原有的方法進行增強。
- AOP(Aspect-Oriented Programming,面向切面編程):是一種新的方法論,是對傳
- AOP中的概念:
- 橫切關注點:從每個方法中抽取出來的同一類非核心業務。
- 切面(Aspect):封裝橫切關注點信息的類,每個關注點體現爲一個通知方法。
- 通知(Advice):切面必須要完成的各個具體工作。
- 目標(Target):被通知的對象,,即被代理對象。
- 代理(Proxy):向目標對象應用通知之後創建的代理對象。
- 連接點(Joinpoint):橫切關注點在程序代碼中的具體體現,對應程序執行的某個特定位置。例如:類某個方法調用前、調用後、方法捕獲到異常後等。在應用程序中可以使用橫縱兩個座標來定位一個具體的連接點:縱座標:通知位於被增強方法的什麼位置,方法執行前,方法執行後,方法執行出現異常後,方法正常返回後。 橫座標:目標中的某個方法。
- 切入點(pointCut):定位連接點的方式。每個類的方法中都包含多個連接點,所以連接點是類中客觀存在的事物。如果把連接點看作數據庫中的記錄,那麼切入點就是查詢條件——AOP可以通過切入點定位到特定的連接點。切點通過org.springframework.aop.Pointcut 接口進行描述,它使用類和方法作爲連接點的查詢條件。
3,AspectJ框架簡介
- AspectJ:Java社區裏最完整最流行的AOP框架。在Spring2.0以上版本中,可以使用基於AspectJ註解或基於XML配置的AOP。
- 如果不是maven項目,需要放入AspectJ的框架,如果是maven項目,放入依賴即可。
- l com.springsource.net.sf.cglib-2.2.0.jar
l com.springsource.org.aopalliance-1.0.0.jar
l com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
l spring-aop-4.0.0.RELEASE.jar
l spring-aspects-4.0.0.RELEASE.jar
需要放入的jar包
- l com.springsource.net.sf.cglib-2.2.0.jar
- 在spring的配置文件中引入aop名稱空間。
- 開啓自動代理的配置,如果是springBoot項目則無需配置。
<aop:aspectj-autoproxy>
當Spring IOC容器偵測到bean配置文件中的aop:aspectj-autoproxy元素時,會自動爲 與AspectJ切面匹配的bean創建代理。
4,用AspectJ註解聲明切面
- 要在Spring中聲明AspectJ切面,只需要在IOC容器中將切面聲明爲bean實例。
- 當在Spring IOC容器中初始化AspectJ切面之後,Spring IOC容器就會爲那些與 AspectJ切面相匹配的bean創建代理。
- 在AspectJ註解中,切面只是一個帶有@Aspect註解的Java類,它往往要包含很多通知。
- 通知是標註有某種註解的簡單的Java方法。
- AspectJ支持5種類型的通知註解:
- @Before:前置通知,在方法執行之前執行
- @After:後置通知,在方法執行之後執行
- @AfterRunning:返回通知,在方法返回結果之後執行
- @AfterThrowing:異常通知,在方法拋出異常之後執行
- @Around:環繞通知,圍繞着方法執行
5,AOP細節
5.1切入點表達式
- 作用:通過表達式的方式定位一個或多個具體的連接點。
- 語法細節:
- 切入點表達式的語法格式
1`. [execution([權限修飾符] [返回值類型] [簡單類名/全類名] 方法名)([參數列表]))]`
- 舉例說明
表達式 execution(* com.atguigu.spring.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類型的方法
在AspectJ中,切入點表達式可以通過 “&&”、“||”、“!”等操作符結合起來。
表達式 execution (* .add(int,..)) \|\| execution( *.sub(int,..))
含義 任意類中第一個參數爲int類型的add方法或sub方法
表達式 !execution (* *.add(int,..))
含義 匹配不是任意類中第一個參數爲int類型的add方法
- 切入點表達式應用到實際的切面類中
此例子爲一個前置通知
@Aspect //標註爲一個切面類
@Component //添加到ioc容器中
public class CalculatorAspect {
//前置通知,通過切入點表達式確定切入點
@Before("execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))")
public void logBefore(JoinPoint joinPoint){
//獲取輸入的參數
Object[] args = joinPoint.getArgs();
//獲取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("計算器開始執行,方法爲["+methodName+"],輸入參數:"+ Arrays.asList(args));
}
}
5.2 當前連接點細節
- 概述:切入點表達式通常都會是從宏觀上定位一組方法,和具體某個通知的註解結合起來就能夠確定對應的連接點。那麼就一個具體的連接點而言,我們可能會關心這個連接點的一些具體信息,例如:當前連接點所在方法的方法名、當前傳入的參數值等等。這些信息都封裝在JoinPoint接口的實例對象中。
- JointPoint接口:
5.3 通知
- 概述:
- 在具體的連接點上要執行的操作。
- 一個切面可以包括一個或者多個通知。
- 通知所使用的註解的值往往是切入點表達式。
- 前置通知
- 前置通知:在方法執行之前執行的通知
使用@Before註解
- 前置通知:在方法執行之前執行的通知
- 後置通知
- 後置通知:後置通知是在連接點完成之後執行的,即連接點返回結果或者拋出異常的時候
使用@After註解
- 後置通知:後置通知是在連接點完成之後執行的,即連接點返回結果或者拋出異常的時候
- 返回通知
- 返回通知:無論連接點是正常返回還是拋出異常,後置通知都會執行。如果只想在連接點返回的時候記錄日誌,應使用返回通知代替後置通知。
- 使用@AfterReturning註解,在返回通知中訪問連接點的返回值
①在返回通知中,只要將returning屬性添加到@AfterReturning註解中,就可以訪問連接點的返回值。該屬性的值即爲用來傳入返回值的參數名稱
②必須在通知方法的簽名中添加一個同名參數。在運行時Spring AOP會通過這個參數傳遞返回值
③原始的切點表達式需要出現在pointcut屬性中
@Aspect //標註爲一個切面類
@Component //添加到ioc容器中
public class CalculatorAspect {
/**
* 返回通知,,在@AfterReturning註解中,使用returning屬性指定返回值的名稱,接可以在方法中使用返回值
* @param joinPoint
* @param result
*/
@AfterReturning(value = "execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))",
returning = "result")
public void logReturning(JoinPoint joinPoint, Object result){
//獲取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("計算器執行結束,方法爲["+methodName+"],返回結果:"+ result);
}
}
- 異常通知
- 異常通知:只在連接點拋出異常時才執行異常通知
- 將throwing屬性添加到@AfterThrowing註解中,也可以訪問連接點拋出的異常。Throwable是所有錯誤和異常類的頂級父類,所以在異常通知方法可以捕獲到任何錯誤和異常。
- 如果只對某種特殊的異常類型感興趣,可以將參數聲明爲其他異常的參數類型。然後通知就只在拋出這個類型及其子類的異常時才被執行。
注意的是,在指定異常時,可以指定某個特定的異常類型,比如指定空指針異常,那麼發送數學異常就不會執行通知,所以,我們一般指定一個較大的異常類型(如:Exception 類型)進行異常捕獲。
- 環繞通知
- 環繞通知是所有通知類型中功能最爲強大的,能夠全面地控制連接點,甚至可以控制是否執行連接點。
- 對於環繞通知來說,連接點的參數類型必須是ProceedingJoinPoint。它是 JoinPoint的子接口,允許控制何時執行,是否執行連接點。
- 在環繞通知中需要明確調用ProceedingJoinPoint的proceed()方法來執行被代理的方法。如果忘記這樣做就會導致通知被執行了,但目標方法沒有被執行。
ProceedingJoinPoint.proceed() 方法,相當於動態代理中的InvocationHandler接口中的invoke方法中的 method.invoke(target, args)方法。 - 注意:環繞通知的方法需要返回目標方法執行之後的結果,即調用 joinPoint.proceed();的返回值,否則會出現空指針異常。
@Aspect //標註爲一個切面類
@Component //添加到ioc容器中
public class CalculatorAspect {
/**
* 環繞通知
* @param joinPoint 的類型必須爲ProceedingJoinPoint,
*/
@Around(value = "execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))")
public void logAround(ProceedingJoinPoint joinPoint){
//獲取輸入的參數
Object[] args = joinPoint.getArgs();
//獲取方法名
String methodName = joinPoint.getSignature().getName();
try {
//前置通知
System.out.println("計算器開始執行,方法爲["+methodName+"],輸入參數:"+ Arrays.asList(args));
//如果目標方法有返回值,必須要返回方法執行的結果,否則會出現空指針異常 此處只是demo 沒有返回執行結果
Object proceed = joinPoint.proceed();
//返回通知 正確返回結果
System.out.println("計算器開始完成,方法爲["+methodName+"]");
} catch (Throwable throwable) {
//異常通知
System.out.println("計算器開始完成,方法爲["+methodName+"]");
//異常進行拋出,如果不拋出的情況下,目標方法額外的異常通知會失效,額外的異常(@AfterThrowing)
throw new RuntimeException("環繞通知代理異常");
}
//後置通知
System.out.println("計算器執行結束,方法爲["+methodName+"],返回結果:"+ methodName);
}
}
5.4重用切入點表達式
- 在編寫AspectJ切面時,可以直接在通知註解中書寫切入點表達式。但同一個切點表達式可能會在多個通知中重複出現。
- 在AspectJ切面中,可以通過@Pointcut註解將一個切入點聲明成簡單的方法。切入點的方法體通常是空的,因爲將切入點定義與應用程序邏輯混在一起是不合理的。
- 切入點方法的訪問控制符同時也控制着這個切入點的可見性。如果切入點要在多個切面中共用,最好將它們集中在一個公共的類中。在這種情況下,它們必須被聲明爲public。在引入這個切入點時,必須將類名也包括在內。如果類沒有與這個切面放在同一個包中,還必須包含包名。
- 其他通知可以通過方法名稱引入該切入點。
5.5 指定切面的優先級
- 在同一個連接點上應用不止一個切面時,除非明確指定,否則它們的優先級是不確定的。
- 切面的優先級可以通過實現Ordered接口或利用@Order註解指定。
- 實現Ordered接口,getOrder()方法的返回值越小,優先級越高。
- 若使用@Order註解,序號出現在註解中。
//前置通知,通過切入點表達式確定切入點
@Before("execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))")
@Order(1) //數組越小,優先級越高
public void logBefore(JoinPoint joinPoint){
//獲取輸入的參數
Object[] args = joinPoint.getArgs();
//獲取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("計算器開始執行,方法爲["+methodName+"],輸入參數:"+ Arrays.asList(args));
}