1. 程式人生 > 實用技巧 >Spring AOP的介紹與實現

Spring AOP的介紹與實現

1. AOP的介紹

AOP(Aspect OrientedProgramming)稱為面向切面程式設計,在程式開發中主要用來解決一些系統層面上的問題,比如日誌,事務,許可權等待,Struts2的攔截器設計就是基於AOP的思想,是個比較經典的例子。

在不改變原有的邏輯的基礎上,增加一些額外的功能。代理也是這個功能,讀寫分離也能用aop來做。 AOP可以說是OOP(Object Oriented Programming,面向物件程式設計)的補充和完善。OOP引入封裝、繼承、多型等概念來建立一種物件層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌程式碼往往橫向地散佈在所有物件層次中,而與它對應的物件的核心功能毫無關係對於其他型別的程式碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的程式碼被稱為橫切(crosscutting),在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。

AOP技術恰恰相反,它利用一種稱為"橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。

使用"橫切"技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。

2. AOP的相關概念

  • 橫切關注點:對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之為橫切關注點

  • Aspect(切面):通常是一個類,裡面可以定義切入點和通知

  • JointPoint(連線點):程式執行過程中明確的點,一般是方法的呼叫。被攔截到的點,因為Spring只支援方法型別的連線點,所以在Spring中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器

  • Advice(通知):AOP在特定的切入點上執行的增強處理,有before(前置),after(後置),afterReturning(最終),afterThrowing(異常),around(環繞)

  • Pointcut(切入點):就是帶有通知的連線點,在程式中主要體現為書寫切入點表示式

  • weave(織入):將切面應用到目標物件並導致代理物件建立的過程

  • introduction(引入):在不修改程式碼的前提下,引入可以在執行期為類動態地新增一些方法或欄位

  • AOP代理(AOPProxy):AOP框架建立的物件,代理就是目標物件的加強。Spring中的AOP代理可以使JDK動態代理,也可以是CGLIB代理,前者基於介面,後者基於子類

  • 目標物件(Target Object): 包含連線點的物件。也被稱作被通知或被代理物件。POJO

3. Advice通知型別:

  • (1)Before:
    在目標方法被呼叫之前做增強處理,
    @Before只需要指定切入點表示式即可

  • (2)AfterReturning:
    在目標方法正常完成後做增強,
    @AfterReturning除了指定切入點表示式後,還可以指定一個返回值形參名returning,代表目標方法的返回值

  • (3)AfterThrowing:
    主要用來處理程式中未處理的異常,
    @AfterThrowing除了指定切入點表示式後,還可以指定一個throwing的返回值形參名,可以通過該形參名來訪問目標方法中所丟擲的異常物件

  • (4)After:
    在目標方法完成之後做增強,無論目標方法時候成功完成。
    @After可以指定一個切入點表示式

  • (5)Around:
    環繞通知,在目標方法完成前後做增強處理,環繞通知是最重要的通知型別,像事務,日誌等都是環繞通知,注意程式設計中核心是ProceedingJoinPoint
    @Around可以指定一個切入點表示式

4. AOP的實現方式

通過介面來實現

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterAdvice;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;

import java.lang.reflect.Method;

/**
 * 前置通知:執行目標方法前時,執行的通知;
 * 後置通知:執行目標方法後時,執行的通知;
 * 異常通知:執行目標方法發生異常時,執行的通知;
 * 環繞通知:在環繞通知中可以定義,前置通知、後置通知、異常通知和最終通知,比較全面;
 * 最終通知:執行方法後,都會執行的通知;
 * <p>
 * 通過介面實現通知
 * 前置通知:繼承MethodBeforeAdvice介面,並重寫before()方法;
 * 後置通知:繼承AfterReturningAdvice介面,並重寫afterReturning()方法;
 * 異常通知:繼承ThrowsAdvice介面,無重寫方法;
 * 環繞通知:繼承MethodInterceptor介面,並重寫invoke()方法;
 * 最終通知;
 */
public class AspectImpl implements MethodBeforeAdvice, AfterReturningAdvice, AfterAdvice, MethodInterceptor, ThrowsAdvice {

    /**
     * 前置通知
     * @param method 目標方法物件
     * @param args   目標方法引數
     * @param target 目標方法的類物件
     * @throws Throwable
     */
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.printf("前置通知:%s%n引數:%s%n", method.getName(), (Integer) args[0]);
    }

    /**
     * 後置通知
     * @param returnValue 目標方法的返回值
     * @param method      目標方法物件
     * @param args        目標方法引數
     * @param target      目標方法的類物件
     * @throws Throwable
     */
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.printf("後置通知:%s%n返回值:%s%n%n", method.getName(), returnValue);
    }


    /**
     * 異常通知
     * @param method 目標方法物件
     * @param args   目標方法引數
     * @param target 目標方法的類物件
     * @param ex     異常型別
     */
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
        System.out.printf("異常通知:%s%n%n", ex.toString());
    }

    /**
     * 環繞通知 攔截器
     * @param invocation 連線點資訊
     * @return 目標方法執行後的結果
     * @throws Throwable
     */
    public Object invoke(MethodInvocation invocation) throws Throwable {

        Object result = null;

        try {

            System.out.printf("環繞通知-前置通知:%s%n", invocation.getMethod().getName());
            result = invocation.proceed();
            System.out.printf("環繞通知-後置通知:%s%n", invocation.getMethod().getName());

        } catch (Exception ex) {
            System.out.printf("環繞通知-異常通知:%s%n", invocation.getMethod().getName());
        } finally {
            System.out.printf("環繞通知-最終通知:%s%n", invocation.getMethod().getName());
        }

        return result;
    }
}

配置檔案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
        https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userService" class="cn.sivan.aop.impl.UserServiceImpl"/>
    <bean id="aspectImpl" class="cn.sivan.aop.aspect.AspectImpl"/>

    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* cn.sivan.aop.impl.*.*(..))"/>
        <aop:advisor advice-ref="aspectImpl" pointcut-ref="pointcut"/>
    </aop:config>

</beans>

測試

public class UserServiceTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
        UserService userService = context.getBean("userService", UserService.class);
        userService.updateUser(1);
    }
}
環繞通知-前置通知:updateUser
前置通知:updateUser
引數:1
updateUser:更新-使用者
後置通知:updateUser
返回值:6059

環繞通知-後置通知:updateUser
環繞通知-最終通知:updateUser

通過配置實現通知

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

/**
 * 通過配置實現通知
 * before 前置通知
 * afterReturning 後置通知
 * after 最終通知
 * afterThrowing 異常通知
 * invoke 環繞通知
 * <p>
 * JoinPoint物件封裝了SpringAop中切面方法的資訊,在切面方法中新增JoinPoint引數,就可以獲取到封裝了該方法資訊的JoinPoint物件
 * Signature getSignature();	獲取封裝了署名資訊的物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊
 * Object[] getArgs();	獲取傳入目標方法的引數物件
 * Object getTarget();	獲取被代理的物件
 * Object getThis();	獲取代理物件
 */
public class AspectConfig {

    public void before(JoinPoint jp) {
        System.out.println("目標方法名為:" + jp.getSignature().getName());
        System.out.println("目標方法所屬類的簡單類名:" + jp.getSignature().getDeclaringType().getSimpleName());
        System.out.println("目標方法所屬類的類名:" + jp.getSignature().getDeclaringTypeName());

        //獲取傳入目標方法的引數
        Object[] args = jp.getArgs();
        for (int i = 0; i < args.length; i++) {
            System.out.println("第" + (i + 1) + "個引數為:" + args[i]);
        }

        System.out.println("被代理的物件:" + jp.getTarget());
        System.out.println("代理物件自己:" + jp.getThis());
    }

    public void after(JoinPoint jp) {
        System.out.println("最終通知");
    }

    public void afterReturning(JoinPoint jp, Object returnValue) {
        System.out.println("後置通知:" + returnValue);
    }

    public void afterThrowing(JoinPoint jp, NullPointerException ex) {
        System.out.println("異常通知:" + ex.toString());
    }

    public Object invoke(ProceedingJoinPoint pjp) {
        System.out.println("環繞通知");
        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return result;
    }

}

配置檔案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
        https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userService" class="cn.sivan.aop.impl.UserServiceImpl"/>
    <bean id="aspectConfig" class="cn.sivan.aop.aspect.AspectConfig"/>

    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* cn.sivan.aop.impl.UserServiceImpl.*(..))"/>
        <aop:aspect ref="aspectConfig">
            <aop:before method="before" pointcut-ref="pointcut"/>
            <aop:after method="after" pointcut-ref="pointcut"/>
            <aop:after-returning method="afterReturning" returning="returnValue" pointcut-ref="pointcut"/>
            <aop:after-throwing method="afterThrowing" throwing="ex" pointcut-ref="pointcut"/>
            <aop:around method="invoke" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

通過註解實現通知

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

/**
 * 通過註解實現通知
 * 前置通知:  註解為   @Before
 * 後置通知:  註解為   @AfterReturning
 * 異常通知:  註解為   @AfterThrowing
 * 環繞通知:  註解為   @Around
 * 最終通知:  註解為   @After
 * 在applicationContext.xml中開啟註解對AOP的支援
 * <p>
 * 兩個注意點:
 * @Aspect 宣告該類是一個通知;
 * @Component("annotationAdvice") 將AnnotationAdvice納入到SpringIoc容器中。
 */
@Aspect
@Component("aspectAnnotation")
public class AspectAnnotation {

    @Before("execution(* cn.sivan.aop.impl.*.*(..))")
    public void myBefore(JoinPoint jp) {
        System.out.println("前置通知:" + jp.getSignature().getName());
    }

    @After("execution(* cn.sivan.aop.impl.*.*(..))")
    public void myAfter(JoinPoint jp) {
        System.out.println("最終通知");
    }

    @AfterReturning(pointcut = "execution(* cn.sivan.aop.impl.*.*(..))", returning = "returnValue")
    public void myAfterReturning(JoinPoint jp, Object returnValue) {
        System.out.println("後置通知:" + returnValue);
    }

    @AfterThrowing(pointcut = "execution(* cn.sivan.aop.impl.*.*(..))", throwing = "ex")
    public void myThrows(JoinPoint jp, NullPointerException ex) {
        System.out.println("異常通知:" + ex.toString());
    }

    @Around("execution(* cn.sivan.aop.impl.*.*(..))")
    public Object myAround(ProceedingJoinPoint pjp) {
        System.out.println("環繞通知");
        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        return result;
    }
}

配置檔案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
        https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--開啟註解對aop的支援-->
    <aop:aspectj-autoproxy/>

    <!-- 裡面放包的名字,可以放多個包。放在裡面之後執行會在相關包中找相關的註解,找到了就將他們納入到SpringIoc容器中 -->
    <context:component-scan base-package="cn.sivan.aop"/>
</beans>

常見使用場景

Advice
環繞通知 控制事務 許可權控制
後置通知 記錄日誌(方法已經成功呼叫)
異常通知 異常處理 控制事務
最終通知 記錄日誌(方法已經呼叫,但不一定成功)

指定每個 aspect 的執行順序

  • 實現org.springframework.core.Ordered介面,實現它的getOrder()方法
  • 給aspect新增@Order註解,該註解全稱為:org.springframework.core.annotation.Order

不管採用上面的哪種方法,都是值越小的 aspect 越先執行。
比如,我們為 apsect1 和 aspect2 分別新增 @Order 註解,如下:

@Order(5)
@Component
@Aspect
public class Aspect1 {
    // ...
}

@Order(6)
@Component
@Aspect
public class Aspect2 {
    // ...
}

這樣修改之後,可保證不管在任何情況下, aspect1 中的 advice 總是比 aspect2 中的 advice 先執行.