淺談AOP以及AspectJ和Spring AOP

導言

AOP(Aspect Orient Programming),做爲面向對象編程的一種補充,普遍應用於處理一些具備橫切性質的系統級服務,如日誌收集、事務管理、安全檢查、緩存、對象池管理等。AOP實現的關鍵就在於AOP框架自動建立的AOP代理,AOP代理則可分爲靜態代理和動態代理兩大類,其中靜態代理是指使用AOP框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,所以也稱爲編譯時加強;而動態代理則在運行時藉助於JDK動態代理CGLIB等在內存中「臨時」生成AOP動態代理類,所以也被稱爲運行時加強。php

面向切面的編程(AOP) 是一種編程範式,旨在經過容許橫切關注點的分離,提升模塊化。AOP提供切面來將跨越對象關注點模塊化。雖然如今能夠得到許多AOP框架,但在這裏咱們要區分的只有兩個流行的框架:Spring AOP和AspectJ。html


關鍵概念

Aspect

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

JoinPoint

JoinPoint(鏈接點)是AOP中的一個重要的關鍵概念。JoinPoint能夠看作是程序運行時的一個執行點。打個比方,好比執行System.out.println("Hello")這個函數,println()就是一個joinpoint;再如給一個變量賦值也是一個joinpoint;還有最經常使用的for循環,也是一個joinpoint。ios

理論上說,一個程序中不少地方均可以被看作是JoinPoint,可是AspectJ中,只有下面所示的幾種執行點被認爲是JoinPoint:spring

<center>表1 JoinPoint的類型</center>express

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。apache

PointCut

PointCut通俗地翻譯爲切入點,一個程序會有多個Join Point,即便同一個函數,也還分爲call和execution類型的Join Point,但並非全部的Join Point都是咱們關心的,Pointcut就是提供一種使得開發者可以選擇本身須要的JoinPoint的方法。PointCut分爲callexecutiontargetthiswithin等關鍵字。與joinPoint相比,pointcut就是一個具體的切點。編程

Advice

Advice翻譯爲通知或者加強(Advisor),就是咱們插入的代碼以何種方式插入,至關於OOP中的方法,有Before、After以及Around。vim

  • Before

前置通知用於將切面代碼插入方法以前,也就是說,在方法執行以前,會首先執行前置通知裏的代碼.包含前置通知代碼的類就是切面。api

  • 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: + 放在類名後,表示當前類及其子類,放在接口後,表示當前接口及其實現類。

<center>表2 方法表達式</center>

表達式 含義
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

<center>表3 參數表達式</center>

參數 含義
() 表示方法沒有任何參數
(..) 表示匹配接受任意個參數的方法
(..,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/aspect...,在下載頁面選擇合適的版本下載,目前最新穩定版是1.9.1。下載完以後雙加jar包安裝,安裝界面以下:
AspectJ安裝界面

安裝目錄用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、aj五、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.classcom/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以及9一、94的地方,會發現invokestaticcom/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.11org.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♪(・ω・)ノ
微信二維碼

相關文章
相關標籤/搜索