天天看點

淺談AOP以及AspectJ和Spring AOP導言關鍵概念AspectJSpringAOPAspectJ和Spring AOP的差別和選擇參考文章

導言

AOP(Aspect Orient Programming),作為面向對象程式設計的一種補充,廣泛應用于處理一些具有橫切性質的系統級服務,如日志收集、事務管理、安全檢查、緩存、對象池管理等。AOP實作的關鍵就在于AOP架構自動建立的AOP代理,AOP代理則可分為靜态代理和動态代理兩大類,其中靜态代理是指使用AOP架構提供的指令進行編譯,進而在編譯階段就可生成 AOP 代理類,是以也稱為編譯時增強;而動态代理則在運作時借助于

JDK動态代理

CGLIB

等在記憶體中“臨時”生成AOP動态代理類,是以也被稱為運作時增強。

面向切面的程式設計(AOP) 是一種程式設計範式,旨在通過允許橫切關注點的分離,提高子產品化。AOP提供切面來将跨越對象關注點子產品化。雖然現在可以獲得許多AOP架構,但在這裡我們要區分的隻有兩個流行的架構:Spring AOP和AspectJ。

關鍵概念

Aspect

Aspect被翻譯方面或者切面,相當于OOP中的類,就是封裝用于橫插入系統的功能。例如日志、事務、安全驗證等。

JoinPoint

JoinPoint(連接配接點)是AOP中的一個重要的關鍵概念。JoinPoint可以看做是程式運作時的一個執行點。打個比方,比如執行System.out.println("Hello")這個函數,println()就是一個joinpoint;再如給一個變量指派也是一個joinpoint;還有最常用的for循環,也是一個joinpoint。

理論上說,一個程式中很多地方都可以被看做是JoinPoint,但是AspectJ中,隻有下面所示的幾種執行點被認為是JoinPoint:

表1 JoinPoint的類型

說明 示例
method call 函數調用 比如調用Logger.info(),這是一處JoinPoint
method execution 函數執行 比如Logger.info()的執行内部,是一處JoinPoint。注意它和method call的差別。method call是調用某個函數的地方。而execution是某個函數執行的内部。
constructor call 構造函數調用 和method call類似
constructor execution 構造函數執行 和method execution類似
field get 擷取某個變量 比如讀取User.name成員
field set 設定某個變量 比如設定User.name成員
pre-initialization Object在構造函數中做得一些工作。
initialization Object在構造函數中做得工作
static initialization 類初始化 比如類的static{}
handler 異常處理 比如try catch(xxx)中,對應catch内的執行
advice execution 這個是AspectJ的内容

這裡列出了AspectJ所認可的JoinPoint的類型。實際上,連接配接點也就是你想把新的代碼插在程式的哪個地方,是插在構造方法中,還是插在某個方法調用前,或者是插在某個方法中,這個地方就是JoinPoint,當然,不是所有地方都能給你插的,隻有能插的地方,才叫JoinPoint。

PointCut

PointCut通俗地翻譯為切入點,一個程式會有多個Join Point,即使同一個函數,也還分為call和execution類型的Join Point,但并不是所有的Join Point都是我們關心的,Pointcut就是提供一種使得開發者能夠選擇自己需要的JoinPoint的方法。PointCut分為

call

execution

target

this

within

等關鍵字。與joinPoint相比,pointcut就是一個具體的切點。

Advice

Advice翻譯為通知或者增強(Advisor),就是我們插入的代碼以何種方式插入,相當于OOP中的方法,有Before、After以及Around。

  • Before

    前置通知用于将切面代碼插入方法之前,也就是說,在方法執行之前,會首先執行前置通知裡的代碼.包含前置通知代碼的類就是切面。

  • After

    後置通知的代碼在調用被攔截的方法後調用。

  • Around

    環繞通知能力最強,可以在方法調用前執行通知代碼,可以決定是否還調用目标方法。也就是說它可以控制被攔截的方法的執行,還可以控制被攔截方法的傳回值。

Target

Target指的是需要切入的目标類或者目标接口。

Proxy

Proxy是代理,AOP工作時是通過代理對象來通路目标對象。其實AOP的實作是通過動态代理,離不開代理模式,是以必須要有一個代理對象。

Weaving

Weaving即織入,在目标對象中插入切面代碼的過程就叫做織入。

AspectJ

AspectJ的介紹

AspectJ是一個面向切面的架構,他定義了AOP的一些文法,有一個專門的位元組碼生成器來生成遵守java規範的 class檔案。

AspectJ的通知類型不僅包括我們之前了解過的三種通知:前置通知、後置通知、環繞通知,在Aspect中還有異常通知以及一種最終通知即無論程式是否正常執行,最終通知的代碼會得到執行。

AspectJ提供了一套自己的表達式語言即切點表達式,切入點表達式可以辨別切面織入到哪些類的哪些方法當中。隻要把切面的實作配置好,再把這個切入點表達式寫好就可以了,不需要一些額外的xml配置。

切點表達式文法:

execution(
    modifiers-pattern? //通路權限比對   如public、protected
    ret-type-pattern //傳回值類型比對
    declaring-type-pattern? //全限定性類名
    name-pattern(param-pattern) //方法名(參數名)
    throws-pattern? //抛出異常類型
)           

注意:

1. 中間以空格隔開,有問号的屬性表示可以省略。

2. 表達式中特殊符号說明:

  • a:

    *

    代表0到多個任意字元,通常用作某個包下面的某些類以及某些方法。
  • b:

    ..

    放在方法參數中,代表任意個參數,放在包名後面表示目前包及其所有子包路徑。
  • c:

    +

    放在類名後,表示目前類及其子類,放在接口後,表示目前接口及其實作類。

表2 方法表達式

表達式 含義
java.lang.String 比對String類型
java.*.String 比對java包下的任何“一級子包”下的String類型,如比對java.lang.String,但不比對java.lang.ss.String
java..* 比對java包及任何子包下的任何類型,如比對java.lang.String、java.lang.annotation.Annotation
java.lang.*ing 比對任何java.lang包下的以ing結尾的類型
java.lang.Number+ 比對java.lang包下的任何Number的自類型,如比對java.lang.Integer,也比對java.math.BigInteger

表3 參數表達式

參數
() 表示方法沒有任何參數
(..) 表示比對接受任意個參數的方法
(..,java.lang.String) 表示比對接受java.lang.String類型的參數結束,且其前邊可以接受有任意個參數的方法
(java.lang.String,..) 表示比對接受java.lang.String類型的參數開始,且其後邊可以接受任意個參數的方法
(*,java.lang.String) 表示比對接受java.lang.String類型的參數結束,且其前邊接受有一個任意類型參數的方法

舉個栗子:

execution(public * com.zhoujunwen.service.*.*(..))

,該表達式表示com.zhoujunwen.service包下的public通路權限的任意類的任意方法。

AspectJ的安裝以及常用指令

AspectJ下載下傳位址(

http://www.eclipse.org/aspectj/downloads.php)

,在下載下傳頁面選擇合适的版本下載下傳,目前最新穩定版是1.9.1。下載下傳完之後雙加jar包安裝,安裝界面如下:

安裝目錄用tree指令可以看到如下結構(省去doc目錄):

├── LICENSE-AspectJ.html
├── README-AspectJ.html
├── bin
│   ├── aj
│   ├── aj5
│   ├── ajbrowser
│   ├── ajc
│   └── ajdoc
└── lib
    ├── aspectjrt.jar
    ├── aspectjtools.jar
    ├── aspectjweaver.jar
    └── org.aspectj.matcher.jar

42 directories, 440 files           
  • bin:存放aj、aj5、ajc、ajdoc、ajbrowser等指令,其中ajc指令最常用,它的作用類似于javac。
  • doc:存放了AspectJ的使用說明、參考手冊、API文檔等文檔。
  • lib:該路徑下的4個JAR檔案是AspectJ的核心類庫。

注意安裝完成後,需要配置将

aspectjrt.jar

配置到CLASSPATH中,并且将

bin

目錄配置到PATH中。下面以MacOs配置為例:

JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:/Users/yourname/Documents/software/aspectj1.9.1/lib/aspectjrt.jar

M2_HOME=/Users/yourname/Documents/software/apache-maven-3.5.0
PATH=$JAVA_HOME/bin:$M2_HOME/bin:/usr/local/bin:/Users/yourname/Documents/software/aspectj1.9.1/bin:$PATH           

注意:其中

/Users/yourname/Documents/software/aspectj1.9.1/lib/aspectjrt.jar

替換為自己安裝AspectJ的路徑的lib,

/Users/yourname/Documents/software/aspectj1.9.1/bin

替換為安裝AspectJ的bin目錄

AspectJ的demo

驗證AspectJ的切面功能,寫個單純的AspectJ的demo,實作方法日志埋點,在方法後增強。

業務代碼(AuthorizeService.java):

package com.zhoujunwen.aop;

/**
* 不用太過于較真業務邏輯的處理,大概意思大家懂就好。
* @author zhoujunwen
* @version 1.0.0
*/
public class AuthorizeService {
    private static final String USERNAME = "zhoujunwen";
    private static final String PASSWORD = "123456";
    public void login(String username, String password) {
        if (username == null || username.length() == 0) {
            System.out.print("使用者名不能為空");
            return;
        }
        if (password == null || password.length() == 0) {
            System.out.print("使用者名不能為空");
            return;
        }
        if (!USERNAME.equals(username) || !PASSWORD.equals(password)) {
            System.out.print("使用者名或者密碼不對");
            return;
        }
        System.out.print("登入成功");
    }

    public static void main(String[] args) {
        AuthorizeService as = new AuthorizeService();
        as.login("zhoujunwen", "123456");
    }
}           

日志埋點切面邏輯(LogAspect.java):

package com.zhoujunwen.aop;

public aspect LogAspect {
    pointcut logPointcut():execution(void AuthorizeService.login(..));
    after():logPointcut(){
         System.out.println("****處理日志****"); 
    }
}            

将上述兩個檔案檔案放置在同一個目錄,在目前目錄下執行acj編譯和織入指令:

ajc -d . AuthorizeService.java LogAspect.java           

如果配置一切OK的話,不會出現異常或者錯誤,并在目前目錄生成

com/zhoujunwen/aop/AuthorizeService.class

com/zhoujunwen/aop/LogAspect.class

兩個位元組碼檔案,執行tree(自己編寫的類似Linux的tree指令)指令檢視目錄結構:

zhoujunwendeMacBook-Air:aop zhoujunwen$ tree
.
├── AuthorizeService.java
├── LogAspect.java
└── com
    └── zhoujunwen
        └── aop
            ├── AuthorizeService.class
            └── LogAspect.class

3 directories, 4 files           

最後執行java執行指令:

java com/zhoujunwen/aop/AuthorizeService           

輸出日志内容:

登入成功處理日志

ajc可以了解為javac指令,都用于編譯Java程式,差別是ajc指令可識别AspectJ的文法;我們可以将ajc當成一個增強版的javac指令。執行ajc指令後的AuthorizeService.class 檔案不是由原來的AuthorizeService.java檔案編譯得到的,該AuthorizeService.class裡新增了列印日志的内容——這表明AspectJ在編譯時“自動”編譯得到了一個新類,這個新類增強了原有的AuthorizeService.java類的功能,是以AspectJ通常被稱為編譯時增強的AOP架構。

為了驗證上述的結論,我們用javap指令反編譯AuthorizeService.class檔案。javap是Java class檔案分解器,可以反編譯(即對javac編譯的檔案進行反編譯),也可以檢視java編譯器生成的位元組碼。用于分解class檔案。

javap -p -c com/zhoujunwen/aop/AuthorizeService.class           

輸出内容如下,在login方法的code為0、3以及91、94的地方,會發現

invokestatic

com/zhoujunwen/aop/LogAspect

的代碼,這說明上面的結論是正确的。

Compiled from "AuthorizeService.java"
public class com.zhoujunwen.aop.AuthorizeService {
  private static final java.lang.String USERNAME;

  private static final java.lang.String PASSWORD;

  public com.zhoujunwen.aop.AuthorizeService();
    Code:
       0: aload_0
       1: invokespecial #16                 // Method java/lang/Object."<init>":()V
       4: return

  public void login(java.lang.String, java.lang.String);
    Code:
       0: invokestatic  #70                 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
       3: invokevirtual #76                 // Method com/zhoujunwen/aop/LogAspect.ajc$before$com_zhoujunwen_aop_LogAspect$2$9fd5dd97:()V
       6: aload_1
       7: ifnull        17
      10: aload_1
      11: invokevirtual #25                 // Method java/lang/String.length:()I
      14: ifne          28
      17: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      20: ldc           #37                 // String 使用者名不能為空
      22: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      25: goto          99
      28: aload_2
      29: ifnull        39
      32: aload_2
      33: invokevirtual #25                 // Method java/lang/String.length:()I
      36: ifne          50
      39: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      42: ldc           #37                 // String 使用者名不能為空
      44: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      47: goto          99
      50: ldc           #8                  // String zhoujunwen
      52: aload_1
      53: invokevirtual #45                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      56: ifeq          68
      59: ldc           #11                 // String 123456
      61: aload_2
      62: invokevirtual #45                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      65: ifne          79
      68: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      71: ldc           #49                 // String 使用者名或者密碼不對
      73: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      76: goto          99
      79: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      82: ldc           #51                 // String 登入成功
      84: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      87: goto          99
      90: astore_3
      91: invokestatic  #70                 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
      94: invokevirtual #73                 // Method com/zhoujunwen/aop/LogAspect.ajc$after$com_zhoujunwen_aop_LogAspect$1$9fd5dd97:()V
      97: aload_3
      98: athrow
      99: invokestatic  #70                 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
     102: invokevirtual #73                 // Method com/zhoujunwen/aop/LogAspect.ajc$after$com_zhoujunwen_aop_LogAspect$1$9fd5dd97:()V
     105: return
    Exception table:
       from    to  target type
           6    90    90   Class java/lang/Throwable

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class com/zhoujunwen/aop/AuthorizeService
       3: dup
       4: invokespecial #57                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #8                  // String zhoujunwen
      11: ldc           #11                 // String 123456
      13: invokevirtual #58                 // Method login:(Ljava/lang/String;Ljava/lang/String;)V
      16: return
}           

SpringAOP

Spring AOP介紹

Spring AOP也是對目标類增強,生成代理類。但是與AspectJ的最大差別在于——Spring AOP的運作時增強,而AspectJ是編譯時增強。

dolphin叔叔文章中寫道自己曾經誤以為AspectJ是Spring AOP的一部分,我想大多數人都沒有弄清楚AspectJ和Spring AOP的關系。

Spring AOP與Aspect無關性

當你不用Spring AOP提供的注解時,Spring AOP和AspectJ沒半毛錢的關系,前者是JDK動态代理,用到了CGLIB(Code Generation Library),CGLIB是一個代碼生成類庫,可以在運作時候動态是生成某個類的子類。代理模式為要通路的目标對象提供了一種途徑,當通路對象時,它引入了一個間接的層。後者是靜态代理,在編譯階段就已經編譯到位元組碼檔案中。Spring中提供了前置通知

org.springframework.aop.MethodBeforeAdvice

、後置通知

org.springframework.aop.AfterReturningAdvice

,環繞通知

org.aopalliance.intercept.MethodInvocation

(通過反射實作,invoke(org.aopalliance.intercept.MethodInvocation mi)中的MethodInvocation擷取目标方法,目标類,目标字段等資訊),異常通知

org.springframework.aop.ThrowsAdvice

。這些通知能夠切入目标對象,Spring AOP的核心是代理Proxy,其主要實作類是

org.springframework.aop.framework.ProxyFactoryBean

,ProxyFactoryBean中

proxyInterfaces

為代理指向的目标接口,Spring AOP無法截獲未在該屬性指定的接口中的方法,

interceptorNames

是攔截清單,

target

是目标接口實作類,一個代理隻能有一個target。

Spring AOP的核心類

org.springframework.aop.framework.ProxyFactoryBean

雖然能實作AOP的行為,但是這種方式具有局限性,需要在代碼中顯式的調用ProxyFactoryBean代理工廠類,舉例:UserService是一個接口,UserServiceImpl是UserService的實作類,ApplicationContext context為Spring上下文,調用方式為

UserService userService = (UserService)context.getBean("userProxy");

完整的配置如下:

<bean id="userService" class="com.zhoujunwen.UserServiceImpl"></bean>  

<!-- 定義前置通知,com.zhoujunwen.BeforeLogAdvice實作了org.springframework.aop.MethodBeforeAdvice -->  
<bean id="beforeLogAdvice" class="com.zhoujunwen.BeforeLogAdvice"></bean>  
<!-- 定義後置通知,com.zhoujunwen.AfterLogAdvice實作了org.springframework.aop.AfterReturningAdvice -->  
<bean id="afterLogAdvice" class="com.zhoujunwen.AfterLogAdvice"></bean>  
<!-- 定義異常通知, com.zhoujunwen.ThrowsLogAdvice實作了org.springframework.aop.ThrowsAdvice-->  
<bean id="throwsLogAdvice" class="com.zhoujunwen.ThrowsLogAdvice"></bean>  
<!-- 定義環繞通知,com.zhoujunwen.LogAroundAdvice實作了org.aopalliance.intercept.MethodInvocation -->  
<bean id="logAroundAdvice" class="com.zhoujunwen.LogAroundAdvice"></bean>  

<!-- 定義代理類,名 稱為userProxy,将通過userProxy通路業務類中的方法 -->  
<bean id="userProxy" class="org.springframework.aop.framework.ProxyFactoryBean">  
    <property name="proxyInterfaces">  
        <value>com.zhoujunwen.UserService</value>  
    </property>  
    <property name="interceptorNames">  
        <list>           
         <value>beforeLogAdvice</value>  
         <!-- 織入後置通知 -->  
         <value>afterLogAdvice</value>  
         <!-- 織入異常通知 -->  
         <value>throwsLogAdvice</value>  
         <!-- 織入環繞通知 -->  
         <value>logAroundAdvice</value>  
        </list>  
    </property>  
    <property name="target" ref="userService"></property>  
</bean>           

當然,上述的局限性spring官方也給出了解決方案,讓AOP的通知在服務調用方毫不知情的下就進行織入,可以通過

org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator

自動代理。

<bean id="myServiceAutoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">  
        <property name="interceptorNames">  
                <list>
                         <value>logAroundAdvice</value> 
                </list>  
        </property>  
        <property name="beanNames">  
                <value>*Service</value>  
        </property>  
</bean>             

這個BeanNameAutoProxyCreator的bean中指明上下文中所有調用以Service結尾的服務類都會被攔截,執行logAroundAdvice的invoke方法。同時它會自動生成Service的代理,這樣在使用的時候就可以直接取服務類的bean,而不用再像上面那樣還用取代理類的bean。

對于BeanNameAutoProxyCreator建立的代理,可以這樣調用:

UserService userService = (UserService) context.getBean("userService");

,context為spring上下文。

Spring AOP與AspectJ有關性

當你用到Spring AOP提供的注入@Before、@After等注解時,Spring AOP和AspectJ就有了關系。在開發中引入了

org.aspectj:aspectjrt:1.6.11

org.aspectj:aspectjweaver:1.6.11

兩個包,這是因為Spring AOP使用了AspectJ的Annotation,使用了Aspect來定義切面,使用Pointcut來定義切入點,使用Advice來定義增強處理。雖然Spring AOP使用了Aspect的Annotation,但是并沒有使用它的編譯器和織入器。

Spring AOP其實作原理是JDK動态代理,在運作時生成代理類。為了啟用Spring對

@AspectJ

切面配置的支援,并保證Spring容器中的目标Bean被一個或多個切面自動增強,必須在Spring配置檔案中添加如下配置

<aop:aspectj-autoproxy/>           

當啟動了@AspectJ支援後,在Spring容器中配置一個帶

@Aspect

注釋的Bean,Spring将會自動識别該 Bean,并将該Bean作為切面Bean處理。切面Bean與普通Bean沒有任何差別,一樣使用

<bean.../>

元素進行配置,一樣支援使用依賴注入來配置屬性值。

Spring AOP注解使用demo

全注解實作

業務邏輯代碼(AuthorizeService.java):

package com.zhoujunwen.engine.service;

import org.springframework.stereotype.Service;

/**
 * Created with IntelliJ IDEA.
 * Date: 2018/10/25
 * Time: 12:47 PM
 * Description:
 *
 * @author zhoujunwen
 * @version 1.0
 */
@Service
public class AuthorizeService {
    private static final String USERNAME = "zhoujunwen";
    private static final String PASSWORD = "123456";
    public void login(String username, String password) {
        if (username == null || username.length() == 0) {
            System.out.print("使用者名不能為空");
            return;
        }
        if (password == null || password.length() == 0) {
            System.out.print("使用者名不能為空");
            return;
        }
        if (!USERNAME.equals(username) || !PASSWORD.equals(password)) {
            System.out.print("使用者名或者密碼不對");
            return;
        }
        System.out.print("登入成功");
    }
}           

切面邏輯代碼(LogAspect.java)

package com.zhoujunwen.engine.service;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * Created with IntelliJ IDEA.
 * Date: 2018/10/25
 * Time: 1:04 PM
 * Description:
 *
 * @author zhoujunwen
 * @version 1.0
 */
@Aspect
@Component
public class LogAspect {
    @After("execution(* com.zhoujunwen.engine.service.AuthorizeService.login(..))")
    public void logPointcut(){
        System.out.println("***處理日志***");
    }
}
           

這樣是實作了對AuthorizeService.login()方法的後置通知。不需要在xml中其他配置,當然前提是開啟

<aop:aspectj-autoproxy/>

aspectj的自動代理。

測試調用代碼:

AuthorizeService authorizeService = SpringContextHolder.getBean(AuthorizeService.class);
authorizeService.login("zhangsan", "zs2018");           

xml配置實作

業務代碼,日志埋點(MeasurementService.java):

package com.zhoujunwen.engine.measurement;

import com.zhoujunwen.common.base.AccountInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * metrics 切面接口
 * @create 2018-08-16-上午10:13
 */
@Service
public class MeasurementService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementService.class);

    public String gainZhimaLog(AccountInfo accountInfo) {
        if (NumberUtils.isNumber(accountInfo.getZhimaPoint())) {
            return "正常";
        } else if (StringUtils.contains(accountInfo.getZhimaPoint(), "*")) {
            return "未授權";
        } else {
            return "未爬到";
        }
    }

    public String gainJiebeiLog(AccountInfo accountInfo) {
        if (NumberUtils.isNumber(accountInfo.getJiebeiQuota())) {
            return "正常";
        }
        return "未爬到";

    }

    public String gainHuabeiLog(AccountInfo accountInfo) {
        if (accountInfo.getCreditQuota() != null) {
            return "正常";
        } else {
            return "未爬到";
        }
    }
}
           

切面邏輯,統計日志中個字段的總和(KeywordMeasurement.java):

package com.zhoujunwen.engine.measurement;

import com.zhoujunwen.common.base.AccountInfo;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;

/**
 * 關鍵字段監控統計 <br>
 *
 * @create 2018-08-15-下午5:41
 */
public class KeywordMeasurement {

    private String invokeCountFieldName = "";
    /**
     * 調用次數
     */
    public void summary(JoinPoint joinPoint, Object result) {
        try {

            String msg;
            String resultStr = "";
            if (result instanceof String) {
                resultStr = (String) result;
            }
            if (StringUtils.isBlank(resultStr)) {
                return;
            }
            if ("正常".equals(resultStr)) {
                msg = "_ok";
            } else if ("未爬到".equals(resultStr)) {
                msg = "_empty";
            } else {
                msg = "_star";
            }

            String methodName = joinPoint.getSignature().getName();
            Object args[] = joinPoint.getArgs();
            AccountInfo accountInfo = null;
            for (Object arg : args) {
                if (arg.getClass().getName().contains("AccountInfo")) {
                    accountInfo = (accountInfo) arg;
                }
            }
           
            if (methodName.contains("Zhima")) {
                invokeCountFieldName = "zhima" + msg;
            } else if (methodName.contains("Jiebei")) {
                invokeCountFieldName = "jiebei" + msg;
            } else if (methodName.contains("Huabei")) {
                invokeCountFieldName = "huabei" + msg;
            } else {
                return;
            }
            // TODO 寫入到influxDB
        } catch (Exception e) {
            //skip
        }
    }
}           

完整的配置(後置通知,并需要傳回結果):

<bean id="keywordMeasurement" class="com.zhoujunwen.engine.measurement.KeywordMeasurement"/>

<aop:config proxy-target-class="true">
    <aop:aspect id="keywordMeasurementAspect" ref="keywordMeasurement">
        <aop:pointcut id="keywordMeasurementPointcut"
                      expression="execution(* com.zhoujunwen.engine.measurement.SdkMeasurementService.gain*(..))"/>
                <!-- 統計summary,summary方法有兩個參數JoinPoint和Object-->
        <aop:after-returning method="summary" returning="result" pointcut-ref="keywordMeasurementPointcut"/>
    </aop:aspect>
</aop:config>           

其他可用的配置(省略了rt、count、qps的aspect):

<!-- 統計RT,rt方法隻有一個參數ProceedingJoinPoint-->
<aop:around method="rt" pointcut-ref="keywordMeasurementPointcut"/> 
<!--統計調用次數,count方法隻有一個參數JoinPoint-->
<aop:after method="count" pointcut-ref="keywordMeasurementPointcut"/>
<!--統計QPS,qps方法隻有一個參數JoinPoint-->
<aop:after method="qps" pointcut-ref="keywordMeasurementPointcut"/>           

注意:關于Spring AOP中,切面代理類一定是由Spirng容器管理,是以委托類也需要交由Spring管理,不可以将委托類執行個體交由自己建立的容器管理(比如放入自己建立的Map中),如果這麼做了,當調用委托類執行個體的時候,切面是不生效的。

原因:(1)實作實作和目标類相同的接口,spring會使用JDK的java.lang.reflect.Proxy類,它允許Spring動态生成一個新類來實作必要的接口,織入通知,并且把這些接口的任何調用都轉發到目标類。

(2)生成子類調用,spring使用CGLIB庫生成目标類的一個子類,在建立這個子類的時候,spring織入通知,并且把對這個子類的調用委托到目标類。

AspectJ和Spring AOP的差別和選擇

兩者的聯系和差別

AspectJ和Spring AOP都是對目标類增強,生成代理類。

AspectJ是在編譯期間将切面代碼編譯到目标代碼的,屬于靜态代理;Spring AOP是在運作期間通過代理生成目标類,屬于動态代理。

AspectJ是靜态代理,故而能夠切入final修飾的類,abstract修飾的類;Spring AOP是動态代理,其實作原理是通過CGLIB生成一個繼承了目标類(委托類)的代理類,是以,final修飾的類不能被代理,同樣static和final修飾的方法也不會代理,因為static和final方法是不能被覆寫的。在CGLIB底層,其實是借助了ASM這個非常強大的Java位元組碼生成架構。關于CGLB和ASM的讨論将會新開一個篇幅探讨。

Spring AOP支援注解,在使用@Aspect注解建立和配置切面時将更加友善。而使用AspectJ,需要通過.aj檔案來建立切面,并且需要使用ajc(Aspect編譯器)來編譯代碼。

選擇對比

首先需要考慮,Spring AOP緻力于提供一種能夠與Spring IoC緊密內建的面向切面架構的實作,以便于解決在開發企業級項目時面臨的常見問題。明确你在應用橫切關注點(cross-cutting concern)時(例如事物管理、日志或性能評估),需要處理的是Spring beans還是POJO。如果正在開發新的應用,則選擇Spring AOP就沒有什麼阻力。但是如果你正在維護一個現有的應用(該應用并沒有使用Spring架構),AspectJ就将是一個自然的選擇了。為了詳細說明這一點,假如你正在使用Spring AOP,當你想将日志功能作為一個通知(advice)加入到你的應用中,用于追蹤程式流程,那麼該通知(Advice)就隻能應用在Spring beans的連接配接點(Joinpoint)之上。

另一個需要考慮的因素是,你是希望在編譯期間進行織入(weaving),還是編譯後(post-compile)或是運作時(run-time)。Spring隻支援運作時織入。如果你有多個團隊分别開發多個使用Spring編寫的子產品(導緻生成多個jar檔案,例如每個子產品一個jar檔案),并且其中一個團隊想要在整個項目中的所有Spring bean(例如,包括已經被其他團隊打包了的jar檔案)上應用日志通知(在這裡日志隻是用于加入橫切關注點的舉例),那麼通過配置該團隊自己的Spring配置檔案就可以輕松做到這一點。之是以可以這樣做,就是因為Spring使用的是運作時織入。

還有一點,因為Spring基于代理模式(使用CGLIB),它有一個使用限制,即無法在使用final修飾的bean上應用橫切關注點。因為代理需要對Java類進行繼承,一旦使用了關鍵字final,這将是無法做到的。在這種情況下,你也許會考慮使用AspectJ,其支援編譯期織入且不需要生成代理。于此相似,在static和final方法上應用橫切關注點也是無法做到的。因為Spring基于代理模式。如果你在這些方法上配置通知,将導緻運作時異常,因為static和final方法是不能被覆寫的。在這種情況下,你也會考慮使用AspectJ,因為其支援編譯期織入且不需要生成代理。

如果你希望使用一種易于實作的方式,就選擇Spring AOP吧,因為Spring AOP支援注解,在使用@Aspect注解建立和配置切面時将更加友善。而使用AspectJ,你就需要通過.aj檔案來建立切面,并且需要使用ajc(Aspect編譯器)來編譯代碼。是以如果你确定之前提到的限制不會成為你的項目的障礙時,使用Spring AOP。AspectJ的一個間接局限是,因為AspectJ通知可以應用于POJO之上,它有可能将通知應用于一個已配置的通知之上。對于一個你沒有注意到這切面問題的大範圍應用的通知,這有可能導緻一個無限循環。在下面這種情況下,當proceed即将被調用時,日志通知會被再次應用,這樣就導緻了嵌套循環。

public aspectLogging {
  Object around() : execution(public * * (..))
  Sysytem.out.println(thisJoinPoint.getSignature());
  return proceed();
}           

參考文章

誠摯感謝以下文章及作者,也是讓我在參考實踐以及理論總結的過程中學習到了很多東西。不做無頭無腦的抄襲者,要做閱讀他人的文章,汲取精粹,親自實踐得出結論。尊重原創,尊重作者!

AspectJ(一) 一些該了解的概念 AspectJ 架構,比用 spring 實作 AOP 好用很多喲! 比較分析 Spring AOP 和 AspectJ 之間的差别 AspectJ基本用法 應用Spring AOP(一) AspectJ官方doc文檔 Spring AOP,AspectJ, CGLIB 有點暈

該文首發

《虛懷若谷》

個人部落格,轉載前請務必署名,轉載請标明出處。

古之善為道者,微妙玄通,深不可識。夫唯不可識,故強為之容:

豫兮若冬涉川,猶兮若畏四鄰,俨兮其若客,渙兮若冰之釋,敦兮其若樸,曠兮其若谷,混兮其若濁。

孰能濁以靜之徐清?孰能安以動之徐生?

保此道不欲盈。夫唯不盈,故能敝而新成。

請關注我的微信公衆号:下雨就像彈鋼琴,Thanks(・ω・)ノ