不要乱摸 阅读(24) 评论(0)

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(){}
}