1. 程式人生 > >Spring 詳解(二)------- AOP關鍵概念以及兩種實現方式

Spring 詳解(二)------- AOP關鍵概念以及兩種實現方式

目錄



1. AOP 關鍵詞


  • target:目標類,需要被代理的類。例如:ArithmeticCalculator
  • Joinpoint(連線點):所謂連線點是指那些可能被攔截到的方法。例如:所有的方法
  • PointCut 切入點:已經被增強的連線點。例如:add()
  • advice:通知/增強,增強程式碼。例如:showRaram、showResult
  • Weaving(織入):是指把增強 advice 應用到目標物件 target 來建立新的代理物件proxy的過程.
  • proxy 代理類:通知+切入點
  • Aspect(切面)::是切入點 pointcut 和通知 advice 的結合

2. AOP 的作用


當我們為系統做引數驗證,登入許可權驗證或者日誌操作等,為了實現程式碼複用,我們可能把日誌處理抽離成一個新的方法。但是這樣我們仍然必須手動插入這些方法,這樣的話模組之間高耦合,不利於後期的維護和功能的擴充套件,有了 AOP 我們可以將功能抽成一個切面,程式碼複用好,低耦合。

3. AOP 的通知型別



Spring 按照通知 Advice 在目標類方法的連線點位置,可以分為5類

  • 前置通知[Before advice]:在連線點前面執行,前置通知不會影響連線點的執行,除非此處丟擲異常。
  • 正常返回通知[After returning advice]:在連線點正常執行完成後執行,如果連線點丟擲異常,則不會執行。
  • 異常返回通知[After throwing advice]:在連線點丟擲異常後執行。
  • 返回通知[After (finally) advice]:在連線點執行完成後執行,不管是正常執行完成,還是丟擲異常,都會執行返回通知中的內容。
  • 環繞通知[Around advice]:環繞通知圍繞在連線點前後,比如一個方法呼叫的前後。這是最強大的通知型別,能在方法呼叫前後自定義一些操作。環繞通知還需要負責決定是繼續處理join point(呼叫ProceedingJoinPoint的proceed方法)還是中斷執行。


    Spring 中使用五種通知
1. 前置通知
    <aop:before method="" pointcut="" pointcut-ref=""/>
        method : 通知,及方法名
        pointcut :切入點表示式,此表示式只能當前通知使用。
        pointcut-ref : 切入點引用,可以與其他通知共享切入點。
    通知方法格式:public void myBefore(JoinPoint joinPoint){
        引數1:org.aspectj.lang.JoinPoint  用於描述連線點(目標方法),獲得目標方法名等

2. 後置通知  目標方法後執行,獲得返回值
    <aop:after-returning method="" pointcut-ref="" returning=""/>
        returning 通知方法第二個引數的名稱
   通知方法格式:public void myAfterReturning(JoinPoint joinPoint,Object result){
        引數1:連線點描述
        引數2:型別Object,引數名 returning="result" 配置的

3. 異常通知  目標方法發生異常後
    <aop:after-throwing method="testException" throwing="e"
    pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/>
        throwing 發生的異常
   通知方法格式:public Object testRound(ProceedingJoinPoint pjp){
        引數1:ProceedingJoinPoint
        返回值為 reslut


4. 基於 xml 的配置方式

xml 配置檔案

<context:component-scan base-package="com.anqi">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--1、 建立目標類 -->
<bean id="arithmeticCalculator" class="com.anqi.testAop.ArithmeticCalculatorImpl"></bean>
<!--2、建立切面類(通知)  -->
<bean id="logAspect" class="com.anqi.testAop.MyLogger"></bean>
<aop:config>
    <aop:aspect ref="logAspect">
        <!-- 切入點表示式 也可以在通知內部分別設定切入點表示式 -->
        <aop:pointcut expression="execution(* com.anqi.testAop.*.*(..))" id="myPointCut"/>
        <!-- 配置前置通知,注意 method 的值要和 對應切面的類方法名稱相同 -->
        <aop:before method="before" pointcut-ref="myPointCut" />
        <aop:after method="after" pointcut-ref="myPointCut" />
        <aop:after-returning method="testAfterReturn" returning="result" pointcut-ref="myPointCut"/>
        <aop:after-throwing method="testException" throwing="e" pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/>
        <!--<aop:around method="testRound"  pointcut-ref="myPointCut"  /> 最強大,但是一般不使用-->
    </aop:aspect>
</aop:config>


目標類

public interface ArithmeticCalculator {
    int add(int i, int j);
    int sub(int i, int j);

    int mul(int i, int j);
    int div(int i, int j);
}

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}



切面類

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import java.util.Arrays;

/**
 * 建立日誌類
 */
public class MyLogger {

    public void before(JoinPoint joinPoint) {
        System.out.println("前置通知 引數為["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");
    }
    public void after(JoinPoint joinPoint) {
        System.out.println("後置通知 "+ joinPoint.getSignature().getName());
    }

    public void testException(JoinPoint joinPoint, Throwable e) {
        System.out.println("丟擲異常: "+ e.getMessage());
    }

    public void testAfterReturn(JoinPoint joinPoint, Object result) {
        System.out.println("返回通知,返回值為 " + result);
    }

    public Object testRound(ProceedingJoinPoint pjp) {
        Object result = null;
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        try {
            //前置通知
            System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));
            //執行目標方法
            result = pjp.proceed();
            //返回通知
            System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);

        }catch(Throwable e) {
            //異常通知
            System.out.println("!!!異常通知 --> The Method"+methodName+" ends with"+ result);
        }
        //後置通知
        System.out.println("!!!後置通知 --> The Method"+methodName+" ends"+ args);
        return result;
    }
}



測試

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
    public static void main(String[] args) {
        ApplicationContext application = new ClassPathXmlApplicationContext("spring-context.xml");
        ArithmeticCalculator a = application.getBean(ArithmeticCalculator.class);
        int result = a.add(1,2);
        System.out.println(result);
        System.out.println(a.div(5,0));
    }
}
/*
    前置通知 引數為[1,2]
    後置通知 add
    返回通知,返回值為 3
    3
    前置通知 引數為[5,0]
    後置通知 div
    丟擲異常: / by zero
*/

5. 基於註解的配置方式

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:context="http://www.springframework.org/schema/context"
       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/context
       http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.anqi">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!-- 使 AspectJ 註解起作用: 自動為匹配的類生成代理物件 -->
    <aop:aspectj-autoproxy/>
</beans>



目標類

public interface ArithmeticCalculator {
    int add(int i, int j);
    int sub(int i, int j);

    int mul(int i, int j);
    int div(int i, int j);
}
import org.springframework.stereotype.Service;

@Service
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}



切面

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;

/**
 * 建立日誌類
 */
@Aspect
@Component
public class MyLogger {

    @Before("execution(* com.anqi.testAop.*.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("前置通知 引數為["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");
    }
    @After("execution(* com.anqi.testAop.*.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("後置通知 "+ joinPoint.getSignature().getName());
    }

    @AfterThrowing(value="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))", throwing = "e")
    public void testException(JoinPoint joinPoint, Throwable e) {
        System.out.println("丟擲異常: "+ e.getMessage());
    }

    @AfterReturning(value="execution(* com.anqi.testAop.*.*(..))", returning = "result")
    public void testAfterReturn(JoinPoint joinPoint, Object result) {
        System.out.println("返回通知,返回值為 " + result);
    }

    @Around("execution(* com.anqi.testAop.*.*(..))")
    public Object testRound(ProceedingJoinPoint pjp) {
        Object result = null;
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        try {
            //前置通知
            System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));
            //執行目標方法
            result = pjp.proceed();
            //返回通知
            System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);

        }catch(Throwable e) {
            //異常通知
            System.out.println("!!!異常通知 --> The Method"+methodName+" ends with"+ result);
        }
        //後置通知
        System.out.println("!!!後置通知 --> The Method"+methodName+" ends"+ args);
        return result;
    }
}



輸出結果與第一種方式一致,這裡就不再贅述了。

6. 切面的優先順序



可以使用@Order來指定切面的優先順序

//引數驗證切面
@Order(1)
@Aspect
@Component
public class ValidateAspect {

@Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
public void validateArgs(JoinPoint join) {
    String methodName = join.getSignature().getName();
    Object[] args = join.getArgs();
    System.out.println("validate"+methodName+Arrays.asList(args));
    }
}

//把這個類宣告為一個切面:需要把該類放入到 IOC 容器中, 再宣告為一個切面
@Order(2)
@Aspect
@Component
public class LoggingAspect2 {

/**
 * 宣告該方法是一個前置通知: 在目標方法開始之前執行
 * @param join
 */
@Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
public void beforeMehod(JoinPoint join) {
    String methodName = join.getSignature().getName();
    List<Object> args = Arrays.asList(join.getArgs());
    System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
    }
}

7. 重用切點表示式

//把這個類宣告為一個切面:需要把該類放入到 IOC 容器中, 再宣告為一個切面
@Order(2)
@Aspect
@Component
public class LoggingAspect {

    /**
     * 定義一個方法, 用於宣告切入點表示式, 一般地, 該方法中再不需要填入其他程式碼
     */
    @Pointcut("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
    public void declareJointPointExpression() {}


    /**
     * 宣告該方法是一個前置通知: 在目標方法開始之前執行
     * @param join
     */
    @Before("declareJointPointExpression()")
    public void beforeMehod(JoinPoint join) {
        String methodName = join.getSignature().getName();
        List<Object> args = Arrays.asList(join.getArgs());
        System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
    }
}


8. 兩種方式的比較(摘自 spring 官方文件)



如果您選擇使用Spring AOP,則可以選擇@AspectJ或XML樣式。需要考慮各種權衡。

XML樣式可能是現有Spring使用者最熟悉的,並且由真正的POJO支援。當使用AOP作為配置企業服務的工具時,XML可能是一個不錯的選擇(一個好的測試是你是否認為切入點表示式是你可能想要獨立改變的配置的一部分)。使用XML樣式,從您的配置可以更清楚地瞭解系統中存在哪些方面。

XML風格有兩個缺點。首先,它沒有完全封裝它在一個地方解決的要求的實現。DRY原則規定,系統中的任何知識都應該有單一,明確,權威的表示。使用XML樣式時,有關如何實現需求的知識將分支到支援bean類的宣告和配置檔案中的XML。使用@AspectJ樣式時,此資訊封裝在單個模組中:方面。其次,XML樣式在它所表達的內容方面比@AspectJ樣式稍微受限:僅支援“單例”方面例項化模型,並且不可能組合在XML中宣告的命名切入點。例如,

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在XML樣式中,您可以宣告前兩個切入點:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺點是您無法 accountPropertyAccess通過組合這些定義來定義切入點。

@AspectJ 樣式支援額外的例項化模型和更豐富的切入點組合。它具有將方面保持為模組化單元的優點。它還具有以下優點:Spring AOP 和 AspectJ 都可以理解(並因此消耗)@AspectJ 方面。因此,如果您以後決定需要 AspectJ 的功能來實現其他要求,則可以輕鬆遷移到基於 AspectJ 的方法。總而言之,只要您的方面不僅僅是簡單的企業服務配置,Spring 團隊更喜歡 @AspectJ 風格。