4-Spring框架 之 AOP
-
- AOP簡介
AOP(Aspect Orient Programming),面向切面程式設計,是面向物件程式設計(OOP)的一種補充,面向物件程式設計是從靜態角度考慮程式的結構,而面向切面程式設計是從動態角度考慮程式執行過程。
AOP底層,就是採用動態代理模式實現的。採用了兩種代理:JDK的動態代理,與CGLIB的動態代理。
面向切面程式設計,就是將交叉 業務邏輯封裝稱切面,利用AOP容器的功能將切面織入到主業務邏輯中。所謂交叉業務邏輯是指,通用的、與主業務邏輯無關的程式碼,如安全檢查、事務、日誌等。
若不使用AOP,則會出現程式碼糾纏,即交叉業務邏輯與主業務邏輯混合在一起。這樣,會使主業務邏輯變的混雜不清。
-
-
- AOP程式設計的術語
-
- 切面(Aspect)
切面泛指交叉業務邏輯。常用的切面有通知與顧問。實際就是對主業務邏輯的一種增強。
- 織入(Weaving)
織入就是指將切面程式碼插入到目標物件的過程。
- 連線點(JoinPoint)
通常業務介面中的方法均為連線點
- 切入點(Pointcut)
切入點指切面具體織入的方法。在StudentServiceImpl類中,若doSome()將被增強,而doOther()不被增強,則doSome()為切入點,而doOther()僅為連線點。
被標記為final的方法不可作為切入點。
- 目標物件(Target)
指將要被增強的物件,即包含主業務邏輯的類的物件。
- 通知(Advice)
通知是切面的一種實現,可以完成簡單織入的功能(織入功能就是在這裡完成的)。換個角度說,通知定義了增強程式碼切入到目的碼的時間點,是目標方法執行之前執行,還是之後執行等。通知型別不同,切入時間不同。
切入點定義切入的位置,通知定義切入的時間。
- 顧問(Advisor)
切面的另一種實現,是將通知包裝為更復雜切面的裝配器。
1.2 通知詳解
1.2.1 前置通知MethodBeforeAdvice
定義前置通知,需要實現MethodBeforeAdvice介面,該介面中有一個方法before(),會在目標方法執行之前執行。前置通知的特點:
- 在目標方法執行之前執行
- 不改變目標方法的執行流程,前置通知程式碼不能阻止目標方法執行
- 不改變目標方法執行的結果
1.2.2 後置通知AfterReturningAdvice
定義後置通知,需要實現借樓AfterReturningAdvice。該介面中有一個方法afterReturning(),會在目標方法執行之後執行。後置通知的特點:
- 在目標方法執行之後執行
- 不改變目標方法執行的流程,後置通知程式碼不能阻止目標方法的執行
- 不改變目標方法執行的結果
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
//後置通知:可以獲取到目標方法的返回值,但無法改變目標方法的結果
public class MyAfterReturningAdvice implements AfterReturningAdvice {
//returnValue:目標方法的返回值
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("目標方法執行之後執行。。。。");
if(returnValue!=null){
returnValue = ((String)returnValue).toUpperCase();
}
System.out.println("returnValue = "+returnValue);
}
}
1.2.3 環繞通知 MethodInterceptor
定義環繞通知,需要實現MethodInterceptor介面。環繞通知也叫方法攔截器,可以在目標方法執行之前之後做處理,可以改變目標方法的的返回值,也可以改變程式執行流程。
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//環繞通知
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("執行環繞通知:目標方法執行之前");
//執行目標方法,result為目標方法的返回值
Object result = invocation.proceed();
System.out.println("執行環繞通知:目標方法執行之後");
return result;
}
}
1.2.4 異常通知 ThrowsAdvice
package edu.sdut.service;
import org.springframework.aop.ThrowsAdvice;
//異常通知
public class MyThrowsAdvice implements ThrowsAdvice {
/**
* 當目標方法丟擲與指定型別的異常具有is-a關係的異常時,
* 執行當前方法
* @param ex
*/
public void afterThrowing(Exception ex){
System.out.println("執行異常通知方法。。。");
}
}
1.3顧問 Advisor
通知(Advice)是Spring提供的一種切面(Aspect)。但其功能過於簡單:只能將切面織入到目標類的所有目標方法中,無法完成將切面織入到指定的目標方法中。
顧問(Advisor)是Spring提供的另一種切面,其可以完成更為複雜的切面織入功能。
PointcutAdvisor是顧問的一種,可以指定具體的切入點。顧問將通知進行了包裝,會根據不同的通知型別,在不同的時間點,將切面織入到不同的切入點。
PointcutAdvisor介面有兩個較為常用的實現類:
- NameMatchMethodPointcutAdvisor名稱匹配方法切入點顧問。
- RegexpMethodPointcutAdvisor正則表示式匹配方法切入點顧問。
1.3.1名稱匹配方法切入點顧問
NameMatchMethodPointcutAdvisor,即名稱匹配方法切入點顧問。容器可根據配置檔案中指定的方法名來設定切入點。
程式碼不同修改,只在配置檔案中註冊一個顧問,然後使用通知屬性advice與切入點的方法名mapperName對其進行配置。代理中的切面,使用這個給顧問即可。
以上程式碼存在的兩個問題:
- 若存在多個目標物件,就需要使用多次ProxyFactoryBean來建立多個代理物件,這會使配置檔案變得臃腫,不便於管理
- 使用者真正想呼叫的是目標物件,而真正可以呼叫的卻是代理物件,這不符合邏輯。
以上連個原因是由於ProxyFactoryBean功能太簡單,解決辦法為使用自動代理生成器。
1.4 自動代理生成器
前面程式碼中所使用的代理物件,均是由ProxyFactoryBean代理工具類生成的。而該代理工具類存在如下缺點:
- 一個代理物件只能代理一個Bean,即如果有兩個Bean同時要織入同一個切面,這時,不僅要配置這兩個Bean,即兩個目標物件,同時還要配置兩個代理物件。
- 在客戶類中獲取Bean時,使用的是代理類的id,而非我們定義的目標物件Bean的id。我們正真想要執行的應該是目標物件。
Spring提供了自動代理生成器,用於解決PeroxyFactoryBean的問題。常用的自動代理生成器有兩個:
- 預設advisor自動代理生成器
存在的問題:
(2)Bean名稱自動代理生成器
- 不能選擇目標物件
- 不能選擇切面型別,切面只能時Advisor
- 不能選擇advisor,所以advisor均將被作為切面織入到目標方法
需要注意的是,自動代理生成器均繼承自Bean後處理器BeanPostProcessor。容器中所有Bean在初始化時均會自動執行Bean後處理器中的方法,故其無需id屬性。所以自動代理生成器的Bean也沒有id屬性,客戶類直接使用目標物件Bean的id。\
1.5 AspectJ對AOP的實現
對於AOP這種程式設計思想,跟讀框架都進行了實現。Spring就是其中之一,可以完成面向切面程式設計。然而,AspectJ也實現了AOP的功能,且其實現方法更為簡捷,使用更為方便,而且還支援註解式開發,所以,Spring又將AspectJ的對於AOP的實現也引入到了自己的框架中。
在Spring中使用AOP開發時,一般使用AspectJ的實現方式。
1.5.1 AspectJ的通知型別
AspectJ中常用的五種通知型別:
- 前置通知
- 後置通知
- 環繞通知
- 異常通知
- 最終通知
其中最終通知是指,無論程式執行是否正常,該通知都會執行。類似於try..catch中的finally程式碼塊。
1.5.2 AspectJ的切入點表示式
execution([modifiers-pattern] 訪問許可權型別
ret-type-pattern 返回值型別
[declaring-type-parrern] 全限定性類名
Name-pattern(param-pattern) 方法名引數()
[throws-pattrtn] 丟擲異常型別)
切入點表示式要匹配的物件就是目標方法的方法名。所以,execution表示式中明顯就是方法的簽名。注意,表示式中加[]的部分表示可以省略部分,各部分間用空格分開。在其中可以使用一下符號:
符號 |
意義 |
* |
零至多個任意字元 |
.. |
用在方法引數中,表示任意多個引數 用在包名後,表示當前包及其子包路徑 |
+ |
用在類名後,表示當前類及其子類 用在介面後,表示當前介面及其實現類 |
1.5.3 AspectJ的開發環境
(1)增加4個Jar包:aop,AspectJ,AspectJ和Spring整合的jar包,Aop聯盟的jar包
(2)在配置檔案中新增aop的約束,因為要用到aop的標籤
基於註解實現:
/**
* 使用AspectJ,實現切面程式設計
*/
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect //表示當前類為註冊
public class MyAspect {
/**
* 執行前置通知
* @param jp
*/
@Before("execution(* *..ISomeService.doFirst(..))")
public void myBefore(JoinPoint jp){
//jp表示切入點表示式
System.out.println("執行前置通知方法jp = " + jp);
}
/**
* 執行後置通知
*/
@AfterReturning(value="execution(* *..ISomeService.doSecond(..))",returning="result")
public void myAfterReturening(Object result){
//result為返回結果
System.out.println("執行後置通知方法 result = " + result);
}
/**
* 執行環繞通知
* @throws Throwable
*/
@Around("execution(* *..ISomeService.doSecond(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("執行環繞通知方法,目標方法執行之前");
//執行目標方法
Object result = pjp.proceed();
System.out.println("執行環繞通知方法,目標方法執行之後");
//因為該通知可以返回值,所以可在在這裡對結果及進行修改
if(result!=null){
result = ((String)result).toUpperCase();
}
return result;
}
/**
* 執行異常通知
*/
@AfterThrowing("execution(* *..ISomeService.doThird(..))")
public void myAfterThrowing(){
System.out.println("執行異常通知");
}
/**
* 執行自定義異常,並捕獲資訊
* @param ex
*/
@AfterThrowing(value="execution(* *..ISomeService.doThird(..))",throwing="ex")
public void myAfterThrowing(Exception ex){
System.out.println("執行異常通知方法 ex = " + ex.getMessage());
}
/**
* 執行最終通知
*/
@After("doThirdPointcut()")
public void myAfter(){
System.out.println("執行最終通知");
}
/**
* 定義了一個切入點
* 叫doThirdPointcut()
*/
@Pointcut("execution(* *..ISomeService.doThird(..))")
public void doThirdPointcut(){}
}
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- bean definitions here -->
<!-- 註冊切面 -->
<bean id="myAspect" class="edu.sdut.service.MyAspect"/>
<bean id="someService" class="edu.sdut.service.SomeServiceIMPL"></bean>
<!-- 註冊aspectj的自動代理 -->
<aop:aspectj-autoproxy/>
</beans>
基於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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- bean definitions here -->
<!-- 註冊切面 -->
<bean id="myAspect" class="edu.sdut.service.MyAspect"/>
<bean id="someService" class="edu.sdut.service.SomeServiceIMPL"></bean>
<!-- AOP配置 -->
<aop:config>
<aop:pointcut expression="execution(* *..ISomeService.doFirst(..))" id="doFirstPointCut"/>
<aop:pointcut expression="execution(* *..ISomeService.doSecond(..))" id="doSecondPointCut"/>
<aop:aspect ref="myAspect">
<aop:before method="myBefore" pointcut="execution(* *..ISomeService.doFirst(..))"/>
<aop:after-returning method="myAfterReturening" pointcut-ref="doFirstPointCut" returning="result"/>
<aop:around method="myAround" pointcut-ref="doSecondPointCut"/>
<aop:after-throwing method="myAfterThrowing(java.lang.Exception)" pointcut-ref="doSecondPointCut" throwing="ex"/>
<aop:after method="myAfter" pointcut-ref="doSecondPointCut"/>
</aop:aspect>
</aop:config>
</beans>
/**
* 使用AspectJ,實現切面程式設計
*/
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
public class MyAspect {
/**
* 執行前置通知
* @param jp
*/
public void myBefore(JoinPoint jp){
//jp表示切入點表示式
System.out.println("執行前置通知方法jp = " + jp);
}
/**
* 執行後置通知
*/
public void myAfterReturening(Object result){
//result為返回結果
System.out.println("執行後置通知方法 result = " + result);
}
/**
* 執行環繞通知
* @throws Throwable
*/
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("執行環繞通知方法,目標方法執行之前");
//執行目標方法
Object result = pjp.proceed();
System.out.println("執行環繞通知方法,目標方法執行之後");
//因為該通知可以返回值,所以可在在這裡對結果及進行修改
if(result!=null){
result = ((String)result).toUpperCase();
}
return result;
}
/**
* 執行異常通知
*/
public void myAfterThrowing(){
System.out.println("執行異常通知");
}
/**
* 執行自定義異常,並捕獲資訊
* @param ex
*/
public void myAfterThrowing(Exception ex){
System.out.println("執行異常通知方法 ex = " + ex.getMessage());
}
/**
* 執行最終通知
*/
public void myAfter(){
System.out.println("執行最終通知");
}
}