65.9K
CodeProject 正在变化。 阅读更多。
Home

构建 Spring 2 企业应用程序:第 4 章:Spring AOP 2.0

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2008 年 5 月 19 日

CPOL

65分钟阅读

viewsIcon

26846

开源的 Spring 框架已成为构建企业 Java 应用程序的流行应用程序框架。本章探讨 Spring AOP(面向切面编程)的最新功能,包括 @AspectJ 风格的注解、AspectJ 切点语言、Spring AOP XML 标签等。

标题 构建 Spring 2 企业应用程序
作者 Interface21, Bram Smeets, Seth Ladd
出版社 Apress
出版日期 2007 年 8 月
ISBN 978-1-59059-918-1
页数 335

欢迎来到关于 Spring AOP 未来的章节。在第 3 章中,我们描述了 Spring AOP 在 Spring 1.2.x 版本中的使用情况。本章介绍 2.0 版本中添加到 Spring AOP 的功能。

没错,*添加*了新功能,所以您到目前为止学到的所有 AOP 知识仍然适用且可用。这确实证明了 Spring 2.0 版本与 Spring 1.2.x 保持了完全的向后兼容性。我们强烈建议您将 Spring 版本升级到最新的 2.0 版本。完全向后兼容性得到了保证。

如果您还没有这样做,现在是回顾第 3 章所涵盖的概念的好时机,因为它们仍然是 Spring AOP 的基础。

以下是本章将详细介绍的新功能

  • 使用 Java 5 注解编写 AspectJ 风格的方面,包括支持的通知类型
  • AspectJ 切点语言
  • Spring AOP XML 标签,用于在 XML 中声明方面,以防 Java 5 不可用或必须使用现有类作为通知
  • Spring AOP XML advisor 标签,用于将经典的 Spring AOP 通知类与 AspectJ 切点语言结合起来

介绍 AspectJ 和方面

虽然经典的 Spring AOP(在第 3 章中介绍)与通知、切点和 advisor 一起工作,但新的 Spring AOP 与通知、切点、advisor 和 *方面*一起工作。您可能认为这差别不大,但很快您就会发现,情况已发生重大变化。字面上所有新的 Spring AOP 功能都构建在与 AspectJ AOP 框架集成之上。(代理拦截机制仍然存在,因此您从上一章获得的技能仍然有用。)那么,什么是 AspectJ?AspectJ FAQ (http://www.eclipse.org/aspectj/doc/released/faq.html) 如此回答这个问题:
AspectJ 是对 Java 编程语言的一个简单而实用的扩展,它为 Java 添加了面向切面的编程 (AOP) 功能。AOP 允许开发人员在模块化的自然单元之外,通过模块化来处理横切关注点。在像 Java 这样的面向对象程序中,模块化的自然单元是类。在 AspectJ 中,方面将影响多个类的关注点进行模块化。
那么,什么是方面?同样,相同的 FAQ 如此回答:

方面是开发人员封装跨越类(Java 中模块化的自然单元)的关注点的方式。
从上一章,您知道横切关注点被模块化为通知。这些被 advisor *封装*,advisor 将一个通知和一个切点结合起来。这种封装告诉您在软件的哪个连接点执行通知。

方面和 advisor 似乎有很多共同之处:它们都封装了跨越类的关注点。通知在由切点匹配的连接点处执行;然而,给定的切点可能不会匹配应用程序中的任何连接点。

现在让我们看看您可以使用方面做什么

  • 您可以声明切点。
  • 您可以为与关联切点匹配的每个连接点声明错误和警告。
  • 您可以在类中声明新的字段、构造函数和方法。这些在 AspectJ 中称为跨类型声明。
  • 您可以声明一个或多个通知,每个通知都为由切点匹配的所有连接点执行。

比较两者,很快就会发现方面是一个比 advisor 更复杂的构造。目前,理解方面和 advisor 都封装了横切关注点但采取了不同的方法就足够了。

AspectJ 中的连接点和切点

AspectJ 支持比 Spring AOP 更多的连接点类型,Spring AOP 只支持方法执行。以下是 AspectJ 支持的连接点选择:
  • 方法调用以及实例和静态方法的执行
  • 实例字段和静态字段的 get 和 set 值调用
  • 构造函数调用和构造函数的执行
  • 类和包
这些额外的连接点在 Spring AOP 中都不支持。但是,在讨论切点时,了解 AspectJ 支持哪些连接点是有用的。

为了选择支持的丰富连接点集,AspectJ 有自己的切点语言。以下切点选择名为 relax 的所有静态和实例方法,无论它们的参数、返回类型或类如何

execution(* relax(..))
当您考虑 AspectJ 支持的所有连接点类型时,适当的语言是定义切点的唯一灵活方式。任何其他方式,包括 XML 配置或 API,都将是一个噩梦般的编写、阅读和维护。

Spring AOP 集成了这个 AspectJ 切点语言,它将在本章后面“使用切点”部分介绍。现在,您只需要知道星号 (*) 匹配任何方法或类名或任何参数类型,双点 (..) 匹配零个或多个参数。

AspectJ 方面创建

AspectJ 有自己的语言,它扩展了 Java 语言规范来创建方面。

最初,这是使用 AspectJ 声明方面的唯一方法。由于方面和切点被视为一等公民,它是一种非常实用的 AOP 语言。Spring AOP 不集成到此语言中,但为了让您更好地理解 AspectJ 方面,这里有一个非常简单的示例:

package com.apress.springbook.chapter04.aspects; 

public aspect MySimpleAspectJAspect { 
  before(): execution(* relax(..)) { 
    System.out.println("relax() method is about to be executed!"); 
  } 
} 
如您所见,该方面与 Java 类有些相似,但您无法使用常规 Java 编译器进行编译。

AspectJ 1.5 引入了 Java 5 注解,允许程序员编写 AspectJ 方面作为 AspectJ 语言的替代方案。(如果您不熟悉 Java 5 注解,可以在 http://www.developer.com/java/other/article.php/3556176 找到介绍。)Spring AOP 集成了这种编写方面的方式,如本章所述。清单 4-1 显示了上一个方面使用注解重写后的样子。这种风格被称为 @AspectJ 风格,尽管使用了 @Aspect 注解。如您所见,方面变成了常规 Java 类。

清单 4-1. 一个用 @AspectJ 风格编写的简单 AspectJ 方面

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class MySimpleAtAspectJAspect { 

  @Before("execution(* relax(..))") 
  public void beforeRelaxingMethod() { 
      System.out.println("relax() method is about to be executed!"); 
  } 
} 
清单 4-1 中的方面声明了一个用 @AspectJ 风格的 Java 5 注解注解的常规 Java 类。该类声明了一个切点/通知对。类声明上的 @Aspect 注解表示该类是 @AspectJ 风格的方面。类需要此注解才能作为方面。

@Before 注解用于将常规的 beforeRelaxingMethod() 方法转换为通知声明,并包含该通知的切点声明。在 AspectJ 中,没有切点的通知无法存在。

注解类型还定义了通知类型;在这种情况下,它是前置通知。@AspectJ 风格支持第 3 章中定义的通知类型,外加一种。只有带有 @AspectJ 通知类型注解的实例方法才是通知声明,因此方面类也可以有常规方法。

@AspectJ 注解*可以*用于抽象类甚至接口,尽管这不是很实用,因为注解不会被继承。

清单 4-2 显示了一个具有一个方法(该方法将是清单 4-1 中切点匹配的连接点之一)的类。

清单 4-2. SunnyDay 类中的 relax() 方法被选为连接点

package com.apress.springbook.chapter04; 

public class SunnyDay { 
  public void relax() { 
      // go to the beach 
  } 
} 
relax() 方法执行之前,控制台会打印一条消息。打印语句是实际执行的通知。@AspectJ 风格要求 Java 5。此外,不能使用不声明 @AspectJ 注解的现有类作为通知。

在典型的 Spring 风格中,您可以在不使用 Java 5 和注解的情况下声明 Spring AOP 中的方面。通过巧妙地利用 Spring 2.0 XML 架构支持(在第 2 章中介绍),Spring 开发人员能够定义 AOP 标签来声明方面、通知和切点。还有一个新标签用于声明 advisor。本章将在详细介绍 @AspectJ 风格的方面声明和切点语言之后介绍这些新 XML 标签。

现在,闲话少说,Spring 2.0 AOP 来了。


注意

您可以在 http://www.eclipse.org/aspectj/ 找到关于 AspectJ 的更多信息。另一个极好的资源是 Ramnivas Laddad 撰写的《AspectJ in Action》(Manning,2003)。


配置 Spring 中的 @AspectJ 风格方面

现在,您知道方面是什么样的以及如何自己编写一个。在本节中,我们将从一个在 Spring 容器中配置的 @AspectJ 风格方面的示例开始。

这将演示 Spring AOP 框架如何使用方面并创建代理对象。示例之后,我们将详细介绍通知类型、切点和代理对象。

一个简单的 @AspectJ 风格方面

@AspectJ 风格的方面必须在 Spring 容器中配置才能被 Spring AOP 使用。从上一章,您会记得代理对象是通过在 Spring 容器中使用 ProxyFactoryBean 创建的。在这种情况下,我们通过每个目标对象的配置迈出了 AOP 的第一步来创建代理对象。使用 @AspectJ 风格的方面,Spring AOP 采用不同的方法来基于方面的切点创建代理对象,正如本示例所示。在此示例中,我们将使用一个简单的切点,以便我们可以专注于方面。随着本章的进展,我们将使用更复杂的切点示例。

方面定义

此示例的方面显示在清单 4-3 中。它有一个切点,用于选择所有它能找到的 startMatch() 方法,以及一个在发生这种情况时向控制台打印消息的通知。在接下来的部分中,我们将更详细地介绍如何搜索连接点以及发现匹配项时会发生什么。

清单 4-3. 具有选择所有 startMatch() 方法的切点以及在连接点执行之前打印消息的通知

package com.apress.springbook.chapter04.aspects; 

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

@Aspect 
public class MessagePrintingAspect { 

  @Before("execution(* startMatch(..))") 
  public void printMessageToInformMatchStarts() { 
      System.out.println("Attempting to start tennis match!"); 
  } 
}

注意

您需要在类路径中包含 aspectjweaver.jaraspectjrt.jar。这两个文件可以在 Spring 框架发行版下的 lib/aspectj 目录中找到。


清单 4-3 中的 MessagePrintingAspect 是一个常规 Java 类,带有 Java 5 注解。由于 @AspectJ 风格的注解,它也是一个方面声明。

类声明上的 @Aspect 注解将该类转换为方面声明。

它现在可以包含切点声明和通知/切点组合。该方面称为 MessagePrintingAspect,表明其职责是向控制台打印消息。当我们想要为其他连接点打印消息时,我们可以向该方面添加更多通知/切点组合。通过将逻辑上属于一起的通知组织(或模块化)到方面中,可以轻松地了解为哪些连接点打印了哪些消息。

@Before 注解在 printMessageToInformMatchStarts() 方法声明上起两个作用:它定义了通知类型(前置通知),并且它包含切点声明。再次,我们选择了一个名称 printMessageToInformMatchStarts,它解释了通知的职责。


提示

为通知提供描述性名称有助于整理思路和组织通知。如果您在为通知命名时遇到困难,无法准确描述其功能,那么也许它们承担了过多的职责,应该拆分成更小的部分。

切点声明选择所有名为 startMatch() 的实例方法,无论参数数量、参数类型、异常声明、返回类型、可见性或声明它们的类如何。现在您已经理解了方面声明,是时候看看此示例的目标类了。

目标类

本示例中的目标类是我们熟悉的朋友 DefaultTournamentMatchManager,如清单 4-4 所示。

清单 4-4. DefaultTournamentMatchManager 类

package com.apress.springbook.chapter04; 

public class DefaultTournamentMatchManager implements TournamentMatchManager { 
  public Match startMatch(long matchId) throws 
    UnknownMatchException, MatchIsFinishedException, 
    MatchCannotBePlayedException, PreviousMatchesNotFinishedException { 
  // implementation omitted 
  } 

  /* other methods omitted */ 
} 
startMatch() 方法符合清单 4-3 中切点的标准。但是,这并不意味着 Spring AOP 会立即开始创建代理对象。首先,我们必须在 Spring 容器中配置一个目标对象和 @AspectJ 风格的方面,如下一节所述。

方面配置

清单 4-5 显示了 Spring XML 配置文件中必要的配置,以便 printMessageToInformMatchStarts 通知在 startMatch() 方法执行之前向控制台打印一条消息(还有另一种方法可以做到这一点,我们将在本章后面的“使用 AOP XML 标签”部分进行探讨)。

清单 4-5. aspect-config.xml:Spring XML 文件中的必需配置

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd"> 
<beans> 
  <bean class="org.springframework.aop.aspectj.annotation.  --> 
AnnotationAwareAspectJAutoProxyCreator"/> 

  <bean class="com.apress.springbook.chapter04.aspects.MessagePrintingAspect"/> 

  <bean id="tournamentMatchManager" 
    class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
    <!-- properties omitted --> 
  </bean> 
</beans> 
Spring AOP 提供了与 Spring 容器强大的集成,称为*自动代理*创建。Spring AOP 将扩展 Spring 容器的 bean 生命周期,为容器中具有切点匹配的连接点的 bean 创建代理对象。

在下一节中,我们将深入研究代理是如何创建的。目前,理解清单 4-5 中 AnnotationAwareAspectJAutoProxyCreator bean 定义的对象将在 Spring 容器(ApplicationContext)加载时首先创建。完成此操作后,Spring 容器会检测任何带有 @Aspect 注解的类,并使用它们来配置 Spring AOP。

AnnotationAwareAspectJAutoProxyCreator bean 有可能影响容器创建的所有其他 bean。在 tournamentMatchManager bean 的 bean 生命周期过程中,AnnotationAwareAspectJAutoProxyCreator 将为该 bean 创建一个代理对象,并用代理对象替换原始 bean,因为它的一个连接点(startMatch() 方法)与 MessagePrintingAspect 中的通知/切点组合匹配。

当 tournamentMatchManager bean 上的 startMatch() 方法执行时,将调用 printMessageToInformMatchStarts 通知。现在,让我们看看 printMessageToInform MatchStarts 通知是否真的被调用并在 startMatch() 方法执行之前打印消息。

配置和方面的集成测试

我们现在可以使用一个简单的集成测试来验证当 tournamentMatchManager bean 上的 startMatch() 方法被调用时消息是否被打印到控制台。我们还将添加一个创建新的 DefaultTournamentMatchManager 对象并调用其 startMatch() 方法的测试,以验证调用此方法时*没有*打印消息。清单 4-6 显示了集成测试用例。

清单 4-6. Spring AOP 配置和方面的集成测试用例

package com.apress.springbook.chapter04; 

import org.springframework.test.AbstractDependencyInjectionSpringContextTests; 

public class MessagePrintingAspectIntegrationTests extends 
    AbstractDependencyInjectionSpringContextTests { 
  
  protected String[] getConfigLocations() { 
    return new String[] { 
     "classpath:com/apress/springbook/chapter04/" + 
     "aspect-config.xml" 
    }; 
  }
  
  private TournamentMatchManager tournamentMatchManager; 
  
  public void setTournamentMatchManager( 
          TournamentMatchManager tournamentMatchManager) { 
    this.tournamentMatchManager = tournamentMatchManager; 
  } 

  public void testCallStartMatchMethodOnBeanFromContainer() 
      throws Exception { 
    System.out.println("=== GOING TO CALL METHOD " + 
          "ON BEAN FROM CONTAINER ==="); 
          
  this.tournamentMatchManager.startMatch(1); 
  
  System.out.println("=== FINISHED CALLING METHOD " + 
        "ON BEAN FROM CONTAINER ===");     
  } 
  
  public void testCallStartMatchMethodOnNewlyCreatedObject() 
      throws Exception { 
    TournamentMatchManager newTournamentMatchManager = 
        new DefaultTournamentMatchManager(); 
        
  System.out.println("=== GOING TO CALL METHOD " + 
          "ON NEWLY CREATED OBJECT ==="); 
          
  newTournamentMatchManager.startMatch(1); 
      System.out.println("=== FINISHED CALLING METHOD " + 
          "ON NEWLY CREATED OBJECT ==="); 
    } 
  } 
清单 4-6 中的测试用例加载 Spring XML 配置文件(清单 4-5)。它声明了两个测试:

testCallStartMatchMethodOnBeanFromContainer():此测试使用从容器注入的 tournamentMatchManager 对象。这是 Spring XML 配置文件中定义的 tournamentMatchManager bean。该测试调用此对象上的 startMatch() 方法。tournamentMatchManager bean 是一个由 AnnotationAware AspectJAutoProxyCreator bean 创建的代理对象。创建代理对象是因为 MessagePrintingAspect 中唯一的切点匹配 startMatch() 连接点。当在代理对象上执行 startMatch() 方法时,将执行打印其消息到控制台的 printMessageToInformMatchStarts 通知,然后执行目标对象的实际方法。

testCallStartMatchMethodOnNewlyCreatedObject():此测试创建一个新的 DefaultTournament MatchManager 对象。此对象不是代理,不受 Spring AOP 的任何影响。调用其 startMatch() 方法时,不会执行任何通知。由于此对象不是由 Spring 容器创建的,因此不受 MessagePrintingAspect 的影响。

当清单 4-6 中的测试用例执行时,控制台将打印如下消息:

=== GOING TO CALL METHOD ON BEAN FROM CONTAINER === 
Attempting to start tennis match! 
=== FINISHED CALLING METHOD ON BEAN FROM CONTAINER === 
=== GOING TO CALL METHOD ON NEWLY CREATED OBJECT === 
=== FINISHED CALLING METHOD ON NEWLY CREATED OBJECT === 

当 tournamentMatchManager bean 上的 startMatch() 连接点执行时,MessagePrintingAspect 中声明的 printMessageToInformMatchStarts() 通知方法被执行。

我们的示例涉及 Spring AOP 处理方面的许多方面。您已经了解了将 @AspectJ 风格方面与 Spring AOP 结合使用的所有必需条件:

  • 连接点需要是对象上的 public 或 protected 实例方法。
  • 对象必须由 Spring 容器创建。
  • 调用者需要调用代理对象上的方法,而不是原始对象上的方法。
  • 方面实例也必须由 Spring 容器创建。
  • 必须由 Spring 容器创建一个特殊的 bean 来处理自动代理创建。

现在,让我们看看 Spring AOP 中方面支持的通知类型。

@AspectJ 风格的通知类型

Spring AOP 中的方面不是通过接口声明的,这与经典的 Spring AOP 不同。相反,通知被声明为常规 Java 方法,它可以带有参数、返回对象和抛出异常。如您在上一个示例中所见,通知类型由方法上的 @Aspect 注解声明定义。以下通知类型是支持的:

前置通知 (@Before):在连接点执行之前执行。它与上一章描述的前置通知具有相同的语义。它只能通过抛出异常来阻止对目标对象的拦截执行。

后置返回通知 (@AfterReturning):在连接点执行完毕且未抛出异常后执行。它与上一章描述的后置返回通知具有相同的语义。如果需要,它可以访问方法执行的返回值,但不能替换返回值。

后置异常通知 (@AfterThrowing):在执行抛出异常的连接点后执行。它与上一章描述的 throws 通知具有相同的语义。如果需要,它可以访问抛出的异常,但除非它抛出另一个异常,否则不能阻止此异常被抛出给调用者。

后置(finally)通知 (@After):无论连接点执行是否抛出异常,总是在连接点执行后调用。这是一种新的通知类型,在经典的 Spring AOP 中不可用。它无法访问返回值或抛出的异常。

环绕通知 (@Around):作为拦截器在连接点执行周围执行。与上一章描述的环绕通知一样,它是最强大的通知类型,但也是最需要工作的类型。


注意

实际上,Spring 2.0 AOP 框架支持第六种通知类型:引入通知。我们不会在本书中讨论这种通知类型,因为它不经常使用。您可以记住它可用,并且可用于向被通知类添加方法和属性。


您在前几个示例中看到了前置通知的示例;MessagePrintingAspect 包含前置通知。让我们快速看一下其他通知类型以及如何在 @AspectJ 风格的方面中声明它们。

后置返回通知

当连接点执行完毕并以返回值或无返回值(如果返回类型为 void)退出时,会调用后置返回通知。清单 4-7 显示了带有后置返回通知的 MessagePrintingAspect。

清单 4-7. 在连接点正常执行后打印消息

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class MessagePrintingAspect { 
  @AfterReturning("execution(* startMatch(..))") 
  public void printMessageWhenTennisMatchHasBeenStartedSuccessfully() { 
    System.out.println("Tennis match was started successfully!"); 
  } 
} 
后置异常通知

如果您想在连接点抛出异常时执行一些操作,可以使用后置异常通知。清单 4-8 显示了带有后置异常通知的 MessagePrintingAspect,当抛出异常时会打印警告。

清单 4-8. 在连接点抛出异常后打印警告消息

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class MessagePrintingAspect { 
  @AfterThrowing("execution(* startMatch(..))") 
  public void printMessageWhenSomethingGoesWrong() { 
    System.out.println("Oops, couldn't start the tennis match. " + 
        "Something went wrong!"); 
  } 
}

后置(finally)通知

后置(finally)通知总是在连接点执行后执行,但它无法获取返回值或抛出的任何异常。换句话说,此通知类型无法确定连接点执行的结果。它通常用于清理资源,例如清理可能仍附加到当前线程的对象。

清单 4-9 显示了带有后置(finally)通知的 MessagePrintingAspect,它打印一条消息以结束网球比赛的开始事件。

清单 4-9. 尝试开始网球比赛时打印消息

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class MessagePrintingAspect { 
  @After("execution(* startMatch(..))")   
  public void printMessageToConcludeTheTennisMatchStartAttempt() { 
    System.out.println("A tennis match start attempt has taken place. " + 
      "We haven't been informed about the outcome but we sincerely " + 
      "hope everything worked out OK and wish you very nice day!"); 
  } 
}

环绕通知

环绕通知是最复杂的类型,因为它没有专门为任何特定任务设计。相反,它基于拦截模型,允许您完全控制连接点的执行。

它的语义与上一章讨论的 MethodInterceptor 相同。与 MethodInterceptor 一样,此通知需要能够继续进行当前的方法执行。为此,每个环绕通知方法*必须*将 ProceedingJoinPoint 声明为其第一个参数,如清单 4-10 所示。

清单 4-10. 传递问候,然后继续前进

package com.apress.springbook.chapter04.aspects; 

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

import org.aspectj.lang.ProceedingJoinPoint; 


@Aspect 
public class MessagePrintingAspect { 
  @Around("execution(* startMatch(..))") 
  public Object printMessageToTellHowNiceTheLifeOfAnAdviceIs( 
      ProceedingJoinPoint pjp) throws Throwable { 

    System.out.println("Greetings, Master, how are you today? I'm " 
      "very glad you're passing by today and hope you'll enjoy " + 
      "your visit!"); 
    try { 
      return pjp.proceed(); 
    } finally { 
      System.out.println("Au revoir, Master, I'm sorry you can't stay " + 
      "longer, but I'm sure you'll pay me a visit again. Have a very " + 
      "nice day yourself, sir!"); 
    } 
  } 
} 
清单 4-10 中的环绕通知示例与之前的示例不同。首先要注意的是通知方法的签名。此签名有三点特殊之处:
  • 返回类型为 java.lang.Object;其他通知类型的返回类型为 void。
  • 第一个参数的类型为 org.aspectj.lang.ProceedingJoinPoint。
  • 该方法在其 throws 子句中声明了 java.lang.Throwable。

清单 4-10 中另一个值得注意的事情是对 ProceedingJoinPoint 对象的 proceed() 方法的调用。整个通知方法实际上与经典 Spring AOP 使用的 MethodInterceptor 上的 invoke() 方法非常相似:

package org.aopalliance.intercept; public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable; } 如果您熟悉 MethodInceptor 及其 MethodInvocation 对象的工作原理,您会发现环绕通知和 ProceedingJoinPoint 非常容易使用。

切点声明和重用

您也可以在 @AspectJ 风格的方面中声明命名的切点。这些切点是跨方面重用切点声明的好方法。

清单 4-11 显示了一个带有命名切点声明的方面的示例。

清单 4-11. 声明系统级切点的方面

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class SystemPointcutsAspect { 
  @Pointcut("within(com.apress.springbook.chapter04.service..*)")   
  public void inServiceLayer() {  } 
} 
inServiceLayer 切点选择 com.apress.springbook.chapter04 中所有的连接点。

service 包,意味着该包及其子包中所有类(public 和 protected 方法)。within() 是一个*切点指示符*,我们将在本章后面“使用切点”部分讨论。

inServiceLayer() 方法是一个切点声明,也是一个常规 Java 方法。但是,Spring AOP 永远不会执行此方法;相反,它会读取其 @Pointcut 注解。因此,在方法体内添加任何实现都没有用,甚至调用此方法本身也无用,*因为*它是一个切点声明。我们建议带有 @Pointcut 注解的方法始终有一个空的方法体。这里重要的是方法的名称。

我们现在可以在其他方面重用 inServiceLayer 切点,如清单 4-12 所示(如果您这样做,请记住配置这两个方面)。

清单 4-12. 在另一个方面重用 inServiceLayer 切点

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class SecurityAspect { 
  @Before("com.apress.springbook.chapter04.aspects." + 
    "SystemPointcutsAspect.inServiceLayer()")   
  public void denyAccessToAll() { 
    throw new IllegalStateException("This system has been compromised. " + 
        "Access is denied to all!"); 
  } 
} 
切点重用为您提供了一个强大的工具,可以在一个地方选择连接点并在任何地方重用这些声明。在清单 4-11 中,我们定义了选择我们应用程序服务层的切点。在清单 4-12 中,我们决定拒绝所有人访问系统,因为存在未解决的安全问题。

我们可以通过在其他方面重用相同的切点来为服务层添加更多行为。

重用切点声明将使您的应用程序更易于维护。

Spring 容器中的自动代理创建

我们已经介绍了如何在 Spring 容器中使用 AnnotationAwareAspectJAutoProxyCreator 来启用自动代理创建,这是在 Spring AOP 中使用 @AspectJ 风格方面的前提条件。在本节中,我们将讨论启用自动代理创建的另一种方法。我们还将解释 Spring AOP 2.0 如何决定使用哪种代理类型,并进一步阐明 Spring AOP 如何决定为 bean 创建代理对象。

使用 AOP XML 架构进行自动代理创建

启用自动代理创建的另一种方法是使用 Spring AOP XML 架构及其 <aop:aspectj-autoproxy> 标签。清单 4-13 显示了一个使用 AOP XML 架构和 aop 命名空间的 Spring XML 配置文件。

清单 4-13. 使用 Spring 的 AOP XML 架构在 Spring 容器中启用 @AspectJ 风格的方面

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns: aop="http://www.springframework.org/schema/aop" 
       xsi: schemaLocation="http://www.springframework.org/schema/beans 
              http://www.springframework.org/schema/beans/spring-beans.xsd 
              http://www.springframework.org/schema/aop 
              http://www.springframework.org/schema/aop/spring-aop.xsd"> 

  <aop:aspectj-autoproxy/> 

</beans> 
使用 <aop:aspectj-autoproxy> XML 标签有一个优点:如果您不小心在 Spring 配置中多次定义此标签,也不会造成任何损害。如果您在配置中多次配置 AnnotationAwareAspectJAutoProxyCreator,则每个 bean 的自动代理创建将发生两次—这是您需要避免的。否则,这两种方法具有完全相同的效果:为整个 Spring 容器启用了自动代理创建。

代理类型选择

Spring AOP 2.0 中的代理类型选择策略与以前版本的 Spring AOP 略有不同。从 2.0 版本开始,将为至少实现一个接口的目标对象创建 JDK 代理对象。对于不实现任何接口的目标对象,将创建 CGLIB 代理对象。

您可以通过将 proxy-target-class 选项设置为 true 来强制为所有目标类创建 CGLIB 代理:

<aop:aspectj-autoproxy proxy-target-class="true"/> 当 Spring 容器中至少有一个 bean 创建了代理对象,并且该 bean 实现了一个或多个接口,但被其调用者用作类类型时,强制使用 CGLIB 代理对象是必需的。在这种情况下,仅实现接口的 JDK 代理对象对于某些调用者来说将不是类型兼容的。CGLIB 创建的代理对象与目标对象保持类型兼容。

在所有其他情况下,您可以安全地禁用 proxy-target-class 选项。

代理过程

在清单 4-5 的示例中,我们配置了三个 bean 由 Spring 容器加载。

只有一个 bean 创建了代理对象。让我们回顾一下清单 4-5 中每个 bean 定义的作用:

AnnotationAwareAspectJAutoProxyCreator:此类负责自动代理创建。

无需为此 bean 创建代理对象,因为它不被应用程序本身调用。相反,它将增强容器中所有其他 bean 的 bean 生命周期。

MessagePrintingAspect:这是一个常规 Java 类*和*一个 @AspectJ 风格的方面。此 bean 不创建代理对象,因为它也不被应用程序调用。相反,它嵌入了通知和切点,这些通知和切点将决定容器中其他 bean 将创建哪些代理对象。

DefaultTournamentMatchManager:此类是应用程序逻辑的一部分。更重要的是,它有一个连接点,该连接点被 MessagePrintingAspect 中的切点匹配:其 startMatch() 方法。因为至少有一个连接点被匹配,所以将创建一个代理对象,并在 bean 生命周期期间用代理对象替换容器中的原始 bean。因此,一旦 tournamentMatchManager bean 的 bean 生命周期成功完成,容器将返回一个带有通知及其目标对象的代理对象。

因此,让我们根据示例总结 Spring 容器中自动代理创建的规则:

  • 实现 org.springframework.beans.factory.BeanPostProcessor 或 org.springframework.beans.factory.BeanFactoryPostProcessor 接口的 bean 永远不会被代理。AnnotationAwareAspectJAutoProxyCreator 实现 BeanPostProcessor 接口,允许它增强 Spring 容器创建的 bean 的 bean 生命周期。
  • @AspectJ 风格的方面、实现 org.springframework.aop.Advisor 或 org.springframework.aop.Pointcut 接口的 bean,以及实现上一章讨论的任何经典 Spring AOP 通知类型接口的 bean,由于它们具有基础架构角色而被排除在自动代理创建之外。
  • 由 Spring 容器创建的所有其他 bean 都有资格进行自动代理创建。

在 Spring 容器创建的每个 bean(单例和原型 bean)的生命周期期间,容器将询问容器中找到的所有方面和 advisor,它们是否有任何切点匹配了 bean 的一个或多个连接点。如果至少有一个匹配,将为该 bean 创建一个代理对象,并替换该 bean。代理对象将拥有为所有匹配的连接点嵌入的所有通知。

要完全理解最后一条规则,您需要知道任何给定的切点将如何匹配连接点。如果您回顾示例及其清单 4-3 中的切点,很明显只有名为 startMatch() 的方法才会被匹配。在本章后面,“使用切点”部分,我们将讨论以特定方式选择连接点的其他切点。

通知和方面排序

方面中声明的通知在自动代理创建期间被自动选择并添加到代理对象中。可能存在两个通知应用于同一个连接点。考虑清单 4-14 中显示的 MessagePrintingAspect @AspectJ 风格方面。

清单 4-14. 两个通知将针对相同的连接点执行

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class MessagePrintingAspect { 
  @Pointcut("execution(* startMatch(..))")   
  public void atMatchStart() {} 

  @Before("atMatchStart()")   
  public void printHowAnnoyedWeAre() { 
    System.out.println("Leave it out! Another tennis match!?"); 
  } 
  
  @Before("atMatchStart()")   
  public void printHowExcitedWeAre() { 
    System.out.println("Hurray for another tennis match!"); 
  } 
} 
清单 4-14 中的方面声明了两个通知,它们将针对相同的连接点执行。

这可能会让您想知道它们将按什么顺序执行。在此示例中,实际顺序不是很重要,但在其他场景下,理解确切的顺序可能很重要。如果这两个通知定义在不同的方面中,顺序会是怎样的?

排序通知

在通知声明在同一个方面中,并且它们都为同一个连接点执行的情况下,Spring AOP 使用与 AspectJ 相同的顺序:声明顺序。因此,在同一个方面中,为同一个连接点执行的通知将保持其声明顺序。

对于清单 4-14 中的方面,考虑清单 4-15 中的 Spring 配置。

清单 4-15. 配置具有在同一方面中声明的两个通知的 DefaultTournamentMatchManager

<beans> 
  <bean class="org.springframework.aop.aspectj.annotation. --> 
AnnotationAwareAspectJAutoProxyCreator"/> 

  <bean class="com.apress.springbook.chapter04.aspects.MessagePrintingAspect"/> 

  <bean id="tournamentMatchManager" 
    class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
    <!-- properties omitted --> 
  </bean> 
</beans> 
当 tournamentMatchManager bean 上的 startMatch() 方法被执行时,控制台将打印以下消息:

请放下!又一场网球比赛?!

为又一场网球比赛欢呼!所以,这两个方面是按照它们的声明顺序执行的。

排序方面

当在*不同*的方面中声明的两个通知为同一个连接点执行时,顺序由 org.springframework.core.Ordered 接口确定,如清单 4-16 所示。

清单 4-16. Spring 的 Ordered 接口

package org.springframework.core; 

public interface Ordered { 
  int getOrder(); 
} 
Spring 框架在需要以特定顺序处理对象列表时使用 Ordered 接口。通过实现 Ordered 接口的方面,您可以将您的通知放置在连接点通知执行顺序中的特定位置。方面的排序规则如下:
  • 不实现 Ordered 接口的方面处于不确定的顺序,并排在实现该接口的方面之后。
  • 实现 Ordered 接口的方面根据 getOrder() 方法的返回值进行排序。值越小,位置越靠前。
  • 两个或多个具有相同的 getOrder() 方法返回值的方面处于不确定的顺序。

为了演示方面的排序是如何工作的,我们首先创建一个公共切点,如清单 4-17 所示。

清单 4-17. 一个公共切点

package com.apress.springbook.chapter04.aspects; 

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


@Aspect 
public class TennisMatchEventsAspect { 
  @Pointcut("execution(* startMatch(..))")   
  public void atMatchStart() {  } 
} 
接下来,我们将声明两个独立的方面中的通知,如清单 4-18 和 4-19 所示。

清单 4-18. 实现 Spring Ordered 接口的方面

package com.apress.springbook.chapter04.aspects; 

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


import org.springframework.core.Ordered; 


@Aspect 
public class HappyMessagePrintingAspect implements Ordered { 
  private int order = Integer.MAX_VALUE; 
public int getOrder() { return this.order; }   
  public void setOrder(int order) { this.order = order; } 
    @Before("com.apress.springbook.chapter04.aspects." + 
        "TennisMatchEventsAspect.atMatchStart()")   
    public void printHowExcitedWeAre() { 
      System.out.println("Hurray for another tennis match!"); 
    } 
  } 
清单 4-19. 未实现 Ordered 接口的方面
package com.apress.springbook.chapter04.aspects;  

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


@Aspect 
public class AnnoyedMessagePrintingAspect { 
  @Before("com.apress.springbook.chapter04.aspects." + 
    "TennisMatchEventsAspect.atMatchStart()")   
  public void printHowAnnoyedWeAre() { 
    System.out.println("Leave it out! Another tennis match!?"); 
  } 
} 
接下来,我们将这两个方面加载到 Spring 容器中,如清单 4-20 所示。

清单 4-20. 配置两个方面供 Spring 容器加载

<beans> 
  <bean class="org.springframework.aop.aspectj.annotation. --> 
AnnotationAwareAspectJAutoProxyCreator"/> 

  <bean class="com.apress.springbook.chapter04.aspects.HappyMessagePrintingAspect"/> 

  <bean class="com.apress.springbook.chapter04.aspects. --> 
AnnoyedMessagePrintingAspect"/> 

  <bean id="tournamentMatchManager" 
    class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
    <!-- properties omitted --> 
  </bean> 
</beans> 
当我们在 tournamentMatchManager bean 上调用 startMatch() 方法时,将向控制台打印以下消息:

为又一场网球比赛欢呼!请放下!又一场网球比赛?!

我们得到这个消息顺序是因为 HappyMessagePrintingAspect 实现 Ordered 接口而 AnnoyedMessagePrintingAspect 没有。

因为我们在 HappyMessagePrintingAspect 中实现了 setOrder() 方法,所以我们可以通过 bean 定义来更改 order 值,如下所示:

<bean class="com.apress.springbook.chapter04.aspects.HappyMessagePrintingAspect"/> <property name="order" value="20"/> </bean> 尽管我们可以控制方面及其通知的顺序,但单个方面内的通知声明顺序仍然不变。

到目前为止,本章只讨论了 @AspectJ 风格的方面,但还有一个替代方案,我们将在下一节中介绍。

使用 AOP XML 标签

Spring 开发人员想出了一种通过创建 AOP XML 架构来定义 XML 中的方面的方法,用于 Spring 2.0 自定义 XML 架构支持。它允许您将 Spring 容器创建的 bean 上的任何公共实例方法转换为通知方法。这些方法与 @AspectJ 风格方面中用 @Aspect 注解的方法相当。

@AspectJ 风格的方面使用 Java 5 注解,因此当生产环境不使用 Java 5 时(许多组织仍使用 Java 1.4),它们不是一个选项。此外,您可能希望使用现有类中的方法作为通知方法。

本节介绍如何创建 XML 中的方面,这将解决这些问题。我们还将展示如何用本章讨论的 AspectJ 切点替换上一章介绍的切点类。

您会注意到,XML 方面和通知声明如果您熟悉 @AspectJ 风格的方面,将非常容易理解和使用。您可能还会注意到,使用 XML 声明时,通知类型和切点与通知方法分离(有些人认为这是一个缺点,因为它分割了一个功能单元)。因此,我们建议您在可能的情况下使用 @AspectJ 风格编写方面。

AOP 配置标签

使用 AOP XML 标签声明方面、切点和 advisor 的第一步是创建一个 Spring XML 文件,如清单 4-21 所示。

清单 4-21. 基于 Spring 2.0 XML 架构的 Spring XML 配置文件

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns: aop="http://www.springframework.org/schema/aop" 
       xsi: schemaLocation="http://www.springframework.org/schema/beans 
              http://www.springframework.org/schema/beans/spring-beans.xsd 
              http://www.springframework.org/schema/aop 
              http://www.springframework.org/schema/aop/spring-aop.xsd"> 
</beans> 
您可以将此文件与其他经典的 Spring XML 配置文件一起加载到 Spring 容器中。要声明 XML 中的方面和 advisor,请将 <aop:config> 标签添加到 XML 文件中,如清单 4-22 所示。

清单 4-22. 使用 aop:config 标签在 XML 中创建 AOP 配置单元

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns: aop="http://www.springframework.org/schema/aop" 
       xsi: schemaLocation="http://www.springframework.org/schema/beans 
              http://www.springframework.org/schema/beans/spring-beans.xsd 
              http://www.springframework.org/schema/aop 
              http://www.springframework.org/schema/aop/spring-aop.xsd"> 

    <aop:config> 

    </aop:config> 

</beans> 
您可以在一个或多个 XML 文件中声明多个 <aop:config> 标签。<aop:config> 标签可以包含零个或多个以下标签:
  • <aop:aspect>:允许您在 XML 中创建与 @AspectJ 风格方面相当的方面。
  • <aop:advisor>:允许您使用 AspectJ 切点和经典的 Spring AOP 通知对象创建 advisor 对象。
  • <aop:pointcut>:允许您在 XML 方面中声明和重用切点。
我们将在接下来的示例中更详细地介绍这些标签。现在我们将清单 4-3 中的 @AspectJ 风格方面重新创建为 XML。

XML 方面配置

本章讨论的 @AspectJ 风格概念也适用于在 XML 中声明的方面。

唯一的区别是使用 XML 而不是注解。

使用 XML 创建方面的第一步是将 <aop:aspect> 标签添加到 <aop:config> 中,如清单 4-23 所示。

清单 4-23. xml-aspect-context.xml:创建一个空的 XML 方面

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns: aop="http://www.springframework.org/schema/aop" 
       xsi: schemaLocation="http://www.springframework.org/schema/beans 
              http://www.springframework.org/schema/beans/spring-beans.xsd 
              http://www.springframework.org/schema/aop 
              http://www.springframework.org /schema/aop/spring-aop.xsd"> 

  <aop:config> 

    <aop:aspect ref="messagePrinter"> 

    </aop:aspect> 
  </aop:config> 

  <bean id="messagePrinter" 
    class="com.apress.springbook.chapter04.MessagePrinter"/> 

  <bean id="tournamentMatchManager" 
      class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
      <!-- properties omitted --> 
  </bean> 
</beans> 
<aop:aspect> 标签接受一个 ref 属性,该属性包含 bean 定义的名称(messagePrinter)。清单 4-24 显示了 MessagePrinter 类。

清单 4-24. MessagePrinter 类

package com.apress.springbook.chapter04; 

public class MessagePrinter { 
  public (.*?) { 
      System.out.println("Attempting to start tennis match!"); 
  } 
} 
MessagePrinter 类与清单 4-3 中的 MessagePrintingAspect 类似,但没有 @AspectJ 风格的注解。所以 MessagePrinter 是一个常规 Java 类,而不是一个方面声明。

但是,我们在清单 4-23 的 bean 定义中配置了 MessagePrinter,*并且*我们让 <aop:aspect> 标签引用了 messagePrinter bean 定义。此配置将 messagePrinter *bean* 声明为方面,而不是 MessagePrinter 类。我们还将 tournamentMatchManager bean 添加到了清单 4-23 的配置中。

到目前为止,此方面配置不会执行任何非凡的操作。但是,我们可以向 <aop:aspect> 标签添加更多配置,将 messagePrinter *bean* 上的 printMessageToInformMatchStarts() 方法转换为通知方法,如清单 4-25 所示。

清单 4-25. xml-aspect-context.xml:将 messagePrinter bean 上的 printMessageToInformMatchStarts() 方法转换为通知方法

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns: aop="http://www.springframework.org/schema/aop" 
       xsi: schemaLocation="http://www.springframework.org/schema/beans 
              http://www.springframework.org/schema/beans/spring-beans.xsd 
              http://www.springframework.org/schema/aop 
              http://www.springframework.org/schema/aop/spring-aop.xsd"> 

  <aop:config> 
    <aop:aspect ref="messagePrinter"> 

      <aop:before method="printMessageToInformMatchStarts" 
            pointcut="execution(* startMatch(..))"/> 
      </aop:aspect> 
    </aop:config> 

  <bean id="messagePrinter" 
      class="com.apress.springbook.chapter04.MessagePrinter"/> 

  <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
    <!-- properties omitted --> 
  </bean> 
</beans> 

就这样——我们创建了 XML 方面。<aop:before> 标签声明 messagePrinter bean 上的 printMessageToInformMatchStarts() 方法成为前置通知,并声明了切点。

让我们回顾一下清单 4-25 中的配置,看看所有元素及其作用。

  • <aop:config> 标签激活了 Spring 容器中的自动代理创建。它还包含一个或多个 XML 方面、切点或 advisor 声明的配置。
  • <aop:aspect> 标签引用 messagePrinter *bean* 并将该*bean* 声明为一个方面。

    这个 bean 本身不受任何影响。事实上,messagePrinter bean 也不知道它被声明为一个方面。此标签本身不会在自动代理创建期间触发代理对象的创建。该标签仅声明一个可以包含零个或多个 XML 中的切点和通知/切点声明的方面。

  • <aop:before> 标签是一个通知/切点声明,它将为由切点匹配的所有连接点的执行执行 messagePrinter bean 上的 printMessageToInformMatchStarts() 方法。
  • messagePrinter bean 是一个普通的 bean,由 Spring 容器根据普通的 bean 定义创建和配置。它不知道 Spring AOP、通知、自动代理创建或切点。事实上,您可以获取这个 bean 并执行它的方法,您会发现它们的响应与预期的一样。
  • tournamentMatchManager bean 在其 bean 生命周期中受到自动代理创建的影响,因为它有一个连接点——startMatch() 方法——该连接点被 XML 方面声明中的切点匹配。当执行其 startMatch() 方法时,将首先执行 messagePrinter bean 上的 printMessageToInformMatchStarts() 方法。

    接下来,我们将把 xml-aspect-context.xml 加载到一个测试用例中,以验证 tournamentMatchManager bean 是否已正确代理。清单 4-26 中的测试用例显示了 MessagePrintingXmlAspectIntegration Tests,它扩展了清单 4-6 中的 MessagePrintingAspectIntegrationTests。

    清单 4-26. 用于验证 XML 方面是否按预期工作的测试用例

    package com.apress.springbook.chapter04; 
    
    public class MessagePrintingXmlAspectIntegrationTests extends 
    MessagePrintingAspectIntegrationTests { 
    protected String[] getConfigLocations() { 
      return new String[] { 
         "classpath:com/apress/springbook/chapter04/" + 
         "xml-aspect-context.xml" 
       }; 
      } 
    } 
    
    清单 4-26 中的测试用例运行清单 4-6 中声明的测试方法,并覆盖 getConfigLocations() 方法以加载清单 4-25 中的 Spring XML 文件。运行测试时,控制台将打印以下消息:
    === GOING TO CALL METHOD ON BEAN FROM CONTAINER === 
    Attempting to start tennis match! 
    === FINISHED CALLING METHOD ON BEAN FROM CONTAINER === 
    === GOING TO CALL METHOD ON NEWLY CREATED OBJECT === 
    === FINISHED CALLING METHOD ON NEWLY CREATED OBJECT === 
    

    XML 中的切点声明和重用

    您可以在 AOP XML 配置中声明和重用切点,也可以重用在 @AspectJ 风格方面中声明的切点。清单 4-27 重用了 SystemPointcutsAspect(清单 4-11)中声明的切点。SecurityEnforcer 类与 SecurityAspect 类相同,但已剥离其方面状态。

    清单 4-27. 重用在 @AspectJ 风格方面中声明的切点

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org/schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:aspect ref="securityEnforcer"> 
          <aop:before method="denyAccessToAll" 
                     pointcut="com.apress.springbook.chapter04.aspects. --> 
          SystemPointcutsAspect.inServiceLayer()"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="securityEnforcer" 
        class="com.apress.springbook.chapter04.SecurityEnforcer"/> 
    </beans> 
    
    您还可以声明 XML 中的切点,并且可以在两个地方声明它们。第一个选项如清单 4-28 所示,它在 <aop:aspect> 标签内声明了一个切点。此切点只能在此方面内重用。

    清单 4-28. 在 XML 方面中声明和重用切点

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org/schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:aspect ref="securityEnforcer"> 
          <aop:pointcut id="inServiceLayer" 
                    expression="within(com.apress.springbook.chapter04..*)"/> 
          <aop:before method="denyAccessToAll" 
                    pointcut-ref="inServiceLayer"/> 
        </aop:aspect> 
      </aop:config> 
    
      <bean id="securityEnforcer" 
        class="com.apress.springbook.chapter04.SecurityEnforcer"/> 
    </beans> 
    

    <aop:pointcut> 标签声明一个切点,并接受一个名称(id)和切点表达式(expression)。然后由 <aop:before> 标签(pointcut-ref)重用此切点。

    清单 4-29 显示了一个声明在 <aop:config> 标签内的切点。此切点可以在此及其他 Spring XML 文件中的 <aop:aspect> 标签内重用。

    清单 4-29. 在方面外部声明一个切点并在 XML 方面内部重用它

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:pointcut id="inServiceLayer" 
    expression="within(com.apress.springbook.chapter04..*)"/> 
    <aop:aspect ref="securityEnforcer"> 
    <aop:before method="denyAccessToAll" 
    pointcut-ref="inServiceLayer"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="securityEnforcer" 
        class="com.apress.springbook.chapter04.SecurityEnforcer"/> 
    </beans> 
    
    XML 中声明的切点存在某些限制,例如它们不能在 @AspectJ 风格的方面中重用。此外,它们也不能接受动态切点指示符,例如 args() 和 @annotation()(切点指示符将在本章后面的“使用切点”部分讨论)。原因是切点声明与方法不耦合,正如在 @AspectJ 风格的方面中所述。

    XML 中的通知声明

    在 XML 中声明的方面支持与 @AspectJ 风格方面相同的通知类型,具有完全相同的语义。如上一节所述,XML 中的通知声明使用对象上的常规 Java 方法作为通知方法。

    现在我们将介绍如何在 XML 中声明每种不同的通知类型。稍后,在“绑定通知参数”部分,我们将重写我们用来解释如何绑定 @AspectJ 风格通知方法上的通知参数的方面,并显示等效的 XML,以便您可以轻松地进行比较。

    清单 4-30 显示了一个前置通知 XML 声明的示例。清单 4-30. XML 中的前置通知声明

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
    
    
      <aop:config> 
        <aop:aspect ref="messagePrinter"> 
    <aop:before method="printMessageToInformMatchStarts" 
    pointcut="execution(* startMatch(..))"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="messagePrinter" 
        class="com.apress.springbook.chapter04.MessagePrinter"/> 
    
      <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
        <!-- properties omitted --> 
      </bean> 
    </beans> 
    
    清单 4-31 显示了一个使用后置返回通知的示例。

    清单 4-31. 在 XML 中声明的后置返回通知

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org/schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:aspect ref="messagePrinter"> 
    <aop:after-returning method="printMessageToInformMatchHasStarted" 
    pointcut="execution(* startMatch(..))"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="messagePrinter" 
        class="com.apress.springbook.chapter04.MessagePrinter"/> 
    
      <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
    <!-- properties omsitted --> 
    </bean> 
    </beans> 
    
    在 XML 中声明后置异常通知同样简单,如清单 4-32 所示。

    清单 4-32. XML 中的后置异常通知声明

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
    
    
    
    
      <aop:config> 
        <aop:aspect ref="messagePrinter"> 
    <aop:after-throwing method="printMessageWhenMatchIdentifierIsNotFound" 
    pointcut="execution(* startMatch(..) throws --> 
    com.apress.springbook.chapter04.UnknownMatchException)"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="messagePrinter" 
        class="com.apress.springbook.chapter04.MessagePrinter"/> 
    
      <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
        <!-- properties omitted --> 
      </bean> 
    </beans> 
    
    
    在清单 4-33 所示的后置(finally)通知示例中,我们再次使用了一个切点来匹配 startMatch() 方法。清单 4-33. XML 中的后置(finally)通知声明
    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:aspect ref="messagePrinter"> 
    <aop:after method="printMessageWhenStartMatchAttemptIsOver" 
    pointcut="execution(* startMatch(..))"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="messagePrinter" 
        class="com.apress.springbook.chapter04.MessagePrinter"/> 
    
      <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
        <!-- properties omitted --> 
      </bean> 
    </beans> 
    
    清单 4-34 中的代码显示了 MessagePrinter 上的 printMessageWhenStartMatchAttemptIsOver() 方法。

    清单 4-34. MessagePrinter 上的 printMessageWhenStartMatchAttemptIsOver() 方法

    package com.apress.springbook.chapter04; 
    
    public class MessagePrinter { 
      public (.*?) { 
          System.out.println("Tried to start a match and this attempt is now over!");    } 
    } 
    

    同样,因为后置(finally)通知 XML 声明与我们之前讨论的 @AspectJ 风格声明非常相似,所以没有意外。

    最后但同样重要的是,环绕通知作为 XML 声明。您可能已经猜到,此通知类型要求在通知方法中声明一个 ProceedingJoinPoint 参数。这会将 MessagePrinter 类绑定到 AspectJ API。清单 4-35 显示了 XML 中的环绕通知声明。

    清单 4-35. XML 中的环绕通知声明

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:aspect ref="messagePrinter"> 
    <aop:around method="controlStartMatchMethodExecution" 
    pointcut="execution(* startMatch(..))"/> 
    </aop:aspect> 
    </aop:config> 
    
      <bean id="messagePrinter" 
        class="com.apress.springbook.chapter04.MessagePrinter"/> 
    
      <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
        <!-- properties omitted --> 
      </bean> 
    </beans> 
    
    清单 4-36 显示了 MessagePrinter 上的 controlStartMatchMethodExecution() 方法。

    清单 4-36. MessagePrinter 上的 controlStartMatchMethodExecution() 方法

    package com.apress.springbook.chapter04; 
    
    import org.aspectj.lang.ProceedingJoinPoint; 
    
    public class MessagePrinter { 
      public Object controlStartMatchMethodExecution(ProceedingJoinPoint pjp) 
        throws Throwable { 
      System.out.println("A match is about to be started!"); 
    try { 
    Object result = pjp.proceed(); 
    System.out.println("The match has been started successfully!"); 
    return result; 
    } catch (Throwable t) { 
    System.out.println("Oops, something went wrong while starting the match."); 
    throw t; 
      } 
    } 
    } 
    

    MessagePrinter 上的 controlStartMatchMethodExecution() 方法是拦截通知的一个经典示例。

    XML 中的通知排序

    XML 中声明的方面的代理对象也通过自动代理创建。仅声明一次 <aop:config> 标签将自动配置 Spring 容器。正如我们之前在本章中讨论过的,自动代理创建意味着通知的顺序是不确定的,您可能需要控制通知顺序。XML 中声明的方面的排序与 @AspectJ 风格方面中声明的方面的排序非常相似:
    • 在同一个 XML 方面中声明的、在一个连接点上执行的通知,按照它们在 XML 文件中的声明顺序进行排序。
    • 如果您想控制不同 XML 方面中声明的通知的顺序,您必须在声明通知方法的类(如清单 4-24 中的 MessagePrinter 类)中实现 org.springframework.core.Ordered 接口。

    通过实现 Ordered 接口,您还可以控制 XML 通知和 @AspectJ 风格通知在同一连接点上的顺序。

    带有 AspectJ 切点的 Advisor

    在上一章中,您了解到 advisor 包含一个通知对象和一个切点对象。

    当您将 advisor 添加到 Spring 容器并配置自动代理创建时,其切点可以匹配容器中 bean 上的连接点;当匹配时,将创建代理对象。

    考虑清单 4-37 中的示例。请注意,必须先定义 advisor,然后才能定义方面。

    清单 4-37. 使用 <aop:advisor> 标签配置 PointcutAdvisor

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:advisor advice-ref="loggingAdvice" pointcut="execution(* startMatch(..))"/> 
    </aop:config> 
    
      <bean id="loggingAdvice" 
        class="org.springframework.aop.interceptor.CustomizableTraceInterceptor"> 
    <property name="enterMessage" 
    value="Entering $[methodName] on $[targetClassShortName]"/> 
    </bean> 
    
      <bean id="tournamentMatchManager" 
        class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
        <!-- properties omitted --> 
      </bean> 
    </beans> 
    

    自动代理创建将为 tournamentMatchManager bean 创建一个代理,因为其 startMatch() 方法与 advisor 的切点匹配。

    advisor 参与自动代理创建,这意味着它们的通知对象可以与其他通知在同一连接点上一起执行。要设置 advisor 的顺序,您可以配置 order 属性,如清单 4-38 所示。

    清单 4-38. 设置此 advisor 的通知顺序

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
      <aop:config> 
        <aop:advisor advice-ref="loggingAdvice" 
                  order="20" 
                  pointcut="execution(* startMatch(..))"/> 
    </aop:config> 
    
      <bean id="loggingAdvice" 
        class="org.springframework.aop.interceptor.CustomizableTraceInterceptor"> 
    <property name="enterMessage" 
                  value="Entering $[methodName] on $[targetClassNameShort]"/> 
    </bean> 
    
      <bean id="tournamentMatchManager" 
                  class="com.apress.springbook.chapter04.DefaultTournamentMatchManager"> 
        <!-- properties omitted --> 
      </bean> 
    </beans>
    

    XML 中的代理类型选择

    在使用 XML 中的方面声明时,您可以选择自动代理类型检测(JDK 或 CGLIB 代理对象)或强制使用 CGLIB 代理对象。您可以切换这两种模式,方法是在 <aop:config> XML 标签上设置 proxy-target-class 属性为 true,如清单 4-39 所示。

    清单 4-39. 强制使用 CGLIB 代理对象

    <?xml version="1.0" encoding="UTF-8"?> 
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns: xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns: aop="http://www.springframework.org/schema/aop" 
           xsi: schemaLocation="http://www.springframework.org/schema/beans 
                  http://www.springframework.org/schema/beans/spring-beans.xsd 
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org /schema/aop/spring-aop.xsd"> 
    
        <aop:config proxy-target-class="true"> 
    
      </aop:config> 
    
    </beans> 
    
    由于您可以在配置中添加多个 <aop:config> 标签并在每个标签中切换 proxy-target-class 属性值,因此可能不清楚哪种代理对象创建模式是活动的。设置此模式的规则很简单:当至少一个 <aop:config> 标签将 proxy-target-class 值设置为 true 或 <aop:aspectj-autoproxy> 将该值设置为 true 时,Spring AOP 将始终创建 CGLIB 代理对象。

    因此,既然您已经了解了配置 Spring AOP 的两种不同方法,让我们更详细地看看切点。

    使用切点

    在本章中,我们在方面中反复使用了相同的简单切点。AspectJ 切点语言在选择连接点方面非常强大且通用。请记住,Spring AOP 只支持方法执行作为连接点。虽然这远少于 AspectJ 支持的内容,但仅凭方法执行,您仍然可以做一些非常巧妙的事情。具体来说,切点允许您根据各种标准选择方法,并且您可以使用逻辑运算符组合标准来缩小选择范围。在 Spring AOP 中,切点的目标是选择 public 或 protected 方法。方法在 Java 类中声明,但在选择它们时,您通常希望跳出类的狭窄边界并进行泛化。以下是一些您可能想选择的示例:
    • 特定类的所有方法
    • 特定包中所有类的所有方法
    • 特定包及其子包中所有类的所有方法
    • 具有特定注解的所有方法
    • 具有特定注解的类的所有方法
    • 具有特定名称的所有方法
    • 名称包含特定字符串的所有方法
    • 具有特定参数类型的所有方法
    为了选择方法,您可以在切点表达式中使用这些构造:
    • 切点指示符:我们已经使用了 execution()within()。Spring AOP 支持更多指示符,您将在以下示例中看到。它们根据某个属性缩小连接点的选择范围。此列表中的其他构造与这些指示符结合使用。
    • 布尔运算符:您可以组合切点指示符来缩小选择范围。支持 AND (&&) 和 OR (||) 运算符,以及 NOT 运算符 (!)。
    • 类名:通常,您需要指定类名,例如将选择范围缩小到目标类型或方法参数类型。这些类名必须始终是完全限定的(包括完整的包名),即使对于 java.lang 包中的类也是如此。
    • 可见性运算符:您可以将搜索范围缩小到 public 或 protected 方法。
    • 通配符:通常,您会想要使用通配符运算符。支持的通配符运算符是星号 (*) 用于任何方法或类名或任何参数类型,以及双点 (..) 用于零个或多个参数。
    • 方法名:可以是完整名称、名称的部分匹配或通配符(任何名称)。
    • 注解:您还可以根据注解选择方法,如以下各节所述。

    让我们从直接选择方法开始。

    直接选择方法

    大多数时候,您希望根据方法的某些部分(例如名称、返回类型、throws 声明或参数)来选择方法。在基于方法部分进行选择时,您将使用 execution() 或 args() 切点指示符或它们的组合。

    按方法名称选择

    选择方法的最简单方法是通过名称缩小选择范围。

    清单 4-40 显示了一个选择所有 relax()enjoy()chillOut() 方法的切点。

    清单 4-40. 选择一些愉快的练习

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("execution(* relax(..)) || execution(* enjoy(..)) || " + 
          "execution(* chillOut(..))") 
      public void goodTimes() {  } 
    } 
    
    清单 4-40 中的每个 execution() 切点指示符都根据方法名称选择方法,而忽略所有其他属性。

    您也可以在方法名称中使用通配符。清单 4-41 显示了一个选择所有名称以 do 开头的方法的切点。

    清单 4-41. 选择所有名称以 do 开头的方法

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("execution(* do*(..))") 
      public void performAction() {  } 
    } 
    

    按方法名称选择是最基本切点标准之一。它们可以与其他标准结合使用,以缩小选择范围,确保只选择预期的那些方法。

    缩小方法选择范围的一种强大方法是同时根据方法参数进行选择。

    按参数类型选择

    您还可以根据方法的参数声明进行选择。清单 4-42 显示了将选择范围缩小到以下内容的切点:
    • 没有参数的所有方法
    • 具有一个参数的所有方法,无论其类型如何
    • 具有一个 java.lang.String 参数的所有方法
    • 以 java.lang.String 作为第一个参数,以及零个或多个其他参数的所有方法
    • 以 java.lang.String 作为第二个参数,以及零个或多个其他参数的所有方法
    清单 4-42. 根据其参数选择方法
    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("execution(* *())")   
      public void allMethodsWithoutArguments() {} 
    
      @Pointcut("execution(* *(*))")   
      public void allMethodsWithOneArgumentRegarlessOfType() {} 
    
      @Pointcut("execution(* *(java.lang.String))")   
      public void allMethodsWithOneArgumentOfTypeString() {} 
    
      @Pointcut("execution(* *(java.lang.String,..))")   
      public void  
      allMethodsWithFirstArgumentOfTypeStringAndZeroOrMoreOtherArguments() {} 
    @Pointcut("execution(* *(*,java.lang.String,..))")   
      public void  
          allMethodsWithSecondArgumentOfTypeStringAndZeroOrMoreOtherArguments () {  } 
    } 
    
    在使用 execution() 切点指示符根据参数缩小选择范围时,Spring AOP 将查看静态方法声明。这是与自动代理创建结合使用的安全选项,因为 Spring AOP 可以查看方法元数据来确定是否有匹配项。请记住,此匹配对于决定是否为 Spring 容器中的任何给定 bean 创建代理对象很重要。另一种选择参数的方法是根据传递给代理对象的方法执行的实际类型。清单 4-43 显示了一个带有选择具有一个参数的方法的切点的方面。

    清单 4-43. 根据实际参数值而不是参数声明选择方法

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    import org.aspectj.lang.annotation.Before; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
    
      @Before("args(java.util.Hashtable)")   
      public void printWarningForUsageOfHashtable() { 
        System.out.println("Warning: java.util.Hashtable is passed as argument!"); 
      } 
    } 
    
    java.util.Hashtable 是一个同步类,用于在多线程环境中共享一个 map 对象的场景。Hashtable 的性能不如非同步 map,我们希望在单线程环境中对其使用发出警告。

    以下三个方法签名都将被此切点选择:

    public java.util.List getUniqueValuesFromMap(java.util.Map map) { … } 
    
    public Object getLowestKeyValueFromMap(java.util.Map map) { … } 
    
    public String transformObjectToString(Object o) { … }
    

    由于 Spring AOP 无法确定将哪个参数值传递给这些方法,因此这三个方法签名都将由清单 4-43 中的切点在自动代理创建期间匹配。Spring AOP 知道这些单参数方法中的任何一个都有可能被此切点匹配。但是,即使 Spring AOP 决定为实现这些方法中任何一种的 bean 创建代理对象,也不意味着通知将始终为这些方法执行。Spring AOP 将需要每次执行这些方法时进行匹配,以决定是否执行通知,具体取决于传递的参数值。此过程可能会产生创建比预期更多的代理对象的非预期副作用。

    在 Spring AOP 中,代理创建相对廉价,并且对于单例 bean 来说不是问题。

    对于原型 bean 来说,情况就不同了,因为每次通过依赖查找或依赖注入请求原型 bean 时都需要创建一个代理对象。如果您要创建许多原型对象,您应该意识到动态切点*可能*对 Spring 容器中的原型 bean 创建产生性能影响。

    根据返回类型声明选择

    另一种根据返回类型声明选择方法的方法是,它可以是任何类型或 void。让我们考虑在本章开头用于选择所有名为 startMatch() 的方法的切点:
    execution(* startMatch(..)) 
    

    此切点中的星号 (*) 匹配任何返回类型;返回类型和方法签名都是 execution() 切点指示符的必需字段。我们可以将此切点更改为仅匹配返回我们期望类型的 startMatch() 方法,如下所示:

    execution(com.apress.springbook.chapter04.Match startMatch(..)) 
    

    为什么我们要指定返回类型,而整个应用程序只有一个 startMatch() 方法,而且 * 输入更少?

    编写切点时,您需要小心您将选择哪些连接点。作为意外副作用而选择的任何连接点都会产生开销。当应用程序增长时,许多小开销通常会变得很重要。

    您还可以使用 void 关键字作为返回类型,以仅选择不返回任何内容的(无返回值)方法。


    注意

    根据返回类型声明来选择方法不需要动态切点,因为 Spring AOP 可以在自动代理创建时查看静态方法签名。


    根据 throws 声明选择方法

    有时您希望根据方法的 throws 声明来选择方法。这些切点与 after throwing 通知并不严格相关;它们可以与任何类型的通知一起使用。

    要匹配异常类型,它必须在方法签名中声明。正如您可能已经知道的,Java 有两种异常类型:

    运行时异常(Unchecked exceptions):这些是无法恢复的异常,通常意味着应用程序在这些异常发生时无能为力。由于其不可恢复的性质,Java 不要求在方法签名中声明这些异常,也不要求应用程序代码捕获它们。与 java.lang.Error 和 java.lang.RuntimeException 类型兼容的异常是运行时异常。

    已检查异常(Checked exceptions):这些通常是可恢复的异常,意味着应用程序逻辑可以采取有意义的措施来响应它们的发生。这些异常必须在 try/catch 块中捕获,或者在方法签名中声明以传递给调用者。

    与 java.lang.Error 和 java.lang.RuntimeException 不兼容的异常是已检查异常。

    如果未在方法体中捕获,已检查异常必须在方法签名中声明;运行时异常可以声明,但这不是必需的。运行时异常有时会在方法签名中声明,以告知调用者它们可能会被抛出。但是,即使运行时异常在方法签名中声明,调用者也不需要捕获它们。

    这让我们得出以下结论:

    • 已检查异常通常在方法签名中声明。
    • 运行时异常通常在方法签名中声明。

    由于切点只能匹配在其签名中声明了异常的方法,因此您通常只能匹配已检查异常,如列表 4-44 所示。

    列表 4-44.匹配 java.io.IOException

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    import org.aspectj.lang.annotation.Before; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
    
      @Before("execution(* *(..) throws java.lang.IOException)")   
      public void printWarningIOExceptionMayBeThrown() { 
    
        System.out.println("Warning: IOException may be thrown. Brace yourselves!"); 
      } 
    } 
    
    您只需要指定要匹配的异常类型。方法签名可能声明了其他类型,但仍然会被匹配。以下方法签名将由列表 4-44 中的切点匹配:

    public String readFileToString(java.io.File file) throws java.io.IOException, java.lang.IllegalArgumentException { … } public void copy(java.io.InputStream in, java.io.OutputStream out) throws java.io.IOException, java.lang.IllegalStateException { … } 当我们在本章后面讨论绑定通知方法参数时,我们将研究如何响应实际由方法抛出的异常。


    注意

    根据 `throws` 声明选择方法不需要动态切点,因为 Spring AOP 可以在自动代理创建时查看静态方法签名。


    通过类、包和继承选择方法

    直接选择方法在编写针对特定方法或具有特定属性的方法的通知时非常有用。然而,切面的最有趣的应用是强制执行系统范围的策略,如下例所示:
    • 日志记录:您想确切地了解应用程序组件如何交互,或者您想衡量方法执行的性能。
    • 审计:您有要求记录谁在执行一系列组件上的操作。
    • 安全:您想在调用方法时强制执行用户角色,或者您想根据安全策略过滤返回值。
    • 优化:您想了解用户如何使用您的领域逻辑,以便了解可以在哪里添加更多功能,或者如何改进性能、减少内存使用或减少常见操作的数据库访问。

    这些要求中的每一个通常都基于通用属性(如包含类、包含包或继承层次结构)对大量方法产生影响。其中一些要求只能通过绑定通知方法参数来实现,我们将在本章后面讨论这一点。在这里,我们将重点介绍如何根据类、包和继承来选择方法。

    按类选择

    在选择一个类中的所有方法时,有两个重要的区别:
    • 能够根据静态(非动态)类和方法信息确定匹配的切点。`execution()` 和 `within()` 切点指示符将只查看静态信息,非常适合在切点中缩小选择范围。
    • 需要方法执行上下文来确定匹配的切点。需要方法执行的切点并非总是可以避免,但了解如何限制要代理的对象数量至仅限目标对象非常重要。

    您已经熟悉 `execution()`,但我们还没有讨论如何在单个类中选择方法。`within()` 切点指示符也可以将选择范围缩小到特定类型的所有方法。以下是两个选择完全相同的连接点的切点:

    execution(* com.apress.springbook.chapter04.DefaultTournamentMatchManager.*(..)) 
    within(com.apress.springbook.chapter04.DefaultTournamentMatchManager) 
    

    这些切点选择 DefaultTournamentMatchManager 类上的所有方法。它们使用静态信息,因此不需要方法执行的上下文。但是,有时您需要它,特别是在使用通知方法参数绑定时,我们稍后将讨论。我们已经讨论了 `args()` 切点指示符,它强制执行动态切点。

    另外两个动态切点指示符是 `target()` 和 `this()`。`target()` 指示符将选择范围缩小到代理对象的实际目标对象。`this()` 指示符将选择范围缩小到代理对象本身。(有关 `target()` 和 `this()` 的更多详细信息,请参阅 Spring 2.0 参考手册的第 6 章。)

    通过继承选择方法

    您可以非常轻松地将上一节中学到的知识应用于通过继承创建的类层次结构。DefaultTournamentMatchManager 类实现了 TournamentMatchManager 接口,因此是该接口层次结构的一部分。我们可以重写上一节中显示的切点,以匹配类层次结构中的所有成员,而不是一个特定的类,如下所示:

    execution(* com.apress.springbook.chapter04.TournamentMatchManager+.*(..)) 
    within(com.apress.springbook.chapter04.TournamentMatchManager+) 
    

    这些切点将选择所有与 TournamentMatchManager 接口类型兼容的对象。请注意使用 + 运算符来告诉 Spring AOP 考虑类层次结构。

    选择包中所有类的方法

    将连接点选择范围缩小到包中的所有类非常容易,并且您可以选择包含所有子包。要选择 com.apress.springbook.chapter04 包中所有类上的所有方法,只需使用星号 (*) 运算符即可:

    execution(* com.apress.springbook.chapter04.*.*(..)) 
    within(com.apress.springbook.chapter04.*) 
    

    如果您想包含所有子包,则需要额外添加一个点 (..)。将前面的切点与以下切点进行比较:

    execution(*com.apress.springbook.chapter04..*.*(..)) 
    within(com.apress.springbook.chapter04..*) 
    

    在包级别缩小范围有助于您选择特定的方法。

    通过注解选择方法

    选择包中所有类上的所有方法可能对于某些切面来说粒度太粗。例如,在审计方面,您会提前知道哪些类上的哪些方法需要保留审计信息。

    在这里,我们将使用审计的例子来演示如何选择带有注解的方法以及带有注解的类中的方法。在本例中,我们将使用列表 4-45 中显示的 `@Audit` 注解。

    列表 4-45.`@Audit` 注解

    package com.apress.springbook.chapter04; 
    
    import java.lang.annotation.Retention; 
    import java.lang.annotation.RetentionPolicy; 
    
    @Retention(RetentionPolicy.RUNTIME) 
    public @interface Audit { 
      } 
    
    我们现在可以在 BusinessOperations 类上使用此注解,如列表 4-46 所示。

    列表 4-46.使用 `@Audit` 注解标记需要保留审计信息的方法

    package com.apress.springbook.chapter04; 
    
    public class BusinessOperations { 
      @Audit   
      public void sensitiveOperation(long recordId) { …   } 
      } 
    
    通过在 `sensitiveOperation()` 方法上声明 `@Audit` 注解,我们表明需要为该方法保留审计信息。但是,我们还没有确定要保留哪些信息,在哪里存储这些信息,以及存储多久。

    要存储的信息以及存储方式很可能在策略文档中定义,并且对于所有方法都是相同的。我们只标记了一个方法,现在可以用切面来实现保留策略。

    按方法注解声明选择

    我们需要编写一个实现审计保留策略的切面。选择哪些方法需要进行审计信息保留是独立于切面或实现它的开发人员的工作。因为我们可以基于 Java 5 注解来构建切点,所以我们可以获得细粒度的语义。

    我们将只选择声明了 `@Audit` 注解的方法,而让其他方法不受影响。这是一种很好的工作方式,因为我们可以确定不会有副作用。我们将实际的审计信息保存委托给 AuditInformationRetentionPolicy 接口,如列表 4-47 所示。

    列表 4-47.AuditInformationRetentionPolicy 接口

    package com.apress.springbook.chapter04; 
    
    public interface AuditInformationRetentionPolicy { 
      public void retainMethodInvocationInformation( 
        String currentUser, String methodDescription, Object[] arguments); 
    } 
    
    您会注意到,AuditInformationRetentionPolicy 接口上的 `retainMethodInvocationInformation()` 方法需要当前用户的姓名。我们需要获取这个姓名,但是我们

    不希望将我们切面的实现与特定的身份验证机制绑定。我们创建了 CurrentUserInformation 接口,如列表 4-48 所示。

    列表 4-48.CurrentUserInformation 接口

    package com.apress.springbook.chapter04; 
    
    public interface CurrentUserInformation { 
      public String getUsername(); 
        } 
    
    现在我们知道了如何保留审计信息以及如何获取当前用户名。由于我们将这两项职责委托给了协作对象,因此我们需要在 Spring 容器中配置我们的切面。

    第一步是在 SystemPointcutsAspect 中添加一个切点,因为我们希望集中系统范围的切点,如列表 4-49 所示。

    列表 4-49.添加系统范围的切点

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("@annotation(com.apress.springbook.chapter04.Audit)")   
      public void auditableMethods() {  }  
    } 
    
    列表 4-49 中的切点使用 @annotation() 切点指示符来选择已声明 `@Audit` 注解的连接点(我们将在本章稍后的“绑定注解”部分更详细地讨论 @annotation() 切点指示符)。Spring AOP 现在可以在自动代理创建期间只选择 Spring 容器中的那些 bean。

    请注意,我们不再选择特定的方法、类或包。如果需要,我们显然可以进一步缩小切点的选择范围。

    列表 4-50 显示了 AuditInformationRetentionAspect,它负责捕获所有被标记为 `@Audit` 注解的方法的执行,并调用 AuditInformationRetentionPolicy 接口上的 retainMethodInvocationInformation()。

    列表 4-50.AuditInformationRetentionAspect 负责为敏感操作保存审计信息

    package com.apress.springbook.chapter04.aspects; 
    
    import com.apress.springbook.chapter04.CurrentUserInformation; 
    import com.apress.springbook.chapter04.AuditInformationRetentionPolicy; 
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 
    import org.aspectj.lang.JoinPoint; 
    
    
    @Aspect 
    public class AuditInformationRetentionAspect { 
      
    
    private AuditInformationRetentionPolicy auditInformationRetentionPolicy; 
    private CurrentUserInformation currentUserInformation;   
      public void setAuditInformationRetentionPolicy( 
      AuditInformationRetentionPolicy auditInformationRetentionPolicy 
    ) { 
       this.auditInformationRetentionPolicy = auditInformationRetentionPolicy; 
      } 
    
      public void setCurrentUserInformation 
       ( 
       CurrentUserInformation currentUserInformation 
       ) { 
          this.currentUserInformation = currentUserInformation; 
      } 
      
      public void init() { 
        if (this.auditInformationRetentionPolicy == null) { 
          throw new IllegalStateException("AuditInformationRetentionPolicy " + 
            "object is not set!"); 
        } 
        if (this.currentUserInformation == null) { 
          throw new IllegalStateException("CurrentUserInformation " + 
            "object is not set!"); 
        } 
      } 
      @Before("com.apress.springbook.chapter04.aspects." + 
      "SystemPointcutsAspect.auditableMethods()")   
        public void retainMethodInvocationInformation(JoinPoint joinPoint) { 
          String currentUser = this.currentUserInformation.getUsername(); 
          String methodDescription = joinPoint.getSignature().toLongString(); 
          Object[] arguments = joinPoint.getArgs(); 
    
          this.auditInformationRetentionPolicy.retainMethodInvocationInformation( 
          currentUser, methodDescription, arguments); 
        } 
    } 
    
    在列表 4-50 中,`retainMethodInvocationInformation()` 通知方法有一个 JoinPoint 参数。除 around 通知外的任何通知方法都可以将 JoinPoint 作为其第一个参数。如果声明了此参数,Spring AOP 将传递一个 JoinPoint 对象,其中包含有关当前连接点的信息。

    列表 4-50 中的切面使用 org.aspectj.lang.JoinPoint 对象来获取已执行方法的描述以及已传递的参数。

    列表 4-51 显示了 BusinessOperations 类和 AuditInformationRetentionAspect 切面的 Spring XML 配置文件。

    列表 4-51.配置可审计类和审计切面

    <beans> 
      <bean class="org.springframework.aop.aspectj.annotation. --> 
    AnnotationAwareAspectJAutoProxyCreator"/> 
    
      <bean class="com.apress.springbook.chapter04.aspects. --> 
    AuditInformationRetentionAspect" 
      init-method="init"> 
    
      <property name="auditInformationRetentionPolicy" 
                   ref="auditInformationRetentionPolicy"/> 
      <property name="currentUserInformation" ref="currentUserInformation"/> 
    </bean> 
    
      <bean id="businessOperations" 
        class="com.apress.springbook.chapter04.BusinessOperations"> 
        <!-- properties omitted --> 
      </bean> 
    </beans> 
    
    列表 4-51 中的 Spring 配置使用 AuditInformationRetentionPolicy 和 CurrentUserInformation 对象配置了 AuditInformationRetentionAspect。这些接口的实现方式对于此示例和切面来说并不重要。切面将这两项职责委托出去,使其通知实现小巧、易于维护且易于实现。

    按类注解声明选择

    上一节中的示例详细说明了如何匹配带有 @Audit 声明的方法。

    有时您还希望用注解标记整个类,为该类中的所有方法强制执行策略。当类的所有职责都需要强制执行策略(如审计)时,这是一种可行的方法。不需要这些策略的方法因此不应放在此类中。

    虽然这是一个有趣的方法,但您需要考虑一个后果。如果您还记得,在 Spring AOP 中使用切面工作的要求之一是调用者使用代理对象,而不是目标对象。

    考虑列表 4-52 中的 MoreBusinessOperations 类。

    列表 4-52.MoreBusinessOperations 类

    package com.apress.springbook.chapter04; 
    
    @Audit 
    public class MoreBusinessOperations { 
      public (.*?) { 
          // do some work 
         someOtherSensitiveOperation(recordId); 
    } 
      
      public void someOtherSensitiveOperation(long recordId) { 
        // work with sensitive data 
      } 
    } 
    
    在列表 4-52 中,`someSensitiveOperation()` 方法调用了同一对象上的 `someOtherSensitiveOperation()` 方法。但是,此对象是目标对象——原始对象,可能已经创建了代理对象。

    是否通过代理对象调用了 `someSensitiveOperation()` 方法并不重要。此方法调用同一对象上的另一个方法,因此该方法调用不会通过代理对象。这意味着 `someOtherSensitiveOperation()` 不会强制执行审计策略。虽然这听起来很严重,但我们需要考虑在执行 `someSensitiveOperation()` 时保存审计信息是否足以覆盖这两个方法调用。如果不足够,那么两个都需要审计信息保留的方法,一个被另一个调用,是否应该放在同一个类中?将这两个方法放在同一个类中可能会使该类承担过多的职责。

    如果 `someOtherSensitiveOperation()` 方法始终需要保存审计信息,那么它可能应该移到一个单独的类中。然后可以将该类的对象注入到 MoreBusinessOperations 对象中,该对象随后可以调用注入对象上的 `someOtherSensitiveOperation()` 方法。

    关于解决此类应用程序设计与 Spring AOP 等技术框架所施加的限制之间的冲突,没有硬性规定。我们建议您运用常识,并考虑类、方法和要强制执行的策略来找到解决方案。

    要匹配声明了 `@Audit` 注解的类中的方法,我们需要更改 SystemPointcutsAspect 中的 auditableMethods() 切点,如列表 4-53 所示。

    列表 4-53.也匹配声明了 `@Audit` 注解的类中的方法

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("@within(com.apress.springbook.chapter04.Audit)") 
      public void auditableMethods() {  } 
    } 
    
    列表 4-53 中的 `auditableMethods()` 切点使用的 @within() 切点指示符也匹配声明了 `@Audit` 注解的类中声明的方法。换句话说,@within() 切点指示符只匹配类级别的 `@Audit` 注解。

    现在我们已经涵盖了如何使用切点匹配 Java 5 注解,让我们来看看如何绑定通知。

    绑定通知参数

    我们将把迄今为止学到的切点往前推进几步。现在,我们将为通知方法添加参数。这被称为参数绑定,它允许您创建功能更强大的通知方法。您可以向通知方法添加参数,并绑定任何方法参数值、异常、返回值或注解对象。

    让我们从本章开头示例中的切面开始,如列表 4-54 所示。

    列表 4-54.在网球比赛开始时打印一条消息

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
      @Before("execution(* startMatch(..))") 
      public void printMessageToInformMatchStarts() { 
          System.out.println("Attempting to start tennis match!"); 
      } 
    } 
    

    列表 4-54 中的 `printMessageToInformMatchStarts()` 通知方法没有参数。

    让我们再次看看 TournamentMatchManager 接口及其 `startMatch()` 方法,如列表 4-55 所示。

    列表 4-55.TournamentMatchManager 接口

    package com.apress.springbook.chapter04; 
    
    public interface TournamentMatchManager { 
      public Match startMatch(long matchId) throws 
        UnknownMatchException, MatchIsFinishedException, 
        MatchCannotBePlayedException, PreviousMatchesNotFinishedException; 
    } 
    
    假设我们想在 printMessageToInformMatchStarts() 通知方法中打印传递给 `startMatch()` 的比赛标识符。我们需要以某种方式获取参数。我们之前已经使用过 JoinPoint 参数,并且可以在这里再次使用它来获取 `startMatch()` 方法的 match identifier 参数值,如列表 4-56 所示。

    列表 4-56.通过 JoinPoint 对象获取参数值

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 
    import org.aspectj.lang.JoinPoint; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
      @Before("execution(* startMatch(..))") 
      public void printMessageToInformMatchStarts(JoinPoint jp) { 
      Long matchId = (Long)jp.getArgs()[0]; 
      System.out.println("Attempting to start tennis match with identifier " + 
          matchId + "!"); 
      } 
    } 
    
    Spring AOP 将检测到 `printMessageToInformMatchStarts()` 通知方法将 JoinPoint 作为其第一个参数类型,并将传递一个 JoinPoint 对象,该对象有一个 Object 数组,其中包含方法调用的参数值。

    所以我们现在有了比赛标识符,但是有一个问题:必须更改切点表达式以避免错误。当前的切点表达式将选择任何 startMatch() 方法,而不管其参数类型。我们需要更改切点以仅选择第一个参数类型为 long 的方法,如列表 4-57 所示,因为通知方法假定它存在。

    列表 4-57.更改切点表达式以进一步缩小方法选择范围

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 
    import org.aspectj.lang.JoinPoint; 
    

    @Aspect public class MessagePrintingAspect { @Before("execution(* startMatch(long,..))") public void printMessageToInformMatchStarts(JoinPoint jp) { Long matchId = (Long)jp.getArgs()[0]; System.out.println("Attempting to start tennis match with identifier " + matchId + "!"); } } 这更好,但列表 4-57 仍可改进。Spring AOP 允许我们在 `printMessageToInformMatchStarts()` 通知方法中声明一个 long 参数,而不是必须使用 JoinPoint 对象,如下节所示。

    Spring AOP 支持以下通知参数绑定:

    • 绑定值,对所有通知类型均有效
    • 绑定返回值,仅对 after returning 通知有效
    • 绑定异常对象,仅对 after throwing 通知有效
    • 绑定注解对象,对所有通知类型均有效

    您需要将 Java 编译器配置为在 Java 类中包含调试信息,以使参数绑定能够正常工作。通常,IDE 中的编译器默认会这样配置。有关更多详细信息,请参阅 Spring 2.0 参考手册的第 6 章。

    绑定方法参数值

    您可以将传递给代理对象上方法执行的参数值绑定到通知方法的参数。绑定方法参数值对所有通知类型都有效。

    对于示例,我们想将比赛标识符值绑定到 printMessageToInformMatchStarts() 通知方法的参数,因此我们首先需要向该方法添加一个参数。

    但是,我们还需要使用 `args()` 切点指示符来指定我们想要绑定方法参数。我们已按列表 4-58 所示更改了 MessagePrintingAspect。

    列表 4-58.将比赛标识符值绑定到通知方法

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 
    import org.aspectj.lang.JoinPoint; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
    
      @Before("execution(* startMatch(..)) && args(matchId, ..)")   
      public void printMessageToInformMatchStarts(long matchId) { 
        System.out.println("Attempting to start tennis match with identifier " + 
          matchId + "!"); 
      } 
    } 
    

    列表 4-58 中的切点告诉 Spring AOP 将连接点的第一个参数值绑定到 `printMessageToInformMatchStarts()` 通知方法的唯一参数。当执行此通知方法时,其参数将包含传递给代理对象上 `startMatch()` 方法执行的值。

    请注意列表 4-58 中的切点和通知方法:

    • 我们保留了 `execution()` 切点指示符中的静态参数选择。请记住,`execution()` 使用静态方法签名信息,而 `args()` 需要动态切点。为了避免在自动代理创建时为太多可以匹配切点的 bean 创建代理对象,我们添加了尽可能多的静态条件。
    • `printMessageToInformMatchStarts()` 通知方法无法更改比赛标识符的值。要更改参数值,必须使用 JoinPoint 对象。
    • 向 `printMessageToInformMatchStarts()` 通知方法添加参数时,此参数必须由切点绑定,因此我们必须添加 `args()` 切点指示符。

    当我们添加更多参数时,我们需要更改切点,以便这些额外的参数也能被绑定。`args()` 切点指示符中使用的名称必须与通知方法参数中的参数名称匹配。

    要在 XML 中执行此操作,请将以下内容添加到您的配置文件中:

    <aop:aspect ref="messagePrinter"> 
      <aop:before method="printMessageToInformMatchStarts" 
                  arg-names="matchId" 
                  pointcut="execution(* startMatch(..)) && args(matchId, ..)"/> 
    </aop:aspect>
    

    绑定返回值

    您还可以将返回值绑定到通知方法的参数,但这仅对 after returning 通知有效。列表 4-59 显示了 MessagePrintingAspect,它获取对 `startMatch()` 方法返回值的访问权限。

    列表 4-59.获取返回值

    package com.apress.springbook.chapter04.aspects; 
    
    import com.apress.springbook.chapter04.Match; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.AfterReturning; 
    
    import org.aspectj.lang.JoinPoint; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
      @AfterReturning( 
        value = "execution(com.apress.springbook.chapter04.Match" + 
            " startMatch(..))", 
        returning = "match" 
    )   
      public void printMessageToInformMatchHasStarted(Match match) { 
        System.out.println("This match has been started: " + match); 
      } 
    } 
    

    绑定返回值是一种特殊的静态切点。Spring AOP 不需要进行任何运行时匹配,但它需要将返回值作为参数传递给 printMessageToInformMatchHasStarted() 通知方法。请注意,我们在 `execution()` 切点指示符中提供了返回类型作为一种安全措施,以便通知方法只为具有正确返回类型的 `startMatch()` 方法执行。

    我们在列表 4-59 的 `@AfterReturning` 注解中指定了 returning 属性,以表明我们希望将返回值绑定到通知方法。传递给 returning 属性的值是我们想要绑定的参数名称。要在 XML 中执行此操作,请添加 returning 属性:

    <aop:aspect ref="messagePrinter"> 
      <aop:after-returning method="printMessageToInformMatchHasStarted" 
                      returning="match" 
                      pointcut="execution(* startMatch(..))"/> 
    </aop:aspect>
    

    绑定异常

    当发生异常时,您也可以将此对象作为参数绑定到通知方法,但前提是您使用的是 after throwing 通知。列表 4-60 显示了 MessagePrintingAspect,它获取对 DefaultTournamentMatchManager 上 `startMatch()` 方法抛出的异常类型的访问权限。

    列表 4-60.获取 `startMatch()` 方法抛出的异常

    package com.apress.springbook.chapter04.aspects; 
    
    import com.apress.springbook.chapter04.UnknownMatchException; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.AfterThrowing; 
    
    import org.aspectj.lang.JoinPoint; 
    
    
    @Aspect 
    public class MessagePrintingAspect { 
      @AfterThrowing( 
        value = "execution(* startMatch(..) throws " + 
          "com.apress.springbook.chapter04." + 
          "UnknownMatchException)", 
        throwing = "exception" 
      )   
      public void printMessageWhenMatchIdentifierIsNotFound( 
      UnknownMatchException exception) { 
        System.out.println("No match found for match identifier " + 
        exception.getInvalidMatchIdentifier() + "!"); 
      } 
    } 
    
    列表 4-60 中的切点仅使用 `execution()` 切点指示符,这意味着它仅使用静态方法签名信息在自动代理创建时匹配连接点。当 `startMatch()` 方法抛出 UnknownMatchException 类型时,将执行 printMessageWhenMatchIdentifierIsNotFound() 通知方法。

    但是,如果 `startMatch()` 方法抛出其他异常类型会怎样?

    除了 UnknownMatchException 之外,`startMatch()` 还声明了三种其他异常类型,并且还可以抛出任何运行时异常。

    只有当抛出其唯一参数中声明的异常类型时,`printMessageWhenMatchIdentifierIsNotFound()` 通知方法才会执行;否则,通知根本不会执行。这使我们可以添加更多 @AfterThrowing 通知来处理特定的异常类型。我们不一定需要使用异常对象,但通过将其绑定到通知方法,Spring AOP 可以选择正确的通知。

    但是,请注意,列表 4-60 中的切点不是动态切点。Spring AOP 将根据静态 `execution()` 切点指示符匹配连接点。当抛出异常时,Spring AOP 将根据绑定信息为每个通知决定是否需要执行。这意味着静态切点必须足够严格,以仅选择实际可能抛出异常的方法。不够严格的切点会在自动代理创建期间触发为过多 bean 创建代理对象。

    绑定异常时,`@AfterThrowing` 注解上的 throwing 属性必须存在,并且其值应为通知方法中声明的异常参数的名称。

    要在 XML 中使用异常进行绑定,您将执行以下操作:

    <aop:aspect ref="messagePrinter"> 
      <aop:after-throwing method="printMessageWhenMatchIdentifierIsNotFound" 
                         throwing="exception" 
                         pointcut="execution(* startMatch(..) throws --> 
                         com.apress.springbook.chapter04.UnknownMatchException)"/> 
    </aop:aspect>
    

    绑定注解

    由于注解被添加到类的字节码中,并且是类和方法声明的一部分,因此它们是静态且不可变的。这意味着 Spring AOP 可以使用静态(非动态)切点来获取注解。

    我们将继续研究将注解对象绑定到来自两个位置的通知方法参数:

    • 声明在方法上的注解
    • 声明在类上的注解 在本节中,我们将扩展我们在本章前面介绍注解时使用的 @Audit 示例。首先,我们将为 `@Audit` 注解添加一个属性,如列表 4-61 所示。

    列表 4-61.向 `@Audit` 注解添加属性

    package com.apress.springbook.chapter04; 
    
    import java.lang.annotation.Retention; 
    import java.lang.annotation.RetentionPolicy; 
    
    @Retention(RetentionPolicy.RUNTIME) 
    public @interface Audit { 
      String value() default ""; 
    } 
    

    通过向 `@Audit` 注解添加值,我们可以传递特定信息,在保留审计信息时可以使用。接下来,我们将更改 SystemPointcutsAdvice 上的 auditableMethods() 切点声明以进行参数绑定,如列表 4-62 所示。

    列表 4-62.更改 `auditableMethods()` 切点声明以进行参数绑定

    package com.apress.springbook.chapter04.aspects; 
    
    import com.apress.springbook.chapter04.Audit; 
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("@annotation(audit)") 
      public void auditableMethods(Audit audit) {  } 
    } 
    
    我们已将切点声明更改为支持绑定声明在方法执行连接点上的 `@Audit` 注解对象。@annotation() 切点指示符使用 auditableMethods() 方法中审计参数的名称。@annotation() 使用变量名而不是类型,以指定我们想要绑定一个注解对象。auditableMethods() 声明一个 Audit 类型的参数来告诉 Spring AOP,此切点只选择 `@Audit` 注解。

    列表 4-63 显示了带有 `@Audit` 注解的 BusinessOperations 类。

    列表 4-63.声明带有附加信息的 `@Audit` 注解

    package com.apress.springbook.chapter04; 
    
    public class BusinessOperations { 
      @Audit("top secret") 
      public void sensitiveOperation(long recordId) { …   } 
    } 
    
    我们需要对 AuditInformationRetentionAspect 切面进行少量修改(与列表 4-50 相比),以启用声明在连接点上的注解对象的参数绑定,如列表 4-64 所示。

    列表 4-64.绑定声明在对象上的注解对象

    package com.apress.springbook.chapter04.aspects; 
    
    import com.apress.springbook.chapter04.Audit; 
    import com.apress.springbook.chapter04.CurrentUserInformation; 
    import com.apress.springbook.chapter04.AuditInformationRetentionPolicy; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 
    
    import org.aspectj.lang.JoinPoint; 
    
    @Aspect 
    public class AuditInformationRetentionAspect { 
      private AuditInformationRetentionPolicy auditInformationRetentionPolicy; 
      private CurrentUserInformation currentUserInformation;   
    
      public void setAuditInformationRetentionPolicy( 
        AuditInformationRetentionPolicy auditInformationRetentionPolicy) { 
        this.auditInformationRetentionPolicy = auditInformationRetentionPolicy; 
      } 
      
      public void setCurrentUserInformation( 
        CurrentUserInformation currentUserInformation) { 
        this.currentUserInformation = currentUserInformation; 
      } 
      
      public void init() { 
        if (this.auditInformationRetentionPolicy == null) { 
        throw new IllegalStateException("AuditInformationRetentionPolicy " + 
        "object is not set!"); 
      } 
    
      if (this.currentUserInformation == null) { 
        throw new IllegalStateException("CurrentUserInformation " + 
        "object is not set!"); 
      } 
    } 
    
    @Before("com.apress.springbook.chapter04.aspects." + 
        "SystemPointcutsAspect.auditableMethods(audit)")   
      public void retainMethodInvocationInformation(JoinPoint joinPoint, Audit audit) { 
        String currentUser = this.currentUserInformation.getUsername(); 
        String methodDescription = audit.value() + ":" + 
           joinPoint.getSignature().toLongString(); 
        Object[] arguments = joinPoint.getArgs(); 
    
        this.auditInformationRetentionPolicy.retainMethodInvocationInformation( 
          currentUser, methodDescription, arguments); 
      } 
    } 
    
    SystemPointcutsAspect 上的 auditableMethods() 切点使用 retainMethodInvocationInformation() 通知方法中的参数名称。请注意,此通知方法仍将 JoinPoint 参数作为其第一个参数,并将 audit 注解类型作为第二个参数。

    JoinPoint 参数由 Spring AOP 自动绑定,auditableMethods() 调用 @annotation() 来绑定 Audit 注解参数。

    由于切点没有指定注解类型,Spring AOP 会决定 @annotation() 中的参数是通过名称引用的。这样,`retainMethodInvocationInformation()` 通知方法只针对声明了 `@Audit` 注解的连接点执行。

    请注意,列表 4-64 中的切点没有重用另一个切点。切点的重用不支持将参数绑定到通知方法。

    我们还可以使用 @within() 切点指示符将声明在类上的注解绑定到通知方法。再次考虑 MoreBusinessOperations 类,如列表 4-65 所示。

    列表 4-65.MoreBusinessOperations 类现被classified 为绝密

    package com.apress.springbook.chapter04; 
    
    @Audit("top secret") 
    public class MoreBusinessOperations { 
      public (.*?) { 
          // do some work 
        someOtherSensitiveOperation(recordId); 
    } 
      
      public void someOtherSensitiveOperation(long recordId) { 
        // work with sensitive data 
      } 
    } 
    
    获取 MoreBusinessOperations 类上的 `@Audit` 注解对象并将其绑定到 retainMethodInvocationInformation() 通知方法需要对 SystemPointcuts Aspect 进行更改,如列表 4-66 所示。

    列表 4-66.SystemPointcutsAspect 选择类上的 `@Audit` 注解

    package com.apress.springbook.chapter04.aspects; 
    
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Pointcut; 
    
    
    @Aspect 
    public class SystemPointcutsAspect { 
      @Pointcut("@within(audit)") 
      public void auditableMethods(Audit audit) {  } 
    } 
    
    同样,@within() 切点指示符匹配类级别的 `@Audit` 注解声明。另请注意,未使用动态切点,因为 @within() 可以在自动代理创建期间根据静态方法签名信息匹配连接点。

    能够创建自己的注解,注解类和方法,并将注解对象绑定到通知方法参数,这是 Spring AOP 的一个非常有趣的功能。有关绑定通知参数以及其他 Spring AOP 主题的更多详细信息,请参阅 Spring 参考手册的第 6 章。

    摘要

    Spring 框架的流行部分归功于 Spring AOP 以及框架其他部分如何使用它。在 Spring 2.0 中,通过引入切面和 AspectJ 切点语言,Spring AOP 与 Spring 容器之间已有的出色集成得到了改进。

    AspectJ 切面是丰富、细粒度且功能强大的构造,允许您更改和增强 Java 类的行为。本章涵盖了您在应用程序中开始使用新的 Spring AOP 功能和切面所需的所有内容。通过利用 @AspectJ 风格的注解,Spring 支持 Java 5,并通过利用 AOP XML Schema 支持旧版 Java。下一章将介绍 Spring 框架的数据访问。


    本示例章节摘录自书籍《Building Spring 2 Enterprise Applications》,作者:Interface 21, Bram Smeets, Seth Ladd,版权所有 2007,Apress, Inc.

    本书的源代码可供读者在 http://www.apress.com/book/view/1590599187 获取。

    Interface21 是一家私营的国际公司,由致力于构建、使用和培训世界上最流行的应用程序框架的顾问组成。Springframework.com 由 Interface21 Limited(一家私营公司)运营。带来 Spring 框架的开发者也提供培训、咨询和支持。Interface21 在欧洲和北美也有合作伙伴,与重视高质量服务的公司合作,这些公司拥有 Spring 经验,并与提供 .NET 服务的开发人员合作。Interface21 希望成为您企业开发的首选合作伙伴。

    Seth Ladd 是一名软件工程师和专业的 Spring Framework 培训师和导师,专注于面向对象和可测试的 Web 应用程序。他 17 岁时就创办了自己的公司来构建网站,但现在喜欢拥有一份真正的工作。Seth 目前在 Camber Corporation 工作,为 NEC、罗切斯特理工学院、Brivo Systems 和 National Information Consortium 构建和部署了系统。他为服务器和远程连接的嵌入式设备架构和开发了 Java 和 C 语言的企业应用程序。他喜欢演讲和教学,并且是本地 Java 用户组和公司开发者会议的常客。Seth 非常感谢能与妻子一起在夏威夷凯卢阿生活和工作。

    Bram Smeets 是一名 Java 架构师,拥有超过 8 年的企业 Java 应用程序开发经验。目前,Bram 是 JTeam (www.jteam.nl) 的技术总监,JTeam 是一家总部位于荷兰的 Java 软件开发公司,也是 SpringSource (www.springsource.com) 的高级顾问。他是 Ajax Experience 和 SpringOne 等技术焦点会议的常客。使用 GWT,Bram 在 JTeam 交付了多个成功的 RIA 项目。他还为多家公司提供了 Ajax 和 GWT 培训。

  • © . All rights reserved.