1. 程式人生 > >4-Spring框架 之 AOP

4-Spring框架 之 AOP

    1. AOP簡介

AOP(Aspect Orient Programming),面向切面程式設計,是面向物件程式設計(OOP)的一種補充,面向物件程式設計是從靜態角度考慮程式的結構,而面向切面程式設計是從動態角度考慮程式執行過程。

AOP底層,就是採用動態代理模式實現的。採用了兩種代理:JDK的動態代理,與CGLIB的動態代理。

面向切面程式設計,就是將交叉 業務邏輯封裝稱切面,利用AOP容器的功能將切面織入到主業務邏輯中。所謂交叉業務邏輯是指,通用的、與主業務邏輯無關的程式碼,如安全檢查、事務、日誌等。

若不使用AOP,則會出現程式碼糾纏,即交叉業務邏輯與主業務邏輯混合在一起。這樣,會使主業務邏輯變的混雜不清。

      1. AOP程式設計的術語
  1. 切面(Aspect)

切面泛指交叉業務邏輯。常用的切面有通知與顧問。實際就是對主業務邏輯的一種增強。

  1. 織入(Weaving)

織入就是指將切面程式碼插入到目標物件的過程。

  1. 連線點(JoinPoint)

通常業務介面中的方法均為連線點

  1. 切入點(Pointcut)

切入點指切面具體織入的方法。在StudentServiceImpl類中,若doSome()將被增強,而doOther()不被增強,則doSome()為切入點,而doOther()僅為連線點。

被標記為final的方法不可作為切入點。

  1. 目標物件(Target)

指將要被增強的物件,即包含主業務邏輯的類的物件。

  1. 通知(Advice)

通知是切面的一種實現,可以完成簡單織入的功能(織入功能就是在這裡完成的)。換個角度說,通知定義了增強程式碼切入到目的碼的時間點,是目標方法執行之前執行,還是之後執行等。通知型別不同,切入時間不同。

切入點定義切入的位置,通知定義切入的時間。

  1. 顧問(Advisor)

切面的另一種實現,是將通知包裝為更復雜切面的裝配器。

1.2 通知詳解

1.2.1 前置通知MethodBeforeAdvice

定義前置通知,需要實現MethodBeforeAdvice介面,該介面中有一個方法before(),會在目標方法執行之前執行。前置通知的特點:

  1. 在目標方法執行之前執行
  2. 不改變目標方法的執行流程,前置通知程式碼不能阻止目標方法執行
  3. 不改變目標方法執行的結果

 

 

 

1.2.2 後置通知AfterReturningAdvice

定義後置通知,需要實現借樓AfterReturningAdvice。該介面中有一個方法afterReturning(),會在目標方法執行之後執行。後置通知的特點:

  1. 在目標方法執行之後執行
  2. 不改變目標方法執行的流程,後置通知程式碼不能阻止目標方法的執行
  3. 不改變目標方法執行的結果
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介面有兩個較為常用的實現類:

  1. NameMatchMethodPointcutAdvisor名稱匹配方法切入點顧問。
  2. RegexpMethodPointcutAdvisor正則表示式匹配方法切入點顧問。

1.3.1名稱匹配方法切入點顧問

NameMatchMethodPointcutAdvisor,即名稱匹配方法切入點顧問。容器可根據配置檔案中指定的方法名來設定切入點。

程式碼不同修改,只在配置檔案中註冊一個顧問,然後使用通知屬性advice與切入點的方法名mapperName對其進行配置。代理中的切面,使用這個給顧問即可。

以上程式碼存在的兩個問題

  1. 若存在多個目標物件,就需要使用多次ProxyFactoryBean來建立多個代理物件,這會使配置檔案變得臃腫,不便於管理
  2. 使用者真正想呼叫的是目標物件,而真正可以呼叫的卻是代理物件,這不符合邏輯。

以上連個原因是由於ProxyFactoryBean功能太簡單,解決辦法為使用自動代理生成器。

1.4 自動代理生成器

前面程式碼中所使用的代理物件,均是由ProxyFactoryBean代理工具類生成的。而該代理工具類存在如下缺點:

  1. 一個代理物件只能代理一個Bean,即如果有兩個Bean同時要織入同一個切面,這時,不僅要配置這兩個Bean,即兩個目標物件,同時還要配置兩個代理物件。
  2. 在客戶類中獲取Bean時,使用的是代理類的id,而非我們定義的目標物件Bean的id。我們正真想要執行的應該是目標物件。

Spring提供了自動代理生成器,用於解決PeroxyFactoryBean的問題。常用的自動代理生成器有兩個:

  1. 預設advisor自動代理生成器

存在的問題:

 

(2)Bean名稱自動代理生成器

  1. 不能選擇目標物件
  2. 不能選擇切面型別,切面只能時Advisor
  3. 不能選擇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中常用的五種通知型別:

  1. 前置通知
  2. 後置通知
  3. 環繞通知
  4. 異常通知
  5. 最終通知

其中最終通知是指,無論程式執行是否正常,該通知都會執行。類似於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("執行最終通知");
	}
}