1. 程式人生 > >Spring AOP切點表示式詳解

Spring AOP切點表示式詳解

1. 簡介

       面向物件程式設計,也稱為OOP(即Object Oriented Programming)最大的優點在於能夠將業務模組進行封裝,從而達到功能複用的目的。通過面向物件程式設計,不同的模板可以相互組裝,從而實現更為複雜的業務模組,其結構形式可用下圖表示:

業務模組

        面向物件程式設計解決了業務模組的封裝複用的問題,但是對於某些模組,其本身並不獨屬於摸個業務模組,而是根據不同的情況,貫穿於某幾個或全部的模組之間的。例如登入驗證,其只開放幾個可以不用登入的介面給使用者使用(一般登入使用攔截器實現,但是其切面思想是一致的);再比如效能統計,其需要記錄每個業務模組的呼叫,並且監控器呼叫時間。可以看到,這些橫貫於每個業務模組的模組,如果使用面向物件的方式,那麼就需要在已封裝的每個模組中新增相應的重複程式碼,對於這種情況,面向切面程式設計就可以派上用場了。

       面向切面程式設計,也稱為AOP(即Aspect Oriented Programming),指的是將一定的切面邏輯按照一定的方式編織到指定的業務模組中,從而將這些業務模組的呼叫包裹起來。如下是其結構示意圖:

切面程式設計模組

2. AOP的各個扮演者

2.1 AOP的主要角色

  • 切面:使用切點表示式表示,指定了當前切面邏輯所要包裹的業務模組的範圍大小;
  • Advice:也即切面邏輯,指定了當前用於包裹切面指定的業務模組的邏輯。

2.2 Advice的主要型別

  • @Before:該註解標註的方法在業務模組程式碼執行之前執行,其不能阻止業務模組的執行,除非丟擲異常;
  • @AfterReturning:該註解標註的方法在業務模組程式碼執行之後執行;
  • @AfterThrowing:該註解標註的方法在業務模組丟擲指定異常後執行;
  • @After:該註解標註的方法在所有的Advice執行完成後執行,無論業務模組是否丟擲異常,類似於finally的作用;
  • @Around:該註解功能最為強大,其所標註的方法用於編寫包裹業務模組執行的程式碼,其可以傳入一個ProceedingJoinPoint用於呼叫業務模組的程式碼,無論是呼叫前邏輯還是呼叫後邏輯,都可以在該方法中編寫,甚至其可以根據一定的條件而阻斷業務模組的呼叫;
  • @DeclareParents:其是一種Introduction型別的模型,在屬性宣告上使用,主要用於為指定的業務模組新增新的介面和相應的實現。
  • @Aspect:嚴格來說,其不屬於一種Advice,該註解主要用在類宣告上,指明當前類是一個組織了切面邏輯的類,並且該註解中可以指定當前類是何種例項化方式,主要有三種:singleton、perthis和pertarget,具體的使用方式後面會進行講解。

        這裡需要說明的是,@Before是業務邏輯執行前執行,與其對應的是@AfterReturning,而不是@After,@After是所有的切面邏輯執行完之後才會執行,無論是否丟擲異常。

3. 切點表示式

3.1 execution

       由於Spring切面粒度最小是達到方法級別,而execution表示式可以用於明確指定方法返回型別,類名,方法名和引數名等與方法相關的部件,並且在Spring中,大部分需要使用AOP的業務場景也只需要達到方法級別即可,因而execution表示式的使用是最為廣泛的。如下是execution表示式的語法:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

       這裡問號表示當前項可以有也可以沒有,其中各項的語義如下:

  • modifiers-pattern:方法的可見性,如public,protected;
  • ret-type-pattern:方法的返回值型別,如int,void等;
  • declaring-type-pattern:方法所在類的全路徑名,如com.spring.Aspect;
  • name-pattern:方法名型別,如buisinessService();
  • param-pattern:方法的引數型別,如java.lang.String;
  • throws-pattern:方法丟擲的異常型別,如java.lang.Exception;

        如下是一個使用execution表示式的例子:

execution(public * com.spring.service.BusinessObject.businessService(java.lang.String,..))

       上述切點表示式將會匹配使用public修飾,返回值為任意型別,並且是com.spring.BusinessObject類中名稱為businessService的方法,方法可以有多個引數,但是第一個引數必須是java.lang.String型別的方法。上述示例中我們使用了..萬用字元,關於萬用字元的型別,主要有兩種:

  • *萬用字元,該萬用字元主要用於匹配單個單詞,或者是以某個詞為字首或字尾的單詞。

       如下示例表示返回值為任意型別,在com.spring.service.BusinessObject類中,並且引數個數為零的方法:

execution(* com.spring.service.BusinessObject.*())

       下述示例表示返回值為任意型別,在com.spring.service包中,以Business為字首的類,並且是類中引數個數為零方法:

execution(* com.spring.service.Business*.*())
  • ..萬用字元,該萬用字元表示0個或多個項,主要用於declaring-type-pattern和param-pattern中,如果用於declaring-type-pattern中,則表示匹配當前包及其子包,如果用於param-pattern中,則表示匹配0個或多個引數。

       如下示例表示匹配返回值為任意型別,並且是com.spring.service包及其子包下的任意類的名稱為businessService的方法,而且該方法不能有任何引數:

execution(* com.spring.service..*.businessService())

       這裡需要說明的是,包路徑service..*.businessService()中的..應該理解為延續前面的service路徑,表示到service路徑為止,或者繼續延續service路徑,從而包括其子包路徑;後面的*.businessService(),這裡的*表示匹配一個單詞,因為是在方法名前,因而表示匹配任意的類。

       如下示例是使用..表示任意個數的引數的示例,需要注意,表示引數的時候可以在括號中事先指定某些型別的引數,而其餘的引數則由..進行匹配:

execution(* com.spring.service.BusinessObject.businessService(java.lang.String,..))

3.2 within

       within表示式的粒度為類,其引數為全路徑的類名(可使用萬用字元),表示匹配當前表示式的所有類都將被當前方法環繞。如下是within表示式的語法:

within(declaring-type-pattern)

       within表示式只能指定到類級別,如下示例表示匹配com.spring.service.BusinessObject中的所有方法:

within(com.spring.service.BusinessObject)

       within表示式路徑和類名都可以使用萬用字元進行匹配,比如如下表達式將匹配com.spring.service包下的所有類,不包括子包中的類:

within(com.spring.service.*)

       如下表達式表示匹配com.spring.service包及子包下的所有類:

within(com.spring.service..*)

3.3 args

       args表示式的作用是匹配指定引數型別和指定引數數量的方法,無論其類路徑或者是方法名是什麼。這裡需要注意的是,args指定的引數必須是全路徑的。如下是args表示式的語法:

args(param-pattern)

       如下示例表示匹配所有隻有一個引數,並且引數型別是java.lang.String型別的方法:

args(java.lang.String)

       也可以使用萬用字元,但這裡萬用字元只能使用..,而不能使用*。如下是使用萬用字元的例項,該切點表示式將匹配第一個引數為java.lang.String,最後一個引數為java.lang.Integer,並且中間可以有任意個數和型別引數的方法:

args(java.lang.String,..,java.lang.Integer)

3.4 this和target

       this和target需要放在一起進行講解,主要目的是對其進行區別。this和target表示式中都只能指定類或者介面,在面向切面程式設計規範中,this表示匹配呼叫當前切點表示式所指代物件方法的物件,target表示匹配切點表示式指定型別的物件。比如有兩個類A和B,並且A呼叫了B的某個方法,如果切點表示式為this(B),那麼A的例項將會被匹配,也即其會被使用當前切點表示式的Advice環繞;如果這裡切點表示式為target(B),那麼B的例項也即被匹配,其將會被使用當前切點表示式的Advice環繞。

       在講解Spring中的this和target的使用之前,首先需要講解一個概念:業務物件(目標物件)和代理物件。對於切面程式設計,有一個目標物件,也有一個代理物件,目標物件是我們宣告的業務邏輯物件,而代理物件是使用切面邏輯對業務邏輯進行包裹之後生成的物件。如果使用的是Jdk動態代理,那麼業務物件和代理物件將是兩個物件,在呼叫代理物件邏輯時,其切面邏輯中會呼叫目標物件的邏輯;如果使用的是Cglib代理,由於是使用的子類進行切面邏輯織入的,那麼只有一個物件,即織入了代理邏輯的業務類的子類物件,此時是不會生成業務類的物件的。

       在Spring中,其對this的語義進行了改寫,即如果當前物件生成的代理物件符合this指定的型別,那麼就為其織入切面邏輯。簡單的說就是,this將匹配代理物件為指定型別的類。target的語義則沒有發生變化,即其將匹配業務物件為指定型別的類。如下是使用this和target表示式的簡單示例:

this(com.spring.service.BusinessObject)
target(com.spring.service.BusinessObject)

       通過上面的講解可以看出,this和target的使用區別其實不大,大部分情況下其使用效果是一樣的,但其區別也還是有的。Spring使用的代理方式主要有兩種:Jdk代理和Cglib代理(關於這兩種代理方式的講解可以檢視本人的文章代理模式實現方式及優缺點對比)。針對這兩種代理型別,關於目標物件與代理物件,理解如下兩點是非常重要的:

  • 如果目標物件被代理的方法是其實現的某個介面的方法,那麼將會使用Jdk代理生成代理物件,此時代理物件和目標物件是兩個物件,並且都實現了該介面;
  • 如果目標物件是一個類,並且其沒有實現任意介面,那麼將會使用Cglib代理生成代理物件,並且只會生成一個物件,即Cglib生成的代理類的物件。

       結合上述兩點說明,這裡理解this和target的異同就相對比較簡單了。我們這裡分三種情況進行說明:

  • this(SomeInterface)或target(SomeInterface):這種情況下,無論是對於Jdk代理還是Cglib代理,其目標物件和代理物件都是實現SomeInterface介面的(Cglib生成的目標物件的子類也是實現了SomeInterface介面的),因而this和target語義都是符合的,此時這兩個表示式的效果一樣;
  • this(SomeObject)或target(SomeObject),這裡SomeObject沒實現任何介面:這種情況下,Spring會使用Cglib代理生成SomeObject的代理類物件,由於代理類是SomeObject的子類,子類的物件也是符合SomeObject型別的,因而this將會被匹配,而對於target,由於目標物件本身就是SomeObject型別,因而這兩個表示式的效果一樣;
  • this(SomeObject)或target(SomeObject),這裡SomeObject實現了某個介面:對於這種情況,雖然表示式中指定的是一種具體的物件型別,但由於其實現了某個介面,因而Spring預設會使用Jdk代理為其生成代理物件,Jdk代理生成的代理物件與目標物件實現的是同一個介面,但代理物件與目標物件還是不同的物件,由於代理物件不是SomeObject型別的,因而此時是不符合this語義的,而由於目標物件就是SomeObject型別,因而target語義是符合的,此時this和target的效果就產生了區別;這裡如果強制Spring使用Cglib代理,因而生成的代理物件都是SomeObject子類的物件,其是SomeObject型別的,因而this和target的語義都符合,其效果就是一致的。

       關於this和target的異同,我們使用如下示例進行簡單演示:

// 目標類
public class Apple {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 切面類
@Aspect
public class MyAspect {
  @Around("this(com.business.Apple)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}
<!-- bean宣告檔案 -->
<bean id="apple" class="chapter7.eg1.Apple"/>
<bean id="aspect" class="chapter7.eg6.MyAspect"/>
<aop:aspectj-autoproxy/>
// 驅動類
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Apple fruit = (Apple) context.getBean("apple");
    fruit.eat();
  }
}

       執行驅動類中的main方法,結果如下:

this is before around advice
Apple.eat method invoked.
this is after around advice

       上述示例中,Apple沒有實現任何介面,因而使用的是Cglib代理,this表示式會匹配Apple物件。這裡將切點表示式更改為target,還是執行上述程式碼,會發現結果還是一樣的:

target(com.business.Apple)

       如果我們對Apple的宣告進行修改,使其實現一個介面,那麼這裡就會顯示出this和target的執行區別了:

public class Apple implements IApple {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Fruit fruit = (Fruit) context.getBean("apple");
    fruit.eat();
  }
}

       我們還是執行上述程式碼,對於this表示式,其執行結果如下:

Apple.eat method invoked.

       對於target表示式,其執行結果如下:

this is before around advice
Apple.eat method invoked.
this is after around advice

       可以看到,這種情況下this和target表示式的執行結果是不一樣的,這正好符合我們前面講解的第三種情況。

3.5 @within

       前面我們講解了within的語義表示匹配指定型別的類例項,這裡的@within表示匹配帶有指定註解的類,其使用語法如下所示:

@within(annotation-type)

       如下所示示例表示匹配使用com.spring.annotation.BusinessAspect註解標註的類:

@within(com.spring.annotation.BusinessAspect)

       這裡我們使用一個例子演示@within的用法(這裡驅動類和xml檔案配置與3.4節使用的一致,這裡省略):

// 註解類
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FruitAspect {
}
// 目標類
@FruitAspect
public class Apple {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 切面類
@Aspect
public class MyAspect {
  @Around("@within(com.business.annotation.FruitAspect)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}

       上述切面表示匹配使用FruitAspect註解的類,而Apple則使用了該註解,因而Apple類方法的呼叫會被切面環繞,執行執行驅動類可得到如下結果,說明Apple.eat()方法確實被環繞了:

this is before around advice
Apple.eat method invoked.
this is after around advice

3.6 @annotation

       @annotation的使用方式與@within的相似,表示匹配使用@annotation指定註解標註的方法將會被環繞,其使用語法如下:

@annotation(annotation-type)

       如下示例表示匹配使用com.spring.annotation.BusinessAspect註解標註的方法:

@annotation(com.spring.annotation.BusinessAspect)

       這裡我們繼續複用3.5節使用的例子進行講解@annotation的用法,只是這裡需要對Apple和MyAspect使用和指定註解的方式進行修改,FruitAspect不用修改的原因是宣告該註解時已經指定了其可以使用在類,方法和引數上:

// 目標類,將FruitAspect移到了方法上
public class Apple {
  @FruitAspect
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
@Aspect
public class MyAspect {
  @Around("@annotation(com.business.annotation.FruitAspect)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}

       這裡Apple.eat()方法使用FruitAspect註解進行了標註,因而該方法的執行會被切面環繞,其執行結果如下:

this is before around advice
Apple.eat method invoked.
this is after around advice

3.7 @args

       @within和@annotation分別表示匹配使用指定註解標註的類和標註的方法將會被匹配,@args則表示使用指定註解標註的類作為某個方法的引數時該方法將會被匹配。如下是@args註解的語法:

@args(annotation-type)

       如下示例表示匹配使用了com.spring.annotation.FruitAspect註解標註的類作為引數的方法:

@args(com.spring.annotation.FruitAspect)

       這裡我們使用如下示例對@args的用法進行講解:

<!-- xml配置檔案 -->
<bean id="bucket" class="chapter7.eg1.FruitBucket"/>
<bean id="aspect" class="chapter7.eg6.MyAspect"/>
<aop:aspectj-autoproxy/>
// 使用註解標註的引數類
@FruitAspect
public class Apple {}
// 使用Apple引數的目標類
public class FruitBucket {
  public void putIntoBucket(Apple apple) {
    System.out.println("put apple into bucket.");
  }
}
@Aspect
public class MyAspect {
  @Around("@args(chapter7.eg6.FruitAspect)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}
// 驅動類
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    FruitBucket bucket = (FruitBucket) context.getBean("bucket");
    bucket.putIntoBucket(new Apple());
  }
}

       這裡FruitBucket.putIntoBucket(Apple)方法的引數Apple使用了@args註解指定的FruitAspect進行了標註,因而該方法的呼叫將會被環繞。執行驅動類,結果如下:

this is before around advice
put apple into bucket.
this is after around advice

3.8 @DeclareParents

       @DeclareParents也稱為Introduction(引入),表示為指定的目標類引入新的屬性和方法。關於@DeclareParents的原理其實比較好理解,因為無論是Jdk代理還是Cglib代理,想要引入新的方法,只需要通過一定的方式將新宣告的方法織入到代理類中即可,因為代理類都是新生成的類,因而織入過程也比較方便。如下是@DeclareParents的使用語法:

@DeclareParents(value = "TargetType", defaultImpl = WeaverType.class)
private WeaverInterface attribute;

       這裡TargetType表示要織入的目標型別(帶全路徑),WeaverInterface中聲明瞭要新增的方法,WeaverType中聲明瞭要織入的方法的具體實現。如下示例表示在Apple類中織入IDescriber介面宣告的方法:

@DeclareParents(value = "com.spring.service.Apple", defaultImpl = DescriberImpl.class)
private IDescriber describer;

       這裡我們使用一個如下例項對@DeclareParents的使用方式進行講解,配置檔案與3.4節的一致,這裡略:

// 織入方法的目標類
public class Apple {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 要織入的介面
public interface IDescriber {
  void desc();
}
// 要織入介面的預設實現
public class DescriberImpl implements IDescriber {
  @Override
  public void desc() {
    System.out.println("this is an introduction describer.");
  }
}
// 切面例項
@Aspect
public class MyAspect {
  @DeclareParents(value = "com.spring.service.Apple", defaultImpl = DescriberImpl.class)
  private IDescriber describer;
}
// 驅動類
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    IDescriber describer = (IDescriber) context.getBean("apple");
    describer.desc();
  }
}

       在MyAspect中聲明瞭我們需要將IDescriber的方法織入到Apple例項中,在驅動類中我們可以看到,我們獲取的是apple例項,但是得到的bean卻可以強轉為IDescriber型別,因而說明我們的織入操作成功了。

3.9 perthis和pertarget

       在Spring AOP中,切面類的例項只有一個,比如前面我們一直使用的MyAspect類,假設我們使用的切面類需要具有某種狀態,以適用某些特殊情況的使用,比如多執行緒環境,此時單例的切面類就不符合我們的要求了。在Spring AOP中,切面類預設都是單例的,但其還支援另外兩種多例的切面例項的切面,即perthis和pertarget,需要注意的是perthis和pertarget都是使用在切面類的@Aspect註解中的。這裡perthis和pertarget表示式中都是指定一個切面表示式,其語義與前面講解的this和target非常的相似,perthis表示如果某個類的代理類符合其指定的切面表示式,那麼就會為每個符合條件的目標類都宣告一個切面例項;pertarget表示如果某個目標類符合其指定的切面表示式,那麼就會為每個符合條件的類宣告一個切面例項。從上面的語義可以看出,perthis和pertarget的含義是非常相似的。如下是perthis和pertarget的使用語法:

perthis(pointcut-expression)
pertarget(pointcut-expression)

       由於perthis和pertarget的使用效果大部分情況下都是一致的,我們這裡主要講解perthis和pertarget的區別。關於perthis和pertarget的使用,需要注意的一個點是,由於perthis和pertarget都是為每個符合條件的類宣告一個切面例項,因而切面類在配置檔案中的宣告上一定要加上prototype,否則Spring啟動是會報錯的。如下是我們使用的示例:

<!-- xml配置檔案 -->
<bean id="apple" class="chapter7.eg1.Apple"/>
<bean id="aspect" class="chapter7.eg6.MyAspect" scope="prototype"/>
<aop:aspectj-autoproxy/>
// 目標類實現的介面
public interface Fruit {
  void eat();
}
// 業務類
public class Apple implements Fruit {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 切面類
@Aspect("perthis(this(com.spring.service.Apple))")
public class MyAspect {

  public MyAspect() {
    System.out.println("create MyAspect instance, address: " + toString());
  }

  @Around("this(com.spring.service.Apple)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}
// 驅動類
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Fruit fruit = context.getBean(Fruit.class);
    fruit.eat();
  }
}

       這裡我們使用的切面表示式語法為perthis(this(com.spring.service.Apple)),這裡this表示匹配代理類是Apple型別的類,perthis則表示會為這些類的每個例項都建立一個切面類。由於Apple實現了Fruit介面,因而Spring使用Jdk動態代理為其生成代理類,也就是說代理類與Apple都實現了Fruit介面,但是代理類不是Apple型別,因而這裡宣告的切面不會匹配到Apple類。執行上述驅動類,結果如下:

Apple.eat method invoked.

       結果表明Apple類確實沒有被環繞。如果我們講切面類中的perthis和this修改為pertarget和target,效果如何呢:

@Aspect("pertarget(target(com.spring.service.Apple))")
public class MyAspect {

  public MyAspect() {
    System.out.println("create MyAspect instance, address: " + toString());
  }

  @Around("target(com.spring.service.Apple)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}

       執行結果如下:

create MyAspect instance, address: [email protected]
this is before around advice
Apple.eat method invoked.
this is after around advice

       可以看到,Apple類被切面環繞了。這裡target表示目標類是Apple型別,雖然Spring使用了Jdk動態代理實現切面的環繞,代理類雖不是Apple型別,但是目標類卻是Apple型別,符合target的語義,而pertarget會為每個符合條件的表示式的類例項建立一個代理類例項,因而這裡Apple會被環繞。

       由於代理類與目標類的差別非常小,因而與this和target一樣,perthis和pertarget的區別也非常小,大部分情況下其使用效果是一致的。關於切面多例項的建立,其演示比較簡單,我們可以將xml檔案中的Apple例項修改為prototype型別,並且在驅動類中多次獲取Apple類的例項:

<!-- xml配置檔案 -->
<bean id="apple" class="chapter7.eg1.Apple" scope="prototype"/>
<bean id="aspect" class="chapter7.eg6.MyAspect" scope="prototype"/>
<aop:aspectj-autoproxy/>
public class AspectApp {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Fruit fruit = context.getBean(Fruit.class);
    fruit.eat();
    fruit = context.getBean(Fruit.class);
    fruit.eat();
  }
}

       執行結果如下:

create MyAspect instance, address: [email protected]
this is before around advice
Apple.eat method invoked.
this is after around advice
create MyAspect instance, address: [email protected]
this is before around advice
Apple.eat method invoked.
this is after around advice

       執行結果中兩次列印的create MyAspect instance表示當前切面例項建立了兩次,這也符合我們進行的兩次獲取Apple例項。

4. 小結

       本文首先對AOP進行了簡單介紹,然後介紹了切面中的各個角色,最後詳細介紹了切點表示式中各個不同型別表示式的語法。

from: https://my.oschina.net/zhangxufeng/blog/1824275