Spring AOP全面詳解(超級詳細)

語言: CN / TW / HK

如果說IOC 是 Spring 的核心,那麼面向切面程式設計AOP就是 Spring 另外一個最為重要的核心@mikechen

AOP的定義

AOP (Aspect Orient Programming),直譯過來就是 面向切面程式設計,AOP 是一種程式設計思想,是面向物件程式設計(OOP)的一種補充。

面向切面程式設計,實現在不修改原始碼的情況下給程式動態統一新增額外功能的一種技術,如下圖所示:\

\

AOP可以攔截指定的方法並且對方法增強,而且無需侵入到業務程式碼中,使業務與非業務處理邏輯分離,比如Spring的事務,通過事務的註解配置,Spring會自動在業務方法中開啟、提交業務,並且在業務處理失敗時,執行相應的回滾策略。

AOP的作用

AOP 採取橫向抽取機制(動態代理),取代了傳統縱向繼承機制的重複性程式碼,其應用主要體現在事務處理、日誌管理、許可權控制、異常處理等方面。

主要作用是分離功能性需求和非功能性需求,使開發人員可以集中處理某一個關注點或者橫切邏輯,減少對業務程式碼的侵入,增強程式碼的可讀性和可維護性。

簡單的說,AOP 的作用就是保證開發者在不修改原始碼的前提下,為系統中的業務元件新增某種通用功能。

AOP的應用場景

比如典型的AOP的應用場景:\

  • 日誌記錄
  • 事務管理
  • 許可權驗證
  • 效能監測

AOP可以攔截指定的方法,並且對方法增強,比如:事務、日誌、許可權、效能監測等增強,而且無需侵入到業務程式碼中,使業務與非業務處理邏輯分離。

Spring AOP的術語

在深入學習SpringAOP 之前,讓我們先對AOP的幾個基本術語有個大致的概念。

AOP核心概念

Spring AOP 通知分類

Spring AOP 織入時期

Spring AOP三種使用方式

AOP程式設計其實是很簡單的事情,縱觀AOP程式設計,程式設計師只需要參與三個部分:

1、定義普通業務元件

2、定義切入點,一個切入點可能橫切多個業務元件

3、定義增強處理,增強處理就是在AOP框架為普通業務元件織入的處理動作

所以進行AOP程式設計的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理物件的方法=增強處理+被代理物件的方法。

方式1:使用Spring自帶的AOP

public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice,MethodInterceptor {
    @Override
    public void before(Method method, Object[] objects, Object target) throws Throwable {
        //前置通知
    }

    @Override
    public void afterReturning(Object result, Method method, Object[] objects, Object target) throws Throwable {
        //後置通知
    }
     @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        //環繞通知
        //目標方法之前執行
        methodInvocation.proceed();   //目標方法

        //目標方法之後執行
        return resultVal;
    }
}

配置通知時需實現org.springframework.aop包下的一些介面

  • 前置通知:MethodBeforeAdvice
  • 後置通知:AfterReturningAdvice
  • 環繞通知:MethodInterceptor
  • 異常通知:ThrowsAdvice

建立被代理物件

<bean id="orderServiceBean" class="com.apesource.service.impl.OrderServiceImpl"/>
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>

通知(Advice)

<bean id="logAdviceBean" class="com.apesource.log.LogAdvice"/>
<bean id="performanceAdviceBean" class="com.apesource.log.PerformanceAdvice"/>

切入點(Pointcut):通過正則表示式描述指定切入點(某些 指定方法)

<bean id="createMethodPointcutBean"         class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <!--注入正則表示式:描述那些方法為切入點-->
    <property name="pattern" value=".*creat.*"/>
</bean>

Advisor(高階通知) = Advice(通知) + Pointcut(切入點)

<bean id="performanceAdvisorBean" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <!--注入切入點-->
        <property name="pointcut" ref="createMethodPointcutBean"/>

        <!--注入通知-->
        <property name="advice" ref="performanceAdviceBean"/>
    </bean>

建立自動代理

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!--Bean名稱規則(陣列):指定那些bean建立自動代理-->
        <property name="beanNames">
            <list>
                <value>*ServiceBean</value>
                <value>*TaskBean</value>
            </list>
        </property>

        <!--通知列表:需要執行那些通知-->
        <property name="interceptorNames">
            <list>
                <value>logAdviceBean</value>
                <value>performanceAdvisorBean</value>
            </list>
        </property>
</bean>

方式2:使用Aspectj實現切面(普通POJO的實現方式)

匯入Aspectj相關依賴

<!--aop依賴1:aspectjrt -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>

<!--aop依賴2: aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

通知方法名隨便起,沒有限制

public class LogAspectj {
    //前置通知
    public void beforeAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj前置通知】 ==========");
    }

    //後置通知:方法正常執行後,有返回值,執行該後置通知:如果該方法執行出現異常,則不執行該後置通知 
    public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){
        System.out.println("========== 【Aspectj後置通知】 ==========");
    }
    public void afterAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj後置通知】 ==========");
    }

    //環繞通知
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("##########【環繞通知中的前置通知】##########");
        Object returnVale = joinPoint.proceed();
        System.out.println("##########【環繞通知中的後置通知】##########");
        return returnVale;
    }

    /**
     * 異常通知:方法出現異常時,執行該通知
     */
    public void throwAdvice(JoinPoint joinPoint, Exception ex){
        System.out.println("出現異常:" + ex.getMessage());
    }

}

使用Aspectj實現切面,使用Spring AOP進行配置

<!--業務元件bean-->
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>

<!--日誌Aspect切面-->
<bean id="logAspectjBean" class="com.apesource.log.LogAspectj"/>

<!--使用Aspectj實現切面,使用Spring AOP進行配置-->
<aop:config>
    <!--配置切面-->
    <!--注入切面bean-->
    <aop:aspect ref="logAspectjBean">
        <!--定義Pointcut:通過expression表示式,來查詢 特定的方法(pointcut)-->
        <aop:pointcut id="pointcut"
                     expression="execution(* com.apesource.service.impl.*.create*(..))"/>

        <!--配置"前置通知"-->
        <!--在pointcut切入點(serviceMethodPointcut)查詢到 的方法執行"前",
            來執行當前logAspectBean的doBefore-->
        <aop:before method="beforeAdvice" pointcut-ref="pointcut"/>

        <!--配置“後置通知”-->
        <!--returning屬性:配置當前方法中用來接收返回值的引數名-->
        <aop:after-returning returning="returnVal" 
                             method="afterReturningAdvice" pointcut-ref="pointcut"/> 
        <aop:after method="afterAdvice" pointcut-ref="pointcut"/>
        
        <!--配置"環繞通知"-->
        <aop:around method="aroundAdvice" pointcut-ref="pointcut"/>
        
        <!--配置“異常通知”-->
        <!--throwing屬性:配置當前方法中用來接收當前異常的引數名-->
        <aop:after-throwing throwing="ex" method="throwAdvice" pointcut-ref="pointcut"/>
    </aop:aspect>

</aop:config>

方式3:使用Aspectj實現切面(基於註解的實現方式)

//聲明當前類為Aspect切面,並交給Spring容器管理
@Component
@Aspect
public class LogAnnotationAspectj {
    private final static String EXPRESSION = 
                            "execution(* com.apesource.service.impl.*.create*(..))";

    //前置通知   
    @Before(EXPRESSION)
    public void beforeAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj前置通知】 ==========");
    }


    //後置通知:方法正常執行後,有返回值,執行該後置通知:如果該方法執行出現異常,則不執行該後置通知
    @AfterReturning(value = EXPRESSION,returning = "returnVal")
    public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){
        System.out.println("========== 【Aspectj後置通知】 ==========");
    }

    //後置通知
    @After(EXPRESSION)
    public void afterAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj後置通知】 ==========");
    }

    //環繞通知
    @Around(EXPRESSION)
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("##########【環繞通知中的前置通知】##########");
        Object returnVale = joinPoint.proceed();
        System.out.println("##########【環繞通知中的後置通知】##########");
        return returnVale;
    }

    // 異常通知:方法出現異常時,執行該通知
    @AfterThrowing(value = EXPRESSION,throwing = "ex")
    public void throwAdvice(JoinPoint joinPoint, Exception ex){
        System.out.println("********** 【Aspectj異常通知】執行開始 **********");
        System.out.println("出現異常:" + ex.getMessage());
        System.out.println("********** 【Aspectj異常通知】執行結束 **********");
    }

}
<!-- 自動掃描器 -->
<context:component-scan base-package="com.apesource"/>

<!--配置Aspectj的自動代理-->
<aop:aspectj-autoproxy/>

Spring AOP的實現原理

Spring的AOP實現原理其實很簡單,就是通過動態代理實現的。

Spring AOP 採用了兩種混合的實現方式:JDK 動態代理和 CGLib 動態代理。

  • JDK動態代理:Spring AOP的首選方法。 每當目標物件實現一個介面時,就會使用JDK動態代理。目標物件必須實現介面
  • CGLIB代理:如果目標物件沒有實現介面,則可以使用CGLIB代理。

JDK動態代理

Spring預設使用JDK的動態代理實現AOP,類如果實現了介面,Spring就會使用這種方式實現動態代理。

JDK實現動態代理需要兩個元件,首先第一個就是InvocationHandler介面。

我們在使用JDK的動態代理時,需要編寫一個類,去實現這個介面,然後重寫invoke方法,這個方法其實就是我們提供的代理方法。

如下原始碼所示:

/**
 * 動態代理
 *
 * @author mikechen
 */
public class JdkProxySubject implements InvocationHandler {

    private Subject subject;

    public JdkProxySubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("before 前置通知");
        Object result = null;

        try {
            result = method.invoke(subject, args);
        }catch (Exception ex) {
            System.out.println("ex: " + ex.getMessage());
            throw ex;
        }finally {
            System.out.println("after 後置通知");
        }
        return result;
    }
}

然後JDK動態代理需要使用的第二個元件就是Proxy這個類,我們可以通過這個類的newProxyInstance方法,返回一個代理物件。

生成的代理類實現了原來那個類的所有介面,並對介面的方法進行了代理,我們通過代理物件呼叫這些方法時,底層將通過反射,呼叫我們實現的invoke方法。

public class Main { public static void main(String[] args) { 
   //獲取InvocationHandler物件 在構造方法中注入目標物件 
   InvocationHandler handler = new JdkProxySubject(new RealSubject()); 

   //獲取代理類物件 
  Subject proxySubject = (Subject)Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Subject.class}, handler); 

   //呼叫目標方法
   proxySubject.request(); proxySubject.response();

}

執行結果:

before 前置通知
執行目標物件的request方法......
after 後置通知
before 前置通知
執行目標物件的response方法......
after 後置通知

JDK動態代理優缺

優點

JDK動態代理是JDK原生的,不需要任何依賴即可使用;

通過反射機制生成代理類的速度要比CGLib操作位元組碼生成代理類的速度更快;

缺點

如果要使用JDK動態代理,被代理的類必須實現了介面,否則無法代理;

JDK動態代理無法為沒有在介面中定義的方法實現代理,假設我們有一個實現了介面的類,我們為它的一個不屬於介面中的方法配置了切面,Spring仍然會使用JDK的動態代理,但是由於配置了切面的方法不屬於介面,為這個方法配置的切面將不會被織入。

JDK動態代理執行代理方法時,需要通過反射機制進行回撥,此時方法執行的效率比較低;

CGLib代理

CGLIB組成結構

Cglib是一個強大的、高效能的程式碼生成包,它廣泛被許多AOP框架使用,為他們提供方法的攔截,如下圖所示Cglib與Spring等應用的關係:

  • 最底層的是位元組碼 Bytecode ,位元組碼是Java為了保證“一次編譯、到處執行”而產生的一種虛擬指令格式,例如iload_0、iconst_1、if_icmpne、dup等
  • 位於位元組碼之上的是 ASM ,這是一種直接操作位元組碼的框架,應用ASM需要對Java位元組碼、Class結構比較熟悉
  • 位於 ASM 之上的是 CGLIBGroovyBeanShell ,後兩種並不是Java體系中的內容而是指令碼語言,它們通過ASM框架生成位元組碼變相執行Java程式碼,這說明 在JVM中執行程式並不一定非要寫Java程式碼----只要你能生成Java位元組碼,JVM並不關心位元組碼的來源 ,當然通過Java程式碼生成的JVM位元組碼是通過編譯器直接生成的,算是最“正統”的JVM位元組碼
  • 位於 CGLIBGroovyBeanShell 之上的就是 HibernateSpring AOP 這些框架了,這一層大家都比較熟悉
  • 最上層的是Applications,即具體應用,一般都是一個Web專案或者本地跑一個程式

所以,Cglib的實現是在位元組碼的基礎上的,並且使用了開源的ASM讀取位元組碼,對類實現增強功能的。

以上

作者簡介

陳睿|mikechen,10年+大廠架構經驗,《BAT架構技術500期》系列文章作者,分享十餘年BAT架構經驗以及面試心得!

閱讀mikechen的網際網路架構更多技術文章合集

Java併發 | JVM | MySQL | Spring | Redis | 分散式 | 高併發 | 架構師

關注「mikechen 的網際網路架構」公眾號,回覆 【架構】 領取我原創的《300 期 + BAT 架構技術系列與 1000 + 大廠面試題答案》