在 OOP 中,模块化的关键单元是 ,而在 AOP 中,模块化的单元是 切面 。切面支持跨多种类型和对象的关注点(例如事务管理)的模块化。

5.1 AOP概念

  • Aspect: 切面。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。
  • Join Point: 连接点。程序执行期间的一个点,如方法的执行或异常的处理。在 Spring Aop 中,连接点始终表示方法执行。
  • Advice: 通知。在特定的连接点采取行动。包含 around、before、after
  • Pointcut: 切入点。
  • Introduction: 代表类型声明其他方法或字段
  • Target object: 切面对象、通知对象。Spring AOP是通过运行时动态代理实现。
  • AOP proxy: 在 Spring 框架中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
  • Weaving: 编织。将切面与其他应用程序类型或对象连接起来,以创建建议的对象。这可以在编译时(例如使用 AspectJ 编译器)、加载时或运行时完成。 Spring AOP与其他纯 Java AOP 框架一样,在运行时执行编织。

包含以下通知:

  • Before advice
  • After returning advice:
  • After throwing advice: 通知在方法退出时通过抛出异常来运行。
  • After (finally) advice: 后(最后)通知: 无论连接点通过何种方式退出(正常或异常返回) ,通知都要运行。
  • Around advice: 最强大的通知。在方法调用之前和之后执行自定义行为。它还负责选择是否继承到连接点(Join Point)或者通过返回自己的返回值或抛出异常的方法缩短被通知方法的执行。

使用最具体的通知类型可以提供更简单的编程模型,减少出错的可能性。
切入点匹配的连接点的概念是AOP的关键,它区别于只提供拦截的旧技术。切入点使通知成为独立于面向对象层次结构的目标。

5.2 AOP的能力和目标

Springaop 目前只支持方法执行连接点(建议 springbean 上方法的执行)。虽然可以在不破坏核心 springaop api 的情况下添加对字段拦截的支持,但是没有实现字段拦截。如果需要建议字段访问和更新连接点,可以考虑使用 AspectJ 之类的语言。

5.3 AOP代理

Springaop 默认使用标准 JDK 动态代理作为 AOP 代理。这使得任何接口(或一组接口)都可以被代理。

5.4 @AspectJ支持

@AspectJ 指的是一种将方面声明为带注释的普通 Java 类的风格。AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或织布器。

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

5.4.2. 声明一个切面

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

5.4.3. 声明切入点

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

Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示器(PCD) :

  • execution: 用于匹配方法执行连接点。这是主要切入点指示器。
  • within: 限制在某些类型中匹配连接点。
  • this: 限制对连接点的匹配,其中 bean 引用(springaop 代理)是给定类型的实例。
  • target: 限制对连接点的匹配,其中目标对象(被代理的应用程序对象)是给定类型的实例。
  • args: 对连接点(使用 springaop 时方法的执行)进行匹配的限制,其中的参数是给定类型的实例。
  • @target: 限制匹配连接点(使用 Spring AOP 时方法的执行) ,其中执行对象的类具有给定类型的注解。
  • @args: 对连接点(使用 Spring AOP 时方法的执行)进行匹配的限制,其中传递的实际参数的运行时类型具有给定类型的注解。
  • @within: 限制在具有给定注释的类型中匹配连接点(使用 Spring AOP 时使用带有给定注释的类型声明的方法的执行)。
  • @annotation: 限制匹配连接点的主题(在 Spring AOP 中运行的方法)具有给定注解的连接点。

组合切入点表达式

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

最佳实践是从较小的命名组件构建更复杂的切入点表达式,如前所示。当通过名称引用切入点时,应用普通的 Java 可见性规则(您可以在相同的类型中看到私有切入点、层次结构中的受保护切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。

共享公共切入点定义

  1. package com.xyz.myapp;
  2. import org.aspectj.lang.annotation.Aspect;
  3. import org.aspectj.lang.annotation.Pointcut;
  4. @Aspect
  5. public class CommonPointcuts {
  6. /**
  7. * A join point is in the web layer if the method is defined
  8. * in a type in the com.xyz.myapp.web package or any sub-package
  9. * under that.
  10. */
  11. @Pointcut("within(com.xyz.myapp.web..*)")
  12. public void inWebLayer() {}
  13. /**
  14. * A join point is in the service layer if the method is defined
  15. * in a type in the com.xyz.myapp.service package or any sub-package
  16. * under that.
  17. */
  18. @Pointcut("within(com.xyz.myapp.service..*)")
  19. public void inServiceLayer() {}
  20. /**
  21. * A join point is in the data access layer if the method is defined
  22. * in a type in the com.xyz.myapp.dao package or any sub-package
  23. * under that.
  24. */
  25. @Pointcut("within(com.xyz.myapp.dao..*)")
  26. public void inDataAccessLayer() {}
  27. /**
  28. * A business service is the execution of any method defined on a service
  29. * interface. This definition assumes that interfaces are placed in the
  30. * "service" package, and that implementation types are in sub-packages.
  31. *
  32. * If you group service interfaces by functional area (for example,
  33. * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
  34. * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
  35. * could be used instead.
  36. *
  37. * Alternatively, you can write the expression using the 'bean'
  38. * PCD, like so "bean(*Service)". (This assumes that you have
  39. * named your Spring service beans in a consistent fashion.)
  40. */
  41. @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
  42. public void businessService() {}
  43. /**
  44. * A data access operation is the execution of any method defined on a
  45. * dao interface. This definition assumes that interfaces are placed in the
  46. * "dao" package, and that implementation types are in sub-packages.
  47. */
  48. @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
  49. public void dataAccessOperation() {}
  50. }

Spring AOP用户可能最经常使用执行切入点指示器。执行表达式的格式如下:

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

下面是一些常见的例子

  • The execution of any public method:

    execution(public * *(..))
    
  • The execution of any method with a name that begins with set:

    execution(* set*(..))
    
  • The execution of any method defined by the AccountService interface:

    execution(* com.xyz.service.AccountService.*(..))
    
  • The execution of any method defined in the service package:
    execution(* com.xyz.service.*.*(..))
    
  • The execution of any method defined in the service package or one of its sub-packages:

    execution(* com.xyz.service..*.*(..))
    
  • Any join point (method execution only in Spring AOP) within the service package:

    within(com.xyz.service.*)
    
  • Any join point (method execution only in Spring AOP) within the service package or one of its sub-packages:
    within(com.xyz.service..*)
    
  • Any join point (method execution only in Spring AOP) where the proxy implements the AccountService interface:

    this(com.xyz.service.AccountService)
    
  • Any join point (method execution only in Spring AOP) where the target object implements the AccountService interface:

    target(com.xyz.service.AccountService)
    
  • Any join point (method execution only in Spring AOP) that takes a single parameter and where the argument passed at runtime is Serializable:

    args(java.io.Serializable)
    
  • Any join point (method execution only in Spring AOP) where the target object has a @Transactional annotation:

    @target(org.springframework.transaction.annotation.Transactional)
    
  • Any join point (method execution only in Spring AOP) where the declared type of the target object has an @Transactional annotation:

    @within(org.springframework.transaction.annotation.Transactional)
    
  • Any join point (method execution only in Spring AOP) where the executing method has an @Transactional annotation:

    @annotation(org.springframework.transaction.annotation.Transactional)
    
  • Any join point (method execution only in Spring AOP) which takes a single parameter, and where the runtime type of the argument passed has the @Classified annotation:

    @args(com.xyz.security.Classified)
    
  • Any join point (method execution only in Spring AOP) on a Spring bean named tradeService:

    bean(tradeService)
    
  • Any join point (method execution only in Spring AOP) on Spring beans having names that match the wildcard expression *Service:

    bean(*Service)
    

    小结:

  1. 修饰符(可选)-返回值类型(必有)-方法参数声明类型(可选)-方法名称类型(必有 (参数类型)(必有) )-异常类型(可选)
  2. * 常用作返回类型模式,匹配任何返回类型
  3. (..) 匹配任意数量的参数(0~N)
  4. a.b.c..* 表示匹配a.b.c包及其子包下的任何方法

    写出好的切入点

    在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中放置一个测试,以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。
    然而,AspectJ 只能处理被告知的内容。为了获得最佳匹配性能,您应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示词自然分为三类: kinded、 scoping 和 contextual:
  • Kinded designators select a particular kind of join point: execution, get, set, call, and handler.
  • Scoping designators select a group of join points of interest (probably of many kinds): within and withincode
  • Contextual designators match (and optionally bind) based on context: this, target, and @annotation

编写良好的切入点至少应该包括前两种类型(kinded 和 scoping)。可以根据连接点上下文包含要匹配的上下文指示器,或者绑定该上下文以便在通知中使用。作用域指示器的匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地删除不应进一步处理的连接点组。一个好的切入点应该尽可能包括一个切入点。

@Before

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

@AfterReturn

当一个方法正常执行完后执行返回通知。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

@AfterThrowing

当匹配的方法执行抛出异常退出时执行异常通知。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {
    // 绑定异常参数
    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

@After

当匹配的方法执行退出时,运行 After (finally)通知。它通常用于释放资源和类似的用途。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}

@Around

如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如启动和停止计时器) ,那么常常使用 Around advice。通知方法的第一个参数必须是 ProceedingJoinPoint 类型。在通知的主体中,调用 procededjoinpoint 上的 proceed ()将导致基础方法运行。Proceed 方法也可以在 Object []中传递。当方法继续执行时,数组中的值用作方法执行的参数。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

JoinPoint
  • getArgs(): Returns the method arguments.
  • getThis(): Returns the proxy object.
  • getTarget(): Returns the target object.
  • getSignature(): Returns a description of the method that is being advised.。
  • toString(): Prints a useful description of the method being advised.

    将参数传递给通知

    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
    public void validateAccount(Account account) {
      // ...
    }
    

    泛型参数

    这种方式不适用于集合。

    public interface Sample<T> {
      void sampleGenericMethod(T param);
      void sampleGenericCollectionMethod(Collection<T> param);
    }
    
    @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
    public void beforeSampleMethod(MyType param) {
      // Advice implementation
    }
    

    确定参数名

    通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过 Java 反射不能使用参数名,因此 Spring AOP 使用以下策略来确定参数名:

  • 如果用户显式指定了参数名称,则使用指定的参数名称。通知和切入点注释都有一个可选的 argNames 属性,您可以使用该属性指定带注释的方法的参数名。这些参数名在运行时可用。下面的示例演示如何使用 argernames 属性:

    @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
    }
    

    5.5 基于XML配置的AOP支持

    文档略。

    5.6 选择哪种AOP声明方式

    Spring AOP更简单易用。

    5.8 代理机制

    如果被代理的目标对象实现了至少一个接口,则使用 JDK 动态代理。所有由目标类型实现的接口都被代理。如果目标对象不实现任何接口,则创建一个 CGLIB 代理。
    如果您想强制使用 CGLIB 代理,需要考虑如下几点:

  • 对于 CGLIB,不能通知 final 方法,因为它们不能在运行时生成的子类中被重写。

  • Spring 4.0中,代理对象的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。只有在 JVM 不允许构造函数绕过的情况下,您才可能会看到 Spring 的 AOP 支持中的双调用和相应的调试日志条目。

强制使用

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

5.8.1. 理解 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...
    }
}

如果你在一个对象引用上调用一个方法,那么这个方法会直接在这个对象引用上被调用,如下面的图片和清单所示:
SpringDoc阅读之Chapter5-使用Spring进行面向切面编程 - 图1
当客户端代码的引用是代理时,情况稍有变化:
SpringDoc阅读之Chapter5-使用Spring进行面向切面编程 - 图2

public class Main {

    public static void main(String[] args) {
        // 代理工厂对Pojo进行代理
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        // 从工厂中获取代理对象
        Pojo pojo = (Pojo) factory.getProxy();
        // 执行方法
        pojo.foo();
    }
}

这里需要理解的关键是 main(..) 方法有一个对代理的引用。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(本例中为 SimplePojo 引用) ,它可能对自身进行的任何方法调用,如this.bar()this.foo(),都将针对此引用而不是代理进行调用。这具有重要的意义。这意味着自调用不会导致与方法调用相关联的通知有机会运行。

5.9 以编程方式创建@AspectJ代理

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

5.10 在Spring应用程序中使用AspectJ

5.11 Further Resources