面向切面編程 ( Aspect Oriented Programming with Spring )

Aspect Oriented Programming with Springjava

1. 簡介

AOP是與OOP不一樣的一種程序結構。在OOP編程中,模塊的單位是class(類);然而,在AOP編程中模塊的單位是aspect(切面)。也就是說,OOP關注的是類,而AOP關注的是切面。web

Spring AOP是用純Java實現的。目前,只支持方法執行級別的鏈接點。spring

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.express

對於AOP代理,Spring AOP默認使用JDK動態代理。這意味着任意接口均可以被代理。編程

Spring AOP也能夠用CGLIB代理。CGLIB代理的是類,而不是接口。若是一個業務對象沒有實現一個接口,那麼默認用CGLIB代理。這是一種很好的實踐,面向接口編程,而不是面向類;業務類一般會實現一個或者多個業務接口。能夠強制使用CGLIB代理,這種狀況下你須要通知一個方法而不是一個接口,你須要傳遞一個代理對象而不是一個具體的類型給一個方法。數組

2. @AspectJ支持

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

2.1. 啓用@AspectJ支持

autoproxying(自動代理)意味着若是Spring檢測到一個Bean被一個或者多個切面通知,那麼它將自動爲這個Bean生成一個代理以攔截其上的方法調用,而且確保通知被執行。app

可使用XML或者Java方式來配置以支持@AspectJ。爲此,你須要aspectjweaver.jarthis

啓用@AspectJ用Java配置的方式

爲了使@AspectJ生效,須要用@Configuration@EnableAspectJAutoProxy註解spa

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     }
相關文章
相關標籤/搜索