面向切面程式設計 ( Aspect Oriented Programming with Spring )

不要亂摸發表於2018-05-29

Aspect Oriented Programming with Spring

1. 簡介

AOP是與OOP不同的一種程式結構。在OOP程式設計中,模組的單位是class(類);然而,在AOP程式設計中模組的單位是aspect(切面)。也就是說,OOP關注的是類,而AOP關注的是切面。

Spring AOP是用純Java實現的。目前,只支援方法執行級別的連線點。

Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.
Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. CGLIB is used by default if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes; business classes normally will implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface, or where you need to pass a proxied object to a method as a concrete type.

對於AOP代理,Spring AOP預設使用JDK動態代理。這意味著任意介面都可以被代理。

Spring AOP也可以用CGLIB代理。CGLIB代理的是類,而不是介面。如果一個業務物件沒有實現一個介面,那麼預設用CGLIB代理。這是一種很好的實踐,面向介面程式設計,而不是面向類;業務類通常會實現一個或者多個業務介面。可以強制使用CGLIB代理,這種情況下你需要通知一個方法而不是一個介面,你需要傳遞一個代理物件而不是一個具體的型別給一個方法。

2. @AspectJ支援

@AspectJ是一種宣告切面的方式(或者說風格),它用註解來標註標準的Java類。

2.1. 啟用@AspectJ支援

autoproxying(自動代理)意味著如果Spring檢測到一個Bean被一個或者多個切面通知,那麼它將自動為這個Bean生成一個代理以攔截其上的方法呼叫,並且確保通知被執行。

可以使用XML或者Java方式來配置以支援@AspectJ。為此,你需要aspectjweaver.jar

啟用@AspectJ用Java配置的方式

為了使@AspectJ生效,需要用@Configuration@EnableAspectJAutoProxy註解

1 @Configuration
2 @EnableAspectJAutoProxy
3 public class AppConfig {
4 
5 }

啟用@AspectJ用XML配置的方式

為了使@AspectJ生效,需要用到aop:aspectj-autoproxy元素

1 <aop:aspectj-autoproxy/>

2.2. 宣告一個切面

下面的例子顯示了定義一個最小的切面:

首先,定義一個標準的Java Bean

1 <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
2     <!-- configure properties of aspect here as normal -->
3 </bean>

其次,用org.aspectj.lang.annotation.Aspect註解標註它

1 package org.xyz;
2 import org.aspectj.lang.annotation.Aspect;
3 
4 @Aspect
5 public class NotVeryUsefulAspect {
6 
7 }

一個切面(PS:被@Aspect註解標註的類)可以向其它的類一樣有方法和欄位。這些方法可能包含切點、通知等等。

通過元件掃描的方式自動偵測切面

你可能在XML配置檔案中註冊一個標準的Bean作為切面,或者通過classpath掃描的方式自動偵測它,就像其它被Spring管理起來的Bean那樣。為了能夠在classpath下自動偵測,你需要在在切面上加@Component註解。

 

In Spring AOP, it is not possible to have aspects themselves be the target of advice from other aspects. The @Aspect annotation on a class marks it as an aspect, and hence excludes it from auto-proxying.

Spring AOP中,不可能有切面自己本身還被其它的切面作為目標通知。用@Aspect註解標註一個類作為切面,因此需要將它自己本身從自動代理中排除。

什麼意思呢?舉個例子,比如

package com.cjs.aspect

@Aspect

@Component

public class LogAspect {

  @Pointcut("execution(* com.cjs..*(..))")

  public void pointcut() {}

}

在這個例子中,切面LogAspect所在的位置是com.cjs.aspect,而它的切入點是com.cjs下的所有的包的所類的所有方法,這其中就包含LogAspect,這是不對的,會造成迴圈依賴。在SpringBoot中這樣寫的話啟動的時候就會報錯,會告訴你檢測到迴圈依賴。

2.3. 宣告一個切入點

切入點是用來控制什麼時候執行通知的,簡單地來講,就是什麼樣的方法會被攔截。Spring AOP目前只支援方法級別的連線點。

一個切入點宣告由兩部分組成:第一部分、由一個名稱和任意引數組成的一個簽名;第二部分、一個切入點表示式。

@AspectJ註解方式的AOP中,一個切入點簽名就是一個標準的方法定義,而切入點表示式則是由@Pointcut註解來指明的。

(作為切入點簽名的方法的返回值型別必須是void)

下面是一個簡單的例子:

1 @Pointcut("execution(* transfer(..))")// the pointcut expression
2 private void anyOldTransfer() {}// the pointcut signature

支援的切入點識別符號

  • execution  -  主要的切入點識別符號
  • within  -  匹配給定的型別
  • target  -  匹配給定型別的例項
  • args  -  匹配例項的引數型別
  • @args  -  匹配引數被特定註解標記的
  • @target  -  匹配有特定註解的類
  • @within  -  匹配用指定的註解型別標註的類下的方法
  • @annotation  -  匹配帶有指定的註解的方法

對於JDK代理,只有public的介面方法呼叫的時候才會被攔截。對於CGLIBpublicprotected的方法呼叫將會被攔截。

組合切入點表示式

Pointcut expressions can be combined using '&&', '||' and '!'

請看下面的例子:

1 @Pointcut("execution(public * *(..))")
2 private void anyPublicOperation() {}
3 
4 @Pointcut("within(com.xyz.someapp.trading..*)")
5 private void inTrading() {}
6 
7 @Pointcut("anyPublicOperation() && inTrading()")
8 private void tradingOperation() {}

在企業應用開發過程中,你可能想要經常對一下模組應用一系列特殊的操作。我們推薦你定義一個“系統架構”層面的切面用來捕獲公共的切入點。下面是一個例子:

 1 package com.xyz.someapp;
 2 
 3 import org.aspectj.lang.annotation.Aspect;
 4 import org.aspectj.lang.annotation.Pointcut;
 5 
 6 @Aspect
 7 public class SystemArchitecture {
 8 
 9     /**
10      * 匹配定義在com.xyz.someapp.web包或者子包下的方法
11      */
12     @Pointcut("within(com.xyz.someapp.web..*)")
13     public void inWebLayer() {}
14 
15     /**
16      * 匹配定義在com.xyz.someapp.service包或者子包下的方法
17      */
18     @Pointcut("within(com.xyz.someapp.service..*)")
19     public void inServiceLayer() {}
20 
21     /**
22      * 匹配定義在com.xyz.someapp.dao包或者子包下的方法
23      */
24     @Pointcut("within(com.xyz.someapp.dao..*)")
25     public void inDataAccessLayer() {}
26 
27     /**
28      * 匹配定義在com.xyz.someapp下任意層級的service包下的任意類的任意方法
29      */
30     @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
31     public void businessService() {}
32 
33     /**
34      * 匹配定義在com.xyz.someapp.dao包下的所有類的所有方法
35      */
36     @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
37     public void dataAccessOperation() {}
38 
39 }

execution表示式

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

萬用字元*表示任意字元,(..)表示任意引數

下面是一些例子

  • 任意public方法

execution(public * *(..))

  • 以set開頭的任意方法

execution(* set*(..))

  • AccountService中的任意方法

execution(* com.xyz.service.AccountService.*(..))

  • service包下的任意方法

execution(* com.xyz.service.*.*(..))

  • service包或者子包下的任意方法

execution(* com.xyz.service..*.*(..))

  • service包下的任意連線點

within(com.xyz.service.*)

  • service包或者子包下的任意連線點

within(com.xyz.service..*)

  • 實現了AccountService介面的代理類中的任意連線點

this(com.xyz.service.AccountService)

  • 只有一個引數且引數型別是Serializable的連線點

args(java.io.Serializable)

  • 有@Transactional註解的目標物件上的任意連線點

@target(org.springframework.transaction.annotation.Transactional)

  • 宣告型別上有@Transactional註解的目標物件上的任意連線點

@within(org.springframework.transaction.annotation.Transactional)

  • 執行方法上有@Transactional註解的任意連線點

@annotation(org.springframework.transaction.annotation.Transactional)

  • 只有一個引數,且執行時傳的引數上有@Classified註解的任意連線點

@args(com.xyz.security.Classified)

  • 名字叫tradeService的bean

bean(tradeService)

  • 名字以Service結尾的bean

bean(*Service)

2.4. 宣告通知

前置通知

 1 import org.aspectj.lang.annotation.Aspect;
 2 import org.aspectj.lang.annotation.Before;
 3 
 4 @Aspect
 5 public class BeforeExample {
 6 
 7     @Before("execution(* com.xyz.myapp.dao.*.*(..))")
 8     public void doAccessCheck() {
 9         // ...
10     }
11 
12 }

返回通知

 1 import org.aspectj.lang.annotation.Aspect;
 2 import org.aspectj.lang.annotation.AfterReturning;
 3 
 4 @Aspect
 5 public class AfterReturningExample {
 6 
 7     @AfterReturning(
 8         pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
 9         returning="retVal")
10     public void doAccessCheck(Object retVal) {
11         // ...
12     }
13 
14 }

異常通知

 1 import org.aspectj.lang.annotation.Aspect;
 2 import org.aspectj.lang.annotation.AfterThrowing;
 3 
 4 @Aspect
 5 public class AfterThrowingExample {
 6 
 7     @AfterThrowing(
 8         pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
 9         throwing="ex")
10     public void doRecoveryActions(DataAccessException ex) {
11         // ...
12     }
13 
14 }

後置通知(最終通知)

 1 import org.aspectj.lang.annotation.Aspect;
 2 import org.aspectj.lang.annotation.After;
 3 
 4 @Aspect
 5 public class AfterFinallyExample {
 6 
 7     @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 8     public void doReleaseLock() {
 9         // ...
10     }
11 
12 }

環繞通知

環繞通知用@Around註解來宣告,第一個引數必須是ProceedingJoinPoint型別的。在通知內部,呼叫ProceedingJoinPointproceed()方法造成方法執行。

 1 import org.aspectj.lang.annotation.Aspect;
 2 import org.aspectj.lang.annotation.Around;
 3 import org.aspectj.lang.ProceedingJoinPoint;
 4 
 5 @Aspect
 6 public class AroundExample {
 7 
 8     @Around("com.xyz.myapp.SystemArchitecture.businessService()")
 9     public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
10         // start stopwatch
11         Object retVal = pjp.proceed();
12         // stop stopwatch
13         return retVal;
14     }
15 
16 }

如果使用者明確指定了引數名稱,那麼這個指定的名稱可以用在通知中,通過argNames引數來指定:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

3. 代理機制

Spring AOP用JDK動態代理或者CGLIB來為給定的目標物件建立代理。(無論你怎麼選擇,JDK動態代理都是首選)

如果目標物件實現了至少一個介面,那麼JDK動態代理將會被使用。

目標物件實現的所有介面都會被代理。

如果目標物件沒有實現任何介面,那麼使用CGLIB代理。

如果你強制用CGLIB代理,那麼下面這些問題你需要注意:

  • final方法不能被通知,因為它們無法被覆蓋
  • Spring 3.2中不需要再引入CGLIB,因為它已經包含在org.springframework中了
  • Spring 4.0代理類的構造方法不能被呼叫兩次以上

為了強制使用CGLIB代理,需要在<aop:config>中的proxy-target-class屬性設定為true

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

當使用@AspectJ自動代理的時候強制使用CGLIB代理,需要將<aop:aspectj-autoproxy>proxy-target-class屬性設定為true

<aop:aspectj-autoproxy proxy-target-class="true"/>

3.1. 理解AOP代理

Spring AOP是基於代理的。這一點極其重要。

考慮下面的程式碼片段

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果你呼叫一個物件中的一個方法,並且是這個物件直接呼叫這個方法,那麼下圖所示。

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

如果引用物件有一個代理,那麼事情變得不一樣了。請考慮下面的程式碼片段。

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

上面的程式碼中,為引用物件生成了一個代理。這就意味著在引用物件上的方法呼叫會傳到代理上的呼叫。

有一點需要注意,呼叫同一個類中的方法時,被呼叫的那個方法不會被代理。也就是說呼叫foo()的時候是攔截不到bar()的。

Example

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

 

1     @Pointcut("@within(com.cjs.log.annotation.SystemControllerLog) " +
2             "|| @within(com.cjs.log.annotation.SystemRpcLog) " +
3             "|| @within(com.cjs.log.annotation.SystemServiceLog)")
4     public void pointcut() {
5 
6     }

 

相關文章