1. 程式人生 > >Spring Aop的應用

Spring Aop的應用

開發十年,就只剩下這套架構體系了! >>>   

AOP的基本概念

  • 連線點( Jointpoint) : 表示需要在程式中插入橫切關注點的擴充套件點,連線點可能是類初始化、方法執行、 方法呼叫、欄位呼叫或處理異常等等, Spring 只支援方法執行連線點, 在 AOP 中表示為“在哪裡幹” ;

  • 切入點( Pointcut) : 選擇一組相關連線點的模式, 即可以認為連線點的集合,Spring 支援 perl5 正則表示式和 AspectJ 切入點模式, Spring 預設使用 AspectJ 語法, 在 AOP 中表示為“在哪裡乾的集合” ;

  • 通知( Advice) : 在連線點上執行的行為, 通知提供了在 AOP 中需要在切入點所選擇的連線點處進行擴充套件現有行為的手段; 包括前置通知( before advice)、後置通知(after advice)、環繞通知( around advice), 在 Spring 中通過代理模式實現AOP,並通過攔截器模式以環繞連線點的攔截器鏈織入通知; 在 AOP 中表示為“幹什麼”;

  • 方面/切面( Aspect): 橫切關注點的模組化,可以認為是通知、引入和切入點的組合; 在 Spring 中可以使用 Schema 和@AspectJ 方式進行組織實現; 在 AOP 中表示為“在哪乾和幹什麼集合”

  • 引入( inter-type declaration) : 也稱為內部型別宣告, 為已有的類新增額外新的欄位或方法, Spring 允許引入新的介面(必須對應一個實現)到所有被代理物件(目標物件) , 在 AOP 中表示為“幹什麼(引入什麼) ” ;

  • 目標物件( Target Object) : 需要被織入橫切關注點的物件,即該物件是切入點選擇的物件,需要被通知的物件,從而也可稱為“被通知物件”;由於 Spring AOP通過代理模式實現,從而這個物件永遠是被代理物件, 在 AOP 中表示為“對誰幹” ;

  • AOP 代理( AOP Proxy) : AOP 框架使用代理模式建立的物件,從而實現在連線點處插入通知(即應用切面) ,就是通過代理來對目標物件應用切面。在 Spring中, AOP 代理可以用 JDK 動態代理或 CGLIB 代理實現,而通過攔截器模型應用切面。

  • 織入( Weaving) : 織入是一個過程,是將切面應用到目標物件從而創建出 AOP代理物件的過程, 織入可以在編譯期、類裝載期、執行期進行。

Spring有哪些通知型別呢?

  • 前置通知( Before Advice) :在切入點選擇的連線點處的方法之前執行的通知,該通知不影響正常程式執行流程(除非該通知丟擲異常,該異常將中斷當前方法鏈的執行而返回)。

  • 後置通知( After Advice) : 在切入點選擇的連線點處的方法之後執行的通知,包括如下型別的後置通知:

    1. 後置返回通知( After returning Advice) :在切入點選擇的連線點處的方法正常執行完畢時執行的通知, 必須是連線點處的方法沒丟擲任何異常正常返回時才呼叫後置通知。

    2. 後置異常通知( After throwing Advice) : 在切入點選擇的連線點處的方法丟擲異常返回時執行的通知, 必須是連線點處的方法丟擲任何異常返回時才呼叫異常通知。

    3. 後置最終通知( After finally Advice) : 在切入點選擇的連線點處的方法返回時執行的通知,不管拋沒丟擲異常都執行,類似於 Java 中的 finally 塊。

  • 環繞通知( Around Advices): 環繞著在切入點選擇的連線點處的方法所執行的通知,環繞通知可以在方法呼叫之前和之後自定義任何行為,並且可以決定是否執行連線點處的方法、替換返回值、丟擲異常等等。

在 AOP 中,通過切入點選擇目標物件的連線點,然後在目標物件的相應連線點處織入通知,而切入點和通知就是切面(橫切關注點),而在目標物件連線點處應用切面的實現方式是通過 AOP 代理物件。

基於註解的AOP程式設計

基於註解的程式設計,需要依賴AspectJ框架(java中最流行的aop框架)。 第一步:匯入AspectJ的jar包,該框架只有Spring 2.0以上才支援。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.7.3</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.7.3</version>
    <scope>runtime</scope>
</dependency>

第三步:切面類,該類有什麼特點?首先它必須是IOC的bean,還要宣告它是AspectJ切面,最後還可以定義切面的優先順序Order(非必填)

通知有五種註解 @Before :前置通知的註解,在目標方法執行前呼叫

@After:後置通知的註解, 在目標方法執行後呼叫,即使程式丟擲異常都會呼叫

@AfterReturning:返回通知的註解, 在目標方法成功執行後呼叫,如果程式出錯則不會呼叫

@AfterThrowing:異常通知的註解, 在目標方法出現指定異常時呼叫

@Around:環繞通知的註解,很強大(相當於前四個通知的組合),但用的不多。

環繞通知

import java.util.Arrays;
import java.util.List;

/**
 * @program: ssm
 * @description: 環繞通知
 * @author: lee
 * @create: 2019-03-14
 **/
@Order(2)
@Aspect
@Component
public class AroundAspect {

    /**
     * 環繞通知,很強大,但用的不多。 用環繞通知測試Order的優先順序看的不明顯(這裡是筆者的失誤)
     * 環繞通知需要用ProceedingJoinPoint 型別的引數
     */
    @Around("execution(* com.*.aspect.*.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
        Object result = null;
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        try {
            System.out.println("@Around 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
            result = joinPoint.proceed();
            System.out.println("@Around 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("@Around 異常通知 : 方法名 【 " + methodName + " 】and  exception is " + e);
        }
        System.out.println("@Around 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);

        return result;
    }
}    

攔截物件

package com.plantform.aspect;

import org.springframework.stereotype.Component;

/**
 * @program: ssm
 * @description: 切面類
 * @author: lee
 * @create: 2019-03-14
 **/
public class AspectMethod {

    public int add(int a, int b) {
        System.out.println("add 方法執行了 ----> " + (a + b));
        return (a + b);
    }
    public int division(int a, int b) {
        System.out.println("division 方法執行了 ----> " + (a / b));
        return (a / b);
    }

}    

攔截通知

package com.plantform.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * @program: ssm
 * @Order(n) : 切面的優先順序,n越小,級別越高
 * @Aspect:宣告該類是一個切面
 * @Component:切面必須是 IOC 中的 bean
 * @author: lee
 * @create: 2019-03-14
 **/
@Order(1)
@Aspect
@Component
public class LogAspect {


    /**
     * 前置通知的註解,在目標方法執行前呼叫
     * execution最基礎的表示式語法。
     * 注意點:
     * 1. 方法裡面不能有行參,及add(int a, int b) 這是會報錯的。
     * 2. int(方法的返回值),add(方法名) 可以用 * 抽象化。甚至可以將類名抽象,指定該包下的類。
     * 3. (int, int) 可以用(..)代替,表示匹配任意數量的引數
     * 4. 被通知的物件(Target),建議加上包的路徑
     */
    @Before(value = "execution(* com.*.aspect.*.*(..))")
    public void beforeAdvice(JoinPoint joinPoint) {
        /**
         * 連線點 joinPoint:add方法就是連線點
         * getName獲取的是方法名,是英文的,可以通過國際化轉換對應的中文比較好。
         */
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("@Before 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
    }

    /**
     * 後置通知的註解, 在目標方法執行後呼叫,即使是程式出錯都會呼叫
     * 這裡將 方法的返回值 和 CalculatorImp類下所有的方法,以及方法的形參 都抽象了
     */
    @After(value = "execution(* com.*.aspect.*.*(..))")
    public void afterAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("@After 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);
    }

    /**
     * 重用切入點定義:宣告切入點表示式。該方法裡面不建議新增其他程式碼
     */
    @Pointcut("execution(* com.*.aspect.*.*(..))")
    public void declareExecutionExpression(){}

    /**
     * 返回通知的註解, 在目標方法成功執行後呼叫,如果程式出錯則不會呼叫
     * returning="result" 和 形參 result 保持一致
     */
    @AfterReturning(value="declareExecutionExpression()", returning="result")
    public void afterRunningAdvice(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("@AfterReturning 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
    }

    /**
     * 異常通知的註解, 在目標方法出現指定異常時呼叫
     * throwing="exception" 和 形參 exception 保持一致 , 且目標方法出了Exception(可以是其他異常)異常才會呼叫。
     */
    @AfterThrowing(value="declareExecutionExpression()", throwing="exception")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("@AfterThrowing 異常通知 : 方法名 【 " + methodName + " 】and  exception is " + exception);
    }
}    

執行方法

/**
 * @program: ssm
 * @description:
 * @author: lee
 * @create: 2019-03-14
 **/
@Controller
public class MainAspect {

    @RequestMapping("/static/index")
    @ResponseBody
    public String main(String[] args) {
        AspectMethod aspectMethod=new AspectMethod();
        aspectMethod.add(11, 12);
        aspectMethod.division(21, 3);
        //aspectMethod.division(21, 0);
        return "執行完了";
    }
}    

輸出結果

@Before 前置通知 : 方法名 【 main 】and args are [null]
@Around 前置通知 : 方法名 【 main 】and args are [null]
add 方法執行了 ----> 23
division 方法執行了 ----> 7
@Around 返回通知 : 方法名 【 main 】and args are [null] , result is 執行成功
@Around 後置通知 : 方法名 【 main 】and args are [null]
@After 後置通知 : 方法名 【 main 】and args are [null]
@AfterReturning 返回通知 : 方法名 【 main 】and args are [null] , result is 執行成功

基於xml的AOP程式設計

第一步:核心檔案applicationContext.xml, 首先是配置三個bean,方便是兩個切面類,和一個方法類。 然後配置AOP, aop:config:註明開始配置AOP

aop:pointcut:配置切點重用表示式,expression的值是具體的表示式,id 該aop:pointcut的唯一標識,

aop:aspect:配置切面,ref的值引用相關切面類的bean,order設定優先順序(也可以不設定)。

五種通知的配置:aop:before,aop:after,aop:after-returning,aop:after-throwing,aop:around。method的值就是對應的方法,poincut-ref的值要引用 aop:pointcut 的id。其中有兩個比較特殊:aop:after-returning 要多配置一個returning,其中returning的值要和對應方法的形參保持一致。同理aop:after-throwing 也要多配置一個throwing,其中throwing的值也要和對應方法的形參保持一致。不然執行程式會報錯。

<?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-4.0.xsd  
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">  
  
    <bean id="calculator" class="com.atguigu.spring.my.xml.CalculatorImp"></bean>  
    <bean id="loggerAspect" class="com.atguigu.spring.my.xml.LoggerAspect"></bean>  
    <bean id="aroundAspect" class="com.atguigu.spring.my.xml.AroundAspect"></bean>  
      
    <!-- AOP配置 -->  
    <aop:config>  
        <!-- 配置切點表示式 類似註解的重用表示式-->  
        <aop:pointcut expression="execution(* com.atguigu.spring.my.xml.CalculatorImp.*(..))"   
            id="pointcut"/>  
        <!-- 配置切面及通知  method的值就是 loggerAspect類中的值-->  
        <aop:aspect ref="loggerAspect" order="2">  
            <aop:before method="beforeAdvice" pointcut-ref="pointcut"/>  
            <aop:after method="afterAdvice" pointcut-ref="pointcut"/>  
            <aop:after-returning method="afterRunningAdvice" pointcut-ref="pointcut" returning="result"/>  
            <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="pointcut" throwing="exception"/>  
        </aop:aspect>  
        <aop:aspect ref="aroundAspect" order="1">  
            <!-- <aop:around method="aroundAdvice" pointcut-ref="pointcut"/>  -->  
        </aop:aspect>  
    </aop:config>  
  
</beans>  

下面幾個類,就是脫去了所有註解的外衣,採用通過配置的xml,實現AOP程式設計。

public interface Calculator {  
      
    public int add(int a, int b);  
    public int division(int a, int b);  
  
}  


public class CalculatorImp implements Calculator {  
  
    @Override  
    public int add(int a, int b) {  
        System.out.println("add 方法執行了 ----> " + (a + b));  
        return (a + b);  
    }  
  
    @Override  
    public int division(int a, int b) {  
        System.out.println("division 方法執行了 ----> " + (a / b));  
        return (a / b);  
    }  
  
}  


import java.util.Arrays;  
import java.util.List;  
import org.aspectj.lang.JoinPoint;  
  
public class LoggerAspect {  
      
    public void beforeAdvice(JoinPoint joinPoint) {  
        String methodName = joinPoint.getSignature().getName();   
        List<Object> args = Arrays.asList(joinPoint.getArgs());  
        System.out.println("Before 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);  
    }  
      
    public void afterAdvice(JoinPoint joinPoint) {  
        String methodName = joinPoint.getSignature().getName();   
        List<Object> args = Arrays.asList(joinPoint.getArgs());  
        System.out.println("After 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);  
    }  
      
    public void afterRunningAdvice(JoinPoint joinPoint, Object result) {  
        String methodName = joinPoint.getSignature().getName();   
        List<Object> args = Arrays.asList(joinPoint.getArgs());  
        System.out.println("AfterReturning 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);  
    }  
      
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {  
        String methodName = joinPoint.getSignature().getName();   
        System.out.println("AfterThrowing 異常通知 : 方法名 【 " + methodName + " 】and  exception is " + exception);  
    }  
  
}  



import java.util.Arrays;  
import java.util.List;  
import org.aspectj.lang.ProceedingJoinPoint;  
  
public class AroundAspect {  
  
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {  
        Object result = null;  
        String methodName = joinPoint.getSignature().getName();   
        List<Object> args = Arrays.asList(joinPoint.getArgs());  
        try {  
            System.out.println("@Around 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);  
            result = joinPoint.proceed();  
            System.out.println("@Around 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);  
        } catch (Throwable e) {  
            e.printStackTrace();  
            System.out.println("@Around 異常通知 : 方法名 【 " + methodName + " 】and  exception is " + e);  
        }  
        System.out.println("@Around 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);  
          
        return result;  
    }  
      
}  


import org.springframework.context.support.ClassPathXmlApplicationContext;  
  
public class Main {  
      
    public static void main(String[] args) {  
          
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");  
        Calculator calculator = (Calculator) ctx.getBean("calculator");  
          
        calculator.add(11, 12);  
        calculator.division(21, 3); // 測試時,將被除數換成0,可以測試AfterReturning ,After 和 AfterThrowing  
          
        ctx.close();  
    }  
  
}  

輸出結果

Before 前置通知 : 方法名 【 add 】and args are [11, 12]  
add 方法執行了 ----> 23  
After 後置通知 : 方法名 【 add 】and args are [11, 12]  
AfterReturning 返回通知 : 方法名 【 add 】and args are [11, 12] , result is 23  
Before 前置通知 : 方法名 【 division 】and args are [21, 3]  
division 方法執行了 ----> 7  
After 後置通知 : 方法名 【 division 】and args are [21, 3]  
AfterReturning 返回通知 : 方法名 【 division 】and args are [21, 3] , re