spring高級之AOP詳解

前言

這是之前開始學spring的時候的筆記,現在添加了一些理解,然後把他搬到博客上來。

動態代理模式演示:

這裏僅是動態代理的演示,要查看詳細的可以查閱相關博文。
動態代理的本質就是增強對象方法,在不修改目標類的情況動態生成一個代理類和代理對象,然後在目標對象的方法執行前、後、等地方可以執行一點邏輯。比如日誌等。

建議要理解Spring的AOP之前要理解好動態代理,因爲AOP底層是動態代理。

以下是基於jdk的動態代理寫的demo,jdk的動態代理只能代理接口,要代理類的話可以使用cglib動態代理。


/**
 * 目標接口,就是要代理的接口
 */
public interface MathI {

    public  Integerdivision(int i,int j);
}

/**
 * 目標實現類,就是要增強的類。
 */
public class MathImpl implements MathI {
    @Override
    public Integer division(int i, int j) {
        return i / j;
    }
}

/**
 * 日誌工具類
 */
public class MyLogger {

    public void before(String methodName,Object[] args){
        System.out.println(methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }

    public void after(String methodName,Object result){
        System.out.println(methodName + "執行後,結果{" + result + "}");
    }

    public void throwing(String methodName,Throwable e){
        System.out.println(methodName + "執行拋出異常{" + e.getCause() + "}");
    }

    public void always(){
        System.out.println("總是你");
    }
}

/**
 * 代理工廠,用於生成代理對象。
 */
public class ProxyFactory {

    //目標對象
    private Object targetObject;

    //目標對象的Class類對象
    private Class targetClass;

    //目標類實現的接口
    private Class[] interfaces;

    //類加載器
    private ClassLoader classLoader;

    //日誌工具類
    private MyLogger myLogger = new MyLogger();

    //一個接口。代理對象執行目標接口對應的方法,其實就是執行invoke方法。
    private InvocationHandler handler = new InvocationHandler() {
        /**
         *
         * @param proxy 代理對象
         * @param method  目標對象的方法
         * @param args  方法的參數
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            Object result = null;
            //在目標對象方法執行前的動作
            myLogger.before(methodName,args);
            try {
                //執行目標方法。
                result = method.invoke(targetObject,args);

            }catch (Throwable e){
                //目標方法執行拋出異常後執行的動作
                myLogger.throwing(methodName,e);
                throw e;
            }finally {
                //總是會執行的動作
                myLogger.always();
            }

            //執行目標方法之後執行的動作
            myLogger.after(methodName,result);
            return result;
        }
    };

    public ProxyFactory(Object targetObject) {
        this.targetObject = targetObject;
        this.targetClass = targetObject.getClass();
        this.classLoader = this.targetClass.getClassLoader();
        this.interfaces = this.targetClass.getInterfaces();
    }

    public Object getProxy(){
        //生成一個代理對象
        return Proxy.newProxyInstance(classLoader, interfaces,handler);
    }
}

結果:
在這裏插入圖片描述
有異常時的結果:異常後就不會執行after()方法了。
在這裏插入圖片描述

AOP

概述

AOP(Aspect Oriented Programming):面向切面編程,是一種方法論,是一種對OOP面向對象編程的補充。

傳統的OOP面向對象編程如果要實現上述功能,在方法執行的前後加日誌等一些公共的,與業務邏輯無關的代碼,要修改原有代碼,把非業務代碼耦合到業務代碼中。或者使用繼承實現子類來實現功能的增強。屬於縱向繼承機制,這樣會使得代碼耦合度過高,繼承體系複雜。

/**
* 使用OOP繼承實現上面的日誌功能。但無疑耦合度會變高和繼承關係會邊複雜。
*/
public class MathImplPlus extends MathImpl {

    private MyLogger myLogger = new MyLogger();

    @Override
    public Integer division(int i, int j) {
        String methodName = "division";
        myLogger.before(methodName,new Object[]{i,j});
        Integer result = null;
        try {
            result = super.division(i,j);
            myLogger.after(methodName,result);
            return result;
        }catch (Throwable e){
            myLogger.throwing(methodName,e);
        }finally {
            myLogger.always();
        }
       
        return result;
    }
}

AOP:面向切面,使用的是橫向抽取機制。將公共的、與業務無關的代碼抽取成一個切面,然後在程序運行時,把他切入到相應的方法代碼處。

AOP的好處:

  • 將公共代碼抽取出來,代碼不分散,便於維護和升級。
  • 使得業務模塊更加簡潔,只有核心業務代碼。

AOP圖解:
在這裏插入圖片描述

AOP術語:

AOP術語是在進行AOP編碼時一些必要的東西,要理解式記着,在編寫AOP代碼時會如魚得水。

橫切關注點:

從目標方法中抽取出來的同一類非業務邏輯代碼,比如上面demo的日誌前置邏輯,日誌後置邏輯等都是一個個關注點。類比上面的MyLogger類中的四個方法。

切面:

由同一類(不同類也行)橫向關注點組成(封裝成)的一個類,切面就是由橫切點組成的類。類比上面的MyLogger類。

通知:

通知相對於目標對象的目標方法而言,就是把關注點作用在目標方法的哪個時期,是方法調用前還是返回後等等。每個關注點就體現爲一個個通知方法。

通知可以分爲五類(實際上就是4類):

  • 前置通知:在方法調用前調用的通知方法。類比爲上面的before方法。用@Before註解定義。
  • 在這裏插入圖片描述
  • 後置返回通知:在方法執行正常返回後(沒有拋出異常)執行的通知方法。類比爲上面的after()方法。如果方法沒有正常返回,例如拋出異常,就不會執行該後置返回通知。
  • 異常通知:目標方法執行拋出異常後執行的通知方法。類比爲上面的throwing方法。
  • 最終通知:目標方法執行後執行的通知方法,與後置返回通知的區別是最終通知無論方法是否正常返回都會執行的通知方法。類比上面的always方法。
目標對象:

說白了就是要被代理的對象,就是需要增強的對象,類比上面的MathImpl。

連接點:

就是要進行增強的方法,目標方法,類比上面的MathImpl類中的division方法。

切入點:

定位到連接點的方式,在spring裏面就是一個表達式,使用該表達式可以定位要進行增強的連接點。

AOP流程:

  1. 定義切面。
  2. 定義關注點,並且定義關注點的通知類型。
  3. 使用切入點表達式定義關注點需要關注增強哪些連接點。
  4. 使用關注點橫切到連接點上。

AspectJ框架:

AspectJ是java社區裏面最流行的AOP框架。
在spring2.0以上,可以使用基於AspectJ註解或者使用基於XML配置的AOP。

所需要的基本jar包:
在這裏插入圖片描述

切入點表達式

切入點表達式是通知定位到相應連接點的表達式,相當於一個條件。spring會根據該條件定位到指定的連接點。

語法:
execution(權限修飾符 返回值類型的全限定名 類全限定名.方法名(參數類型列表))

比如上面如果要定位到division方法,可以使用切入點表達式:

//這個是精確匹配,直接匹配到MathImpl方法。也可以實現模糊匹配,匹配條件符合的連接點。
"execution(public java.lang.Integer com.cong.springdemo.aopdemo.MathImpl.division(int,int))"

模糊匹配語法:

  1. 使用 * 號可以代表任意任意(返回值類型 + 權限修飾符)、包名、方法名。
  2. 使用 .. 符號可以代表任意參數列表。
//下面兩個表達式代碼任意方法。第一個*代表(任意返回值類型 + 任意權限修飾符),用一個*號代表2個。不能用兩個。
//第二個*號代表任意方法名。  或者第二個表達式中 *.*代表任意類中的任意方法名。
//..符號代表任意參數類型列表。
"execution(* *(..))"  或者 "execution(* *.*(..))"

//com.cong.springdemo.aopdemo包下的任意類中的任意方法。如果是jdk動態代理實現的話是接口中的方法。
"execution(* com.cong.springdemo.aopdemo.*.*(..))"

//com.cong.{任意包名}.aopdemo.任意類名.任意方法名  的方法。*也代表了一個包層級結構。
"execution(* com.cong.*.aopdemo.*.*(..))"


Spring AOP的相關注解和xml配置:

@Before 作用在橫切點方法上,標記該橫切點爲前置通知。
@After 作用在橫切點方法上,標記該橫切點爲最終通知。
@AfterReturning 作用在橫切點方法上,標記該橫切點爲成功返回通知。
@AfterThrowing 作用在橫切點方法上,標記該橫切點爲異常通知。
@Around 作用在橫切點方法上,標記該橫切點爲環繞通知。
@Aspect 作用在類上,標記該類爲切面。
@Pointcut 用於定義一個切入點表達式,用於實現表達式的複用。
@Order 用於定義切面的優先級或者橫切關注點的優先級。

必要點:切面和目標對象都要成爲spring的組件,讓spring管理才能實現AOP。

配置文件配置要點:

  1. 要開啓AspectJ切面自動代理
  2. 要開啓包掃描。
  <!-- 開啓基於aspectj的AOP自動代理 -->
   <aop:aspectj-autoproxy />
  
  <!-- 掃描組件 -->
  <context:component-scan base-package="com.cong.springdemo"/>
/**
 * 目標接口
 */
public interface MathI {

    public  Integer division(int i,int j);
}

@Component  //目標對象也要交由spring ioc容器管理
public class MathImpl implements MathI {
    @Override
    public Integer division(int i, int j) {
        System.out.println("執行division中");
        return i / j;
    }
}


/**
 * 日誌工具切面類
 */

@Aspect   //此註解把MyLogger類標註爲一個切面
@Component  //切面對象要交給交給spring 容器管理才能實現AOP。
public class MyLogger {

	//前置通知,用@Before註解定義。裏面的value值是切入點表達式,表示該通知要作用到哪些連接點上。
	@Before(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
    public void before(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println(methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }

	@AfterReturning(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))",
			returning = "result")
    public void after(JoinPoint point,Object result){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "執行後,結果{" + result + "}");
    }

	@AfterThrowing(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))",
			throwing = "e")
    public void throwing(JoinPoint point,Throwable e){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "執行拋出異常{" + e + "}");
    }

	@After(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
    public void always(){
        System.out.println("總是你");
    }
}

//測試類
public class Test {
	
	public static void main(String[] args) {
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
		//生成的代理類會實現與目標類一樣的接口,所以目標類與代理類實際上屬於“兄弟類”,
		//不能相互轉型,只能轉型爲們的接口。
		MathI mathI = (MathI) context.getBean("mathImpl");
		mathI.division(5, 0);
	}

}

spring-aop.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"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd">
  
  
  <!-- 開啓基於aspectj的AOP自動代理 -->
   <aop:aspectj-autoproxy />
  
  <!-- 掃描組件 -->
  <context:component-scan base-package="com.cong.springdemo"/>
  
 
   		
 
</beans>

執行結果:
在這裏插入圖片描述
方法異常後結果:
在這裏插入圖片描述
可以看出這個與上述的動態代理例子十分相似。

環繞通知:
後置通知實際上就是前面四種通知的總和,可以在環繞通知中設置以上四種通知。看過代碼你會發現環繞通知跟開始的動態代理demo是一個樣的。

環繞通知代碼demo(用@Around註解標註):

@Around(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
		
		//執行前置通知
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println(methodName + "執行前,參數{" + Arrays.toString(args) + "}");
        
        Object result = null;
        
        try {
        	//執行方法
			result = point.proceed();
		} catch (Throwable e) {
			//異常通知
			System.out.println(methodName + "執行拋出異常{" + e + "}");
			throw e;
		}finally {
			//最終通知
			System.out.println("總是你");
		}
        
        //後置返回通知
        System.out.println(methodName + "執行後,結果{" + result + "}");
        return result;
	}

執行結果:
在這裏插入圖片描述
異常後的結果:
在這裏插入圖片描述
可見與上面四種通知配合出來的結果一個樣。

一些細節:

@Pointcut註解的使用:

該註解用於定義切入點表達式,以達到複用。避免像上面那樣,每個橫切關注點都要定義一個切入點表達式。

@Pointcut註解只能作用在方法上,可以定義一個空方法。然後加上註解,填寫好切入點表達式。

然後在本來要寫切入點表達式的橫切關注點上用方法名()代替便可,類似下面的pointcut()

使用:

@Aspect   //此註解把MyLogger類標註爲一個切面
@Component  //切面對象要交給交給spring 容器管理才能實現AOP。
public class MyLogger {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	//前置通知,用@Before註解定義。裏面的value值是切入點表達式,表示該通知要作用到哪些連接點上。
	@Before(value = "pointcut()")
    public void before(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println(methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }

	@AfterReturning(value = "pointcut()",
			returning = "result")
    public void after(JoinPoint point,Object result){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "執行後,結果{" + result + "}");
    }

	@AfterThrowing(value = "pointcut()",
			throwing = "e")
    public void throwing(JoinPoint point,Throwable e){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "執行拋出異常{" + e + "}");
    }

	@After(value = "pointcut()")
    public void always(){
        System.out.println("總是你");
    }
}

結果與上面demo一樣。

JoinPoint 與 ProceedingJoinPoint

該接口封裝這對攔截的方法的信息,可以獲得攔截的方法的參數,方法名等信息。ProceedingJoinPoint是JoinPoint 的子接口,提供更加強大的功能,可以執行目標方法等。

@AfterReturning

該註解有個returning的成員變量,可以獲取目標方法執行後的返回值。

使用方法:先在註解使用時,指定該返回值的名稱,然後在對應的通知方法中定義一個形參名稱與註解中returning 的值一樣的形參。 就會把一個Object類型的返回值注入到該形參中。

這個返回值形參的定義一般定義爲Object,除非你能非常確定目標方法返回值的類型,纔可以定義爲該類型,否則如果類型轉換失敗的話,該通知就會不生效。比如你目標方法返回一個Integer類型返回值,但是通知方法中形參定義的類型是String類型,該通知方法就不會作用在該目標方法上。

@AfterReturning(value = "pointcut()",
			returning = "result")
    public void after(JoinPoint point,Object result){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "執行後,結果{" + result + "}");
    }
AfterThrowing註解

該註解有個成員變量throwing能讓我們捕獲到異常的類型。

使用方法與上面@AfterReturning方法的返回值類似。

throwing 設置的值與形參的名稱要一致。會爲該形參注入一個Throwable 類型的異常。形參也可以定義特定類型的異常。但是如果異常類型轉換失敗的話,該異常處理通知就會對該目標方法失效。不會執行。

@AfterThrowing(value = "pointcut()",
			throwing = "e")
    public void throwing(JoinPoint point,Throwable e){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "執行拋出異常{" + e + "}");
    }

@Order註解

同一個連接點,同一個通知可以有多個橫切關注點同時作用,此時就要指定橫切關注點的優先級。默認哪個切面被spring先加載,裏面的橫切關注點優先級就高,就會先作用於目標方法中。同一切面的橫切關注點,誰先被掃描到,誰的優先級就高。大概就是clazz.getMethods()之類的方法返回的方法數組,誰在前面誰的優先級就高。可能與方法名有關。

可以用@Order註解顯示地指定前面和橫切關注點的優先級。

該註解位於 org.springframework.core.annotation 包中,是spring的註解。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
	//這個定義的是優先級,值越小優先級越高,默認int類型的最大值。
    int value() default 2147483647;
}

demo:定義三個切面:

@Aspect   //此註解把MyLogger類標註爲一個切面
@Component  //切面對象要交給交給spring 容器管理才能實現AOP。
//@Order(value = 2)
public class MyLogger {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger前置通知before1==>" + methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }
	
}


@Aspect   //此註解把MyLogger類標註爲一個切面
@Component  //切面對象要交給交給spring 容器管理才能實現AOP。
//@Order(value = 3)
public class MyLogger1 {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	//@Order(value = 0)
	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger1前置通知before1==>" + methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }
	
}

@Aspect   //此註解把MyLogger類標註爲一個切面
@Component  //切面對象要交給交給spring 容器管理才能實現AOP。
//@Order(value = -5)
public class MyLogger2 {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	//@Order(value = 100)
	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger2前置通知before1==>" + methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }

}


@Aspect   //此註解把MyLogger類標註爲一個切面
@Component  //切面對象要交給交給spring 容器管理才能實現AOP。
public class MyLogger4 {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger4前置通知before1==>" + methodName + "執行前,參數{" + Arrays.toString(args) + "}");
    }
	

}

分析:
第一次不使用Order註解的默認情況:默認順序。
在這裏插入圖片描述

第二次把切面類上的Order的註解的註釋都去掉。然後MyLogger4仍然不用Order註解。

在這裏插入圖片描述
解釋:因爲 MyLogger4沒有用Order註解,所以優先級最低,MyLogger2 Order註解值爲-5 ,MyLogger爲 2,MyLogger1 爲3,所以優先級 2 >MyLogger>1>4。

有挺多種情況的,這裏就不一一貼圖了,就總結一下,有興趣可以自己嘗試:

  1. 定義在切面上的Order註解先比較,再比較定義在方法上的註解。假如 我切面1優先級定義爲0,而裏面的橫切關注點定義爲10000,切面2優先級定義爲2,裏面的橫切關注點定義爲-10000,仍然是切面1的橫切關注點的優先級高。因爲切面1優先級比切面2要高。
  2. 同一切面裏面的橫切關注點比較優先級纔有意義。不同切面的橫切關注點優先級取決於切面。
  3. 相同優先級的切面或者橫切關注點採用默認優先級比較方法。
  4. 除非你用Order註解定義的優先級值爲int的最大值,否則用Order註解定義的切面或者橫切關注點比沒有用Order註解的優先級要高。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章