1. 程式人生 > >【小家Spring】Spring AOP的多種使用方式以及神一樣的AspectJ-AOP使用介紹

【小家Spring】Spring AOP的多種使用方式以及神一樣的AspectJ-AOP使用介紹

相關閱讀

【小家java】java5新特性(簡述十大新特性) 重要一躍
【小家java】java6新特性(簡述十大新特性) 雞肋升級
【小家java】java7新特性(簡述八大新特性) 不溫不火
【小家java】java8新特性(簡述十大新特性) 飽受讚譽
【小家java】java9新特性(簡述十大新特性) 褒貶不一
【小家java】java10新特性(簡述十大新特性) 小步迭代
【小家java】java11新特性(簡述八大新特性) 首個重磅LTS版本


什麼是AOP

AOP(Aspect-OrientedProgramming,面向方面程式設計),可以說是OOP(Object-Oriented Programing,面向物件程式設計)的補充和完善。

AOP技它利用一種稱為“橫切”的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果說“物件”是一個空心的圓柱體,其中封裝的是物件的屬性和行為;那麼面向方面程式設計的方法,就彷彿一把利刃,將這些空心圓柱體剖開,以獲得其內部的訊息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。

實現AOP的技術,主要分為兩大類:一是採用動態代理技術(典型代表為Spring AOP

),利用擷取訊息的方式(典型代表為AspectJ-AOP),對該訊息進行裝飾,以取代原有物件行為的執行;二是採用靜態織入的方式,引入特定的語法建立“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的程式碼。

相關概念

  • 切面(Aspect):一個關注點的模組化,這個關注點實現可能另外橫切多個物件。事務管理是J2EE應用中一個很好的橫切關注點例子。切面用spring的 Advisor或攔截器實現。
  • 連線點(Joinpoint): 程式執行過程中明確的點,如方法的呼叫或特定的異常被丟擲。
  • 通知(Advice): 在特定的連線點,AOP框架執行的動作。各種型別的通知包括“around”、“before”和“throws”通知。通知型別將在下面討論。許多AOP框架包括Spring都是以攔截器做通知模型,維護一個“圍繞”連線點的攔截器鏈。Spring中定義了四個advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
  • 切入點(Pointcut): 指定一個通知將被引發的一系列連線點的集合。AOP框架必須允許開發者指定切入點:例如,使用正則表示式。 Spring定義了Pointcut介面,用來組合MethodMatcher和ClassFilter,可以通過名字很清楚的理解, MethodMatcher是用來檢查目標類的方法是否可以被應用此通知,而ClassFilter是用來檢查Pointcut是否應該應用到目標類上
  • 引入(Introduction): 新增方法或欄位到被通知的類。 Spring允許引入新的介面到任何被通知的物件。例如,你可以使用一個引入使任何物件實現 IsModified介面,來簡化快取。Spring中要使用Introduction, 可有通過DelegatingIntroductionInterceptor來實現通知,通過DefaultIntroductionAdvisor來配置Advice和代理類要實現的介面(使用較少)
  • 目標物件(Target Object): 包含連線點的物件。也被稱作被通知或被代理物件。POJO
  • AOP代理(AOP Proxy): AOP框架建立的物件,包含通知。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。
  • 織入(Weaving): 組裝方面來建立一個被通知物件。這可以在編譯時完成(例如使用AspectJ編譯器),也可以在執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。

AOP概念的通俗理解

1.通知(Advice): 通知定義了切面是什麼以及何時使用。描述了切面要完成的工作和何時需要執行這個工作。
2.連線點(Joinpoint): 程式能夠應用通知的一個“時機”,這些“時機”就是連線點,例如方法被呼叫時、異常被丟擲時等等。
3.切入點(Pointcut) :通知定義了切面要發生的“故事”和時間,那麼切入點就定義了“故事”發生的地點,例如某個類或方法的名稱,Spring中允許我們方便的用正則表示式來指定
4.切面(Aspect) :通知和切入點共同組成了切面:時間、地點和要發生的“故事”
5.引入(Introduction) :引入允許我們向現有的類新增新的方法和屬性(Spring提供了一個方法注入的功能)
6.目標(Target) :即被通知的物件,如果沒有AOP,那麼它的邏輯將要交叉別的事務邏輯,有了AOP之後它可以只關注自己要做的事(AOP讓他做愛做的事)
7.代理(proxy) :應用通知的物件,詳細內容參見設計模式裡面的代理模式
8.織入(Weaving) :把切面應用到目標物件來建立新的代理物件的過程,織入一般發生在如下幾個時機:
---- (1)編譯時:當一個類檔案被編譯時進行織入,這需要特殊的編譯器才可以做的到,例如AspectJ的織入編譯器
---- (2)類載入時:使用特殊的ClassLoader在目標類被載入到程式之前增強類的位元組程式碼
----(3)執行時:切面在執行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術

Spring AOP的三種實現方式(基於Spring Boot)

一、基於XML配置的Spring AOP

現在都是spring boot的時代了,因此基於xml配置的例子,本文不做介紹了,有需要的可以自己去找其餘博文閱讀

二、基於ProxyFactoryBean,編碼的方式來實現

導包

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

有如下包即可正常工作了
在這裡插入圖片描述

為何Spring自己實現了AOP,還需要匯入org.aspectj:aspectjweaver的包呢?

官網解釋的原因如下:

  • 原因一:spring確實有自己的AOP。功能已經基本夠用了,除非你的要在介面上動態代理或者方法攔截精確到getter和setter。這些都是寫奇葩的需求Spring做不到,但一般不使用。
  • 原因二:1、如果使用xml方式,不需要任何額外的jar包。2、如果使用@Aspect的註解方式。你就可以在類上直接一個@Aspect就搞定,不用費事在xml裡配了。但是這需要額外的jar包( aspectjweaver.jar)。因為spring直接使用AspectJ的註解功能,注意只是使用了它 的註解功能而已。並不是核心功能 !!!

注意到文件上還有一句很有意思的話:文件說到 是選擇spring AOP還是使用full aspectJ?什麼是full aspectJ?如果你使用"full aspectJ"。就是說你可以實現基於介面的動態代理,等等強大的功能。而不僅僅是aspectj的 注-解-功-能 !!!

如果用full AspectJ。比如說Load-Time Weaving的方式 還 需要額外的jar包 spring-instrument.jar。。。現在明白了吧~~~ 具體詳情,後面在講述AspectJ裡可以看見~~

基本類如下:

// A類:
@Service
public class AServiceImpl implements AService {
    @Override
    public void sayHelloA() {
        System.out.println("hello A");
    }
}
// B類:
@Service
public class BServiceImpl implements BService {
    @Override
    public void sayHelloB() {
        System.out.println("hello B");
    }
}
// C類:
@Service
public class CServiceImpl implements CService {
    @Override
    public void sayHelloC() {
        System.out.println("hello C");
    }
}

通過實現介面的方式編寫的通知類

/**
 * 在方法之前、之後 列印輸出日誌
 *
 * @author [email protected]
 * @description
 * @date 2018-10-29 17:42
 */
@Component //通知元件交給容器管理
public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice, MethodInterceptor {

    //MethodBeforeAdvice的方法
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("MethodBeforeAdvice...before...");
    }

    //AfterReturningAdvice的方法
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("AfterReturningAdvice...afterReturning...");
    }


    //MethodInterceptor的方法
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("MethodInterceptor...invoke...start");
        Object proceed = invocation.proceed();
        System.out.println("MethodInterceptor...invoke...end");
        return proceed;
    }
}

實現介面 MethodBeforeAdvice該攔截器會在呼叫方法前執行
實現介面 AfterReturningAdvice該攔截器會在呼叫方法後執行
實現介面 MethodInterceptor該攔截器會在呼叫方法前後都執行,實現環繞效果

然後就是配置了,其中最重要的類為ProxyFactoryBean、BeanNameAutoProxyCreator、AspectJExpressionPointcutAdvisor等等代理類,能達到強大的效果。這種一般都是spring時代基於xml的書寫方式,因此這裡不做詳細講解,SpringBoot時代,建議使用優雅的註解的風格編寫,但本文提供一個參考博文:
Spring AOP之ProxyFactoryBean與BeanNameAutoProxyCreator

三、基於註解方式@AspectJ實現AOP

PS:其實springboot此配置是預設開啟的,所以根本可以不用管了,在Springboot中使用過註解配置方式的人會問是否需要在程式主類中增加@EnableAspectJAutoProxy來啟用,實際並不需要。看下面關於AOP的預設配置屬性,其中spring.aop.auto屬性預設是開啟的,也就是說只要引入了AOP依賴後,其實預設已經增加了@EnableAspectJAutoProxy。
截圖看看Boot的一些預設配置:
SpringBoot AOP的預設配置

不過後面會有點奇怪的問題,springboot中,不管這個項是否設定為true或者false,都不會跟以前spring專案中,如果沒有設定為true,當代理類沒有繼承介面,啟動專案的時候會報錯。而springboot專案中,會自動轉換成使用CGLIB進行動態代理,其中原理是怎麼實現,就沒有去看底層程式碼了,估計底層進行了改造吧!
切面:

/**
 * @author [email protected]
 * @description
 * @date 2018-10-30 11:32
 */
@Component //這個元件一定得加入到容器才行
@Aspect
public class SpringLogAspect {

    //定義一個切入點:指定哪些方法可以被切入(如果是別的類需要使用 請用該方法的全類名)
    @Pointcut("execution(* com.fsx.run.service..*.*(..))")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        System.out.println("AOP Before Advice...");
    }

    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint) {
        System.out.println("AOP After Advice...");
    }

    @AfterReturning(pointcut = "pointCut()", returning = "returnVal")
    public void afterReturn(JoinPoint joinPoint, Object returnVal) {
        System.out.println("AOP AfterReturning Advice:" + returnVal);
    }

    @AfterThrowing(pointcut = "pointCut()", throwing = "error")
    public void afterThrowing(JoinPoint joinPoint, Throwable error) {
        System.out.println("AOP AfterThrowing Advice..." + error);
        System.out.println("AfterThrowing...");
    }

    // 環繞通知:此處有一個坑,當AfterReturning和Around共存時,AfterReturning是獲取不到返回值的
    //@Around("pointCut()")
    //public void around(ProceedingJoinPoint pjp) {
    //    System.out.println("AOP Aronud before...");
    //    try {
    //        pjp.proceed();
    //    } catch (Throwable e) {
    //        e.printStackTrace();
    //    }
    //    System.out.println("AOP Aronud after...");
    //}

}

idea很智慧:如果切中了,會有這個小圖示的
在這裡插入圖片描述
controller層這麼測試:

    @Autowired
    AService aService;
    @Autowired
    BService bService;

    @Override
    public Object testDemo(String str) {
        aService.sayHelloA();
        bService.sayHelloB();
        return str + "succuss~";
    }

訪問,控制檯列印結果如下:

AOP Before Advice...
hello A
AOP After Advice...
AOP AfterReturning Advice:AServiceImpl
AOP Before Advice...
hello B
AOP After Advice...
AOP AfterReturning Advice:BServiceImpl

此處有一個坑,當AfterReturning和Around共存時,AfterReturning是獲取不到返回值的。當然,如果你的方法本來就沒有返回值,那肯定也是null咯

關於@Pointcut切點表示式的書寫,請參見:
小家Spring】Spring AOP中@Pointcut切入點表示式最全面使用介紹


神一樣的AspectJ --> AOP的領跑者

AspectJ 可以成為是AOP的鼻祖,規範的制定者。】

因此本文先進行一個簡單案例的演示,然後引出AOP中一些晦澀難懂的抽象概念,放心,通過本篇部落格,我們將會非常輕鬆地理解並掌握它們。編寫一個HelloWord的類,然後利用AspectJ技術切入該類的執行過程。
Hello類:

public class HelloWord {

    public void sayHello(){
        System.out.println("hello world !");
    }
    public static void main(String args[]){
        HelloWord helloWord =new HelloWord();
        helloWord.sayHello();
    }
}

編寫AspectJ類,注意關鍵字為aspect(MyAspectJDemo.aj,其中aj為AspectJ的字尾),含義與class相同,即定義一個AspectJ的類(注意:字尾名是.aj

public aspect MyAspectJDemo {
    /**
     * 定義切點,日誌記錄切點
     */
    pointcut recordLog():call(* HelloWord.sayHello(..));

    /**
     * 定義切點,許可權驗證(實際開發中日誌和許可權一般會放在不同的切面中,這裡僅為方便演示)
     */
    pointcut authCheck():call(* HelloWord.sayHello(..));

    /**
     * 定義前置通知!
     */
    before():authCheck(){
        System.out.println("sayHello方法執行前驗證許可權");
    }

    /**
     * 定義後置通知
     */
    after():recordLog(){
        System.out.println("sayHello方法執行後記錄日誌");
    }
}

這樣直接執行HelloWorld的main,是不會有AOP效果的。因為我們還需要配置:改變編譯器重新編譯後再執行,具體參考:
AspectJ——簡介以及在IntelliJ IDEA下的配置
配置好後執行結果如下:
執行結果

AspectJ是一個java實現的AOP框架,它能夠對java程式碼進行AOP編譯(一般在編譯期進行),讓java程式碼具有AspectJ的AOP功能(當然需要特殊的編譯器),可以這樣說AspectJ是目前實現AOP框架中最成熟,功能最豐富的語言,更幸運的是,AspectJ與java程式完全相容,幾乎是無縫關聯,因此對於有java程式設計基礎的工程師,上手和使用都非常容易。

AspectJ的織入方式及其原理概要

對於織入這個概念,可以簡單理解為aspect(切面)應用到目標函式(類)的過程。對於這個過程,一般分為動態織入和靜態織入,動態織入的方式是在執行時動態將要增強的程式碼織入到目標類中,這樣往往是通過動態代理技術完成的,如Java JDK的動態代理(Proxy,底層通過反射實現)或者CGLIB的動態代理(底層通過繼承實現),Spring AOP採用的就是基於執行時增強的代理技術,這點另一篇博文已經有分析了,這裡主要重點分析一下靜態織入。

ApectJ採用的就是靜態織入的方式。ApectJ主要採用的是編譯期織入,在這個期間使用AspectJ的acj編譯器(類似javac)把aspect類編譯成class位元組碼後,在java目標類編譯時織入,即先編譯aspect類再編譯目標類。
在這裡插入圖片描述
關於ajc編譯器,是一種能夠識別aspect語法的編譯器,它是採用java語言編寫的,由於javac並不能識別aspect語法,便有了ajc編譯器,注意ajc編譯器也可編譯java檔案。為了更直觀瞭解aspect的織入方式,我們開啟前面案例中已編譯完成的HelloWord.class檔案,反編譯後的java程式碼如下:

//編譯後織入aspect類的HelloWord位元組碼反編譯類
public class HelloWord {
    public HelloWord() {
    }

    public void sayHello() {
        System.out.println("hello world !");
    }

    public static void main(String[] args) {
        HelloWord helloWord = new HelloWord();
        HelloWord var10000 = helloWord;

   try {
        //MyAspectJDemo 切面類的前置通知織入
        MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541();
        //目標類函式的呼叫
           var10000.sayHello();
        } catch (Throwable var3) {
        MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
            throw var3;
        }

        //MyAspectJDemo 切面類的後置通知織入 
        MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
    }
}

顯然AspectJ的織入原理已很明朗了,當然除了編譯期織入,還存在連結期(編譯後)織入,即將aspect類和java目標類同時編譯成位元組碼檔案後,再進行織入處理,這種方式比較有助於已編譯好的第三方jar和Class檔案進行織入操作,由於這不是本篇的重點,暫且不過多分析,掌握以上AspectJ知識點就足以協助理解Spring AOP了

Spring AOP的優點

Spring AOP 與ApectJ 的目的一致,都是為了統一處理橫切業務,但與AspectJ不同的是,Spring AOP 並不嘗試提供完整的AOP功能(即使它完全可以實現),Spring AOP 更注重的是與Spring IOC容器的結合,並結合該優勢來解決橫切業務的問題,因此在AOP的功能完善方面,相對來說AspectJ具有更大的優勢

同時,Spring注意到AspectJ在AOP的實現方式上依賴於特殊編譯器(ajc編譯器),因此Spring很機智迴避了這點,轉向採用動態代理技術的實現原理來構建Spring AOP的內部機制(動態織入),這是與AspectJ(靜態織入)最根本的區別。

在AspectJ 1.5後,引入@Aspect形式的註解風格的開發,Spring也非常快地跟進了這種方式,因此Spring 2.0後便使用了與AspectJ一樣的註解。請注意,Spring 只是使用了與 AspectJ 5 一樣的註解,但仍然沒有使用 AspectJ 的編譯器,底層依是動態代理技術的實現,因此並不依賴於 AspectJ 的編譯器

最後

博主希望通過本文,讓讀者能夠了解到Spring AOP和AspectJ的區別與聯絡。