1. 程式人生 > >Spring AOP切點表達式用法總結

Spring AOP切點表達式用法總結

情況 cut 需要 xml文件 表示 標註 blog 封裝 HR

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: chapter7.eg6.MyAspect@48fa0f47
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: chapter7.eg6.MyAspect@48fa0f47
this is before around advice
Apple.eat method invoked.
this is after around advice
create MyAspect instance, address: chapter7.eg6.MyAspect@56528192
this is before around advice
Apple.eat method invoked.
this is after around advice

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

4. 小結

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

Spring AOP切點表達式用法總結