Spring5-高级教程-三-

61 阅读1小时+

Spring5 高级教程(三)

原文:Pro Spring 5

协议:CC BY-NC-SA 4.0

五、Spring AOP 简介

除了依赖注入(DI),Spring 框架提供的另一个核心特性是支持面向方面编程(AOP)。AOP 通常被称为实现横切关注点的工具。术语横切关注点指的是应用中不能从应用的其余部分分解的逻辑,它可能导致代码重复和紧密耦合。通过使用 AOP 将逻辑的各个部分模块化,称为关注点,您可以将它们应用到应用的许多部分,而无需复制代码或创建硬依赖。日志和安全性是许多应用中都存在的横切关注点的典型例子。考虑一个为调试目的记录每个方法的开始和结束的应用。您可能会将日志记录代码重构为一个特殊的类,但是为了执行日志记录,您仍然需要对应用中的每个方法调用该类上的方法两次。使用 AOP,您可以简单地指定您希望在应用中的每个方法调用之前和之后调用日志记录类上的方法。

重要的是要理解 AOP 是对面向对象编程(OOP)的补充,而不是与之竞争。OOP 非常擅长解决我们作为程序员遇到的各种各样的问题。然而,如果您再次查看日志记录示例,就可以明显看出在大规模实现横切逻辑时,OOP 的不足之处。考虑到 AOP 是在 OOP 之上运行的,单独使用 AOP 来开发一个完整的应用实际上是不可能的。同样,尽管使用 OOP 来开发整个应用肯定是可能的,但是通过使用 AOP 来解决某些涉及横切逻辑的问题,您可以更聪明地工作。

本章涵盖以下主题:

  • AOP 基础知识:在讨论 Spring 的 AOP 实现之前,我们先介绍 AOP 作为一种技术的基础知识。“AOP 概念”一节中涉及的大多数概念并不特定于 Spring,可以在任何 AOP 实现中找到。如果您已经熟悉了另一个 AOP 实现,可以跳过“AOP 概念”部分。
  • AOP 的类型:有两种不同类型的 AOP:静态和动态。在静态 AOP 中,就像 AspectJ 的 1 编译时编织机制所提供的那样,横切逻辑在编译时应用于你的代码,如果不修改代码并重新编译,你就不能改变它。对于动态 AOP,比如 Spring AOP,横切逻辑是在运行时动态应用的。这允许您对 AOP 配置进行更改,而不必重新编译应用。这些类型的 AOP 是互补的,当一起使用时,它们形成了一个强大的组合,可以在您的应用中使用。
  • Spring AOP 架构:Spring AOP 只是其他实现(如 AspectJ)中完整 AOP 特性集的一个子集。在这一章中,我们将从较高的层次来看 Spring 中有哪些特性,它们是如何实现的,以及为什么有些特性被排除在 Spring 实现之外。
  • Spring AOP 中的代理:代理是 Spring AOP 工作方式的重要组成部分,您必须理解它们才能充分利用 Spring AOP。在这一章中,我们来看两种代理:JDK 动态代理和 CGLIB 代理。具体来说,我们看一下 Spring 使用每种代理的不同场景,两种代理类型的性能,以及在应用中要从 Spring AOP 中获得最大收益需要遵循的一些简单准则。
  • 使用 Spring AOP:在这一章中,我们给出了一些 AOP 使用的实例。我们从一个简单的 Hello World 示例开始,让您更容易理解 Spring 的 AOP 代码,然后我们继续用示例详细描述 Spring 中可用的 AOP 特性。
  • 切入点的高级使用:我们探索了在应用中使用切入点时应该使用的ComposablePointcutControlFlowPointcut类、介绍和适当的技术。
  • AOP 框架服务:Spring 框架完全支持透明地和声明性地配置 AOP。我们看三种方式(ProxyFactoryBean类、aop命名空间和@AspectJ-样式注释)将声明定义的 AOP 代理作为协作者注入到您的应用对象中,从而使您的应用完全不知道它正在与被通知的对象一起工作。
  • 集成 AspectJ: AspectJ 是一个全功能的 AOP 实现。AspectJ 和 Spring AOP 的主要区别在于 AspectJ 通过编织(编译时或加载时编织)将通知应用于目标对象,而 Spring AOP 是基于代理的。AspectJ 的特性集比 Spring AOP 大很多,但是用起来比 Spring 复杂很多。当你发现 Spring AOP 缺少一个你需要的特性时,AspectJ 是一个很好的解决方案。

AOP 概念

和大多数技术一样,AOP 有自己的一套特定的概念和术语,理解它们的含义很重要。以下是 AOP 的核心概念:

  • 连接点:连接点是应用执行过程中定义明确的点。连接点的典型例子包括方法调用、方法调用本身、类初始化和对象实例化。连接点是 AOP 的核心概念,它定义了应用中可以使用 AOP 插入额外逻辑的点。
  • 建议:在特定连接点执行的代码是建议,由类中的方法定义。有许多类型的通知,比如 before,它在连接点之前执行,after,它在连接点之后执行。
  • 切入点:切入点是连接点的集合,用于定义何时应该执行通知。通过创建切入点,您可以更好地控制如何将建议应用到应用的组件中。如前所述,典型的连接点是方法调用,或者特定类中所有方法调用的集合。通常,您可以在复杂的关系中组合切入点,以进一步限制何时执行建议。
  • 方面:方面是封装在类中的通知和切入点的组合。这种组合定义了应该包含在应用中的逻辑以及应该在哪里执行。
  • 编织:这是在适当的时候将方面插入到应用代码中的过程。对于编译时 AOP 解决方案,这种编织通常在构建时完成。同样,对于运行时 AOP 解决方案,编织过程是在运行时动态执行的。AspectJ 支持另一种称为加载时编织(load-time weaving,LTW)的编织机制,它拦截底层 JVM 类加载器,并在类加载器加载字节码时为字节码提供编织。
  • 目标:其执行流被 AOP 过程修改的对象被称为目标对象。您经常会看到目标对象被称为建议对象。
  • 简介:这是一个过程,通过这个过程,您可以通过向对象中引入额外的方法或字段来修改对象的结构。您可以使用 introduction AOP 使任何对象实现特定的接口,而不需要该对象的类显式地实现该接口。

如果你发现这些概念令人困惑,不要担心;当你看到一些例子时,这一切就会变得清楚了。此外,请注意,在 Spring AOP 中,许多这些概念都是屏蔽的,而且由于 Spring 对实现的选择,有些概念是不相关的。在本章中,我们将在 Spring 的上下文中讨论这些特性。

AOP 的类型

正如我们前面提到的,有两种不同类型的 AOP:静态和动态。它们之间的区别实际上是编织过程发生的点以及这个过程是如何实现的。

使用静态 AOP

在静态 AOP 中,编织过程构成了应用构建过程中的另一个步骤。用 Java 术语来说,通过修改应用的实际字节码,根据需要更改和扩展应用代码,可以在静态 AOP 实现中实现编织过程。这是实现编织过程的一种很好的方式,因为最终结果只是 Java 字节码,并且在运行时不需要执行任何特殊的技巧来决定何时应该执行通知。这种机制的缺点是,对方面的任何修改,即使只是想添加另一个连接点,也需要重新编译整个应用。AspectJ 的编译时编织是静态 AOP 实现的一个很好的例子。

使用动态 AOP

动态 AOP 实现(如 Spring AOP)与静态 AOP 实现的不同之处在于编织过程是在运行时动态执行的。这是如何实现的取决于实现,但是正如您将看到的,Spring 的方法是为所有被通知的对象创建代理,允许在需要时调用通知。动态 AOP 的缺点是,通常情况下,它的性能不如静态 AOP,但是性能在稳步提高。动态 AOP 实现的主要好处是可以轻松地修改应用的整个方面集,而不需要重新编译主应用代码。

选择 AOP 类型

选择使用静态还是动态 AOP 是一个非常困难的决定。两者都有各自的好处,你不局限于只使用一种类型。总的来说,静态 AOP 实现已经存在了很长时间,并且倾向于拥有更多功能丰富的实现,有更多可用的连接点。通常,如果性能是绝对重要的,或者你需要一个没有在 Spring 中实现的 AOP 特性,你会希望使用 AspectJ。在大多数其他情况下,Spring AOP 是理想的。请记住,Spring 已经为您提供了许多基于 AOP 的解决方案,比如事务管理,所以在推出您自己的解决方案之前,请检查一下框架的功能!和往常一样,让您的应用的需求来驱动您对 AOP 实现的选择,如果技术的组合更适合您的应用,不要将自己局限于单一的实现。一般来说,Spring AOP 没有 AspectJ 复杂,所以它往往是理想的首选。

Spring 的 AOP

Spring 的 AOP 实现可以被视为两个逻辑部分。第一部分是 AOP 核心,它提供完全解耦的、纯编程的 AOP 功能(也称为 Spring AOP API)。AOP 实现的第二部分是一组框架服务,它们使 AOP 更容易在您的应用中使用。在此基础上,Spring 的其他组件,比如事务管理器和 EJB 助手类,提供了基于 AOP 的服务来简化应用的开发。

AOP 联盟

AOP 联盟( http://aopalliance.sourceforge.net/ ))是许多开源 AOP 项目的代表共同努力的结果,它为 AOP 实现定义了一套标准的接口。只要适用,Spring 就使用 AOP 联盟接口,而不是定义自己的接口。这允许您在支持 AOP 联盟接口的多个 AOP 实现之间重用某些建议。

AOP 中的 Hello World

在我们开始详细讨论 Spring AOP 实现之前,让我们看一个例子。我们将看到如何改变典型的 Hello World 示例,我们将从电影中寻找灵感。我们将编写一个名为Agent的类,我们将实现它来打印Bond。当使用 AOP 时,这个类的实例将在运行时被转换以打印James Bond!。下面的代码描述了Agent类:

package com.apress.prospring5.ch5;

public class Agent {
    public void speak() {
        System.out.print("Bond");
    }
}

实现了名称打印方法后,让我们建议——在 AOP 术语中意味着添加建议——这个方法,以便speak()打印James Bond!

为此,我们需要在现有主体执行之前执行代码(编写James),并且我们需要在方法主体执行之后执行代码(编写!)。用 AOP 术语来说,我们需要的是围绕着建议,建议围绕着连接点执行。在这种情况下,连接点是对speak()方法的调用。下面的代码片段显示了AgentDecorator类的代码,它充当 around-advice 实现:

package com.apress.prospring5.ch5;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class AgentDecorator implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.print("James ");

        Object retVal = invocation.proceed();

        System.out.println("!");
        return retVal;
    }
}

MethodInterceptor接口是一个标准的 AOP 联盟接口,用于实现方法调用连接点的 around advice。MethodInvocation对象表示被建议的方法调用,使用这个对象,我们可以控制何时允许方法调用继续进行。因为这是关于建议的,所以我们能够在方法被调用之前以及在方法被调用之后但在它返回之前执行动作。所以在前面的代码片段中,我们简单地将James写入控制台输出,通过调用invocation.proceed()来调用该方法,然后将!写入控制台输出。

该示例的最后一步是将AgentDecorator建议(更具体地说,是invoke()方法)编织到代码中。为此,我们创建一个目标Agent的实例,然后创建这个实例的代理,指示代理工厂织入AgentDecorator通知。如下所示:

package com.apress.prospring5.ch5;

import org.springframework.aop.framework.ProxyFactory;

public class AgentAOPDemo {
    public static void main(String... args) {
        Agent target = new Agent();

        ProxyFactory pf = new ProxyFactory();
        pf.addAdvice(new AgentDecorator());
        pf.setTarget(target);

        Agent proxy = (Agent) pf.getProxy();

        target.speak();
        System.out.println("");
        proxy.speak();
    }
}

这里重要的部分是我们使用ProxyFactory类来创建目标对象的代理,同时织入通知。我们通过调用addAdvice()AgentDecorator建议传递给ProxyFactory,并通过调用setTarget()指定编织的目标。一旦目标被设定,一些建议被添加到ProxyFactory,我们就通过调用getProxy()来生成代理。最后,我们在原始目标对象和代理对象上调用speak()。运行前面的代码会产生以下输出:

Bond
James Bond!

如您所见,在未触及的目标对象上调用speak()会导致标准的方法调用,并且没有额外的内容被写入控制台输出。然而,代理的调用导致执行AgentDecorator中的代码,创建期望的James Bond!输出。从这个例子中,您可以看到 advised 类不依赖于 Spring 或 AOP Alliance 接口;Spring AOP 的美妙之处,实际上也是 AOP 的美妙之处,在于你可以建议几乎任何类,即使这个类是在没有考虑 AOP 的情况下创建的。唯一的限制是,至少在 Spring AOP 中,不能通知 final 类,因为它们不能被覆盖,因此不能被代理。

Spring AOP 架构

Spring AOP 的核心架构是基于代理的。当您想要创建一个类的通知实例时,您必须使用ProxyFactory来创建该类的代理实例,首先向ProxyFactory提供您想要编织到代理中的所有方面。使用ProxyFactory是一种创建 AOP 代理的纯编程方法。在大多数情况下,您不需要在您的应用中使用它;相反,您可以依靠 Spring 提供的声明性 AOP 配置机制(ProxyFactoryBean类、aop名称空间和@AspectJ-样式注释)来利用声明性代理创建。然而,理解代理创建是如何工作的很重要,所以我们将首先展示代理创建的编程方法,然后深入研究 Spring 的声明性 AOP 配置。

在运行时,Spring 分析为ApplicationContext中的 bean 定义的横切关注点,并动态生成代理 bean(包装底层目标 bean)。调用者不是直接调用目标 bean,而是被注入代理 bean。然后,代理 bean 分析运行条件(即连接点、切入点或通知),并相应地织入适当的通知。图 5-1 展示了一个 Spring AOP 代理的高级视图。在内部,Spring 有两个代理实现:JDK 动态代理和 CGLIB 代理。默认情况下,当被通知的目标对象实现一个接口时,Spring 将使用 JDK 动态代理来创建目标的代理实例。然而,当被通知的目标对象没有实现接口时(例如,它是一个具体的类),CGLIB 将用于代理实例的创建。一个主要原因是 JDK 动态代理只支持接口代理。我们将在“理解代理”一节中详细讨论代理

A315511_5_En_5_Fig1_HTML.jpg

图 5-1。

Spring AOP proxy in action

Spring 的连接点

Spring AOP 中一个比较明显的简化是它只支持一种连接点类型:方法调用。乍一看,如果您熟悉其他 AOP 实现,比如 AspectJ,它支持更多的连接点,这似乎是一个严重的限制,但实际上这使得 Spring 更容易访问。

方法调用连接点是迄今为止最有用的连接点,使用它,您可以完成许多使 AOP 在日常编程中有用的任务。请记住,如果您需要在连接点而不是在方法调用上建议一些代码,您总是可以一起使用 Spring 和 AspectJ。

Spring 的景象

在 Spring AOP 中,一个方面由一个实现了Advisor接口的类的实例来表示。Spring 提供了方便的Advisor实现,您可以在您的应用中重用这些实现,这样您就不需要创建定制的Advisor实现了。Advisor有两个子接口:PointcutAdvisorIntroductionAdvisor

所有使用切入点来控制应用于连接点的通知的Advisor实现都实现了PointcutAdvisor接口。在 Spring 中,介绍被视为特殊类型的建议,通过使用IntroductionAdvisor接口,您可以控制介绍适用于哪些类。

我们将在下一节“Spring 中的顾问和切入点”中详细讨论PointcutAdvisor实现

关于 ProxyFactory 类

ProxyFactory类控制 Spring AOP 中的编织和代理创建过程。在创建代理之前,必须指定建议对象或目标对象。正如您之前看到的,您可以通过使用setTarget()方法来实现这一点。在内部,ProxyFactory将代理创建过程委托给DefaultAopProxyFactory的一个实例,后者又委托给Cglib2AopProxyJdkDynamicAopProxy,这取决于应用的设置。我们将在本章后面更详细地讨论代理创建。

ProxyFactory类提供了您在前面的代码示例中看到的addAdvice()方法,用于您希望建议应用于类中所有方法的调用,而不仅仅是选择的情况。在内部,addAdvice()将您传递的通知包装在一个DefaultPointcutAdvisor的实例中,这是PointcutAdvisor的标准实现,并且用一个默认包含所有方法的切入点来配置它。当您想要对创建的Advisor进行更多的控制,或者想要向代理添加介绍时,您可以自己创建Advisor并使用ProxyFactoryaddAdvisor()方法。

您可以使用同一个ProxyFactory实例来创建许多代理,每个代理都有不同的方面。为了帮助做到这一点,ProxyFactoryremoveAdvice()removeAdvisor()方法,允许您从之前传递给它的ProxyFactory中删除任何建议或顾问。要检查一个ProxyFactory是否附加了特定的建议,调用adviceIncluded(),传入您想要检查的建议对象。

在 Spring 创造建议

Spring 支持六种风格的建议,如表 5-1 所述。

表 5-1。

Advice Types in Spring

| 建议名称 | 连接 | 描述 | | --- | --- | --- | | `Before` | `org.springframework.aop.MethodBeforeAdvice` | 使用 before advice,您可以在连接点执行之前执行自定义处理。因为 Spring 中的连接点总是一个方法调用,这实质上允许您在方法执行之前执行预处理。Before advice 可以完全访问方法调用的目标以及传递给方法的参数,但是它不能控制方法本身的执行。如果 before 通知抛出异常,拦截器链(以及目标方法)的进一步执行将被中止,并且异常将向上传播回拦截器链。 | | `After-Returning` | `org.springframework.aop.AfterReturningAdvice` | 在连接点的方法调用完成执行并返回一个值之后,执行返回后通知。返回后通知可以访问方法调用的目标、传递给方法的参数和返回值。因为在调用返回后通知时已经执行了该方法,所以它对方法调用没有任何控制。如果目标方法抛出异常,返回后通知将不会运行,异常将照常向上传播到调用堆栈。 | | `After(finally)` | `org.springframework.aop.AfterAdvice` | 仅当建议的方法正常完成时,才执行返回后建议。但是,无论建议方法的结果如何,都将执行 after (finally)建议。即使建议的方法失败并抛出异常,建议也会执行。 | | `Around` | `org.aopalliance.intercep` t `.MethodInterceptor` | 在 Spring 中,around advice 使用 AOP 联盟标准的方法拦截器进行建模。您的建议允许在方法调用前后执行,并且您可以控制方法调用允许进行的时间点。如果愿意,您可以选择完全绕过该方法,提供您自己的逻辑实现。 | | `Throws` | `org.springframework.aop.ThrowsAdvice` | Throws advice 在方法调用返回后执行,但前提是该调用抛出了异常。throws 通知可以只捕捉特定的异常,如果您选择这样做,您可以访问抛出异常的方法、传递给调用的参数以及调用的目标。 | | `Introduction` | `org.springframework.aop.IntroductionInterceptor` | Spring 将引入建模为特殊类型的拦截器。使用引入拦截器,您可以指定由通知引入的方法的实现。 |

建议界面

根据我们之前对ProxyFactory类的讨论,回想一下,通知可以通过使用addAdvice()方法直接添加到代理中,也可以通过使用带有addAdvisor()方法的 Advisor 实现间接添加到代理中。advice 和 advisor 之间的主要区别在于,advisor 携带带有关联切入点的 advice,这提供了对 advice 将拦截哪些连接点的更细粒度的控制。关于建议,Spring 为建议接口创建了一个定义良好的层次结构。这个层次结构基于 AOP 联盟接口,如图 5-2 所示。

A315511_5_En_5_Fig2_HTML.jpg

图 5-2。

Interfaces for Spring advice types as depicted in IntelliJ IDEA

这种层次结构的好处是不仅是合理的 OO 设计,而且使您能够处理一般的通知类型,例如通过在ProxyFactory上使用单个addAdvice()方法,并且您可以容易地添加新的通知类型,而不必修改ProxyFactory类。

建议前创建

Before advice 是 Spring 中最有用的建议类型之一。该建议可以修改传递给方法的参数,并可以通过引发异常来阻止方法执行。在本节中,我们将向您展示使用 before advice 的两个简单示例:一个是在方法执行之前将包含方法名称的消息写入控制台输出,另一个是您可以用来限制对对象上的方法的访问。在下面的代码片段中,您可以看到SimpleBeforeAdvice类的代码:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
    public static void main(String... args) {
        Guitarist johnMayer = new Guitarist();

        ProxyFactory pf = new ProxyFactory();
        pf.addAdvice(new SimpleBeforeAdvice());
        pf.setTarget(johnMayer)

;

        Guitarist proxy = (Guitarist) pf.getProxy();

        proxy.sing();
    }

    @Override
    public void before(Method method, Object[] args, Object target)
            throws Throwable {
        System.out.println("Before '" + method.getName() + "', tune guitar.");
    }
}

Guitarist很简单,只有一个方法sing(),它在控制台中打印出一段歌词。它扩展了Singer接口,该接口将在整本书中使用。

package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Singer;

public class Guitarist implements Singer {

        private String lyric="You're gonna live forever in me";
@Override
        public void sing(){
                System.out.println(lyric);
        }
}

基本上,这个建议可以确保豆子在唱歌前先给吉他调音。在这段代码中,你可以看到我们已经用SimpleBeforeAdvice类的一个实例通知了我们之前创建的Guitarist类的一个实例。由SimpleBeforeAdvice实现的MethodBeforeAdvice接口定义了一个方法before(),在连接点的方法被调用之前,AOP 框架会调用这个方法。记住,现在,我们使用由addAdvice()方法提供的默认切入点,它匹配一个类中的所有方法。向before()方法传递三个参数:要调用的方法、将传递给该方法的参数以及作为调用目标的ObjectSimpleBeforeAdvice类使用before()方法的Method参数将消息写入控制台输出,其中包含要调用的方法的名称。运行此示例会得到以下输出:

Before 'sing', tune guitar.
You're gonna live forever in me

正如您所看到的,显示了调用sing()的输出,但是在它之前,您可以看到由SimpleBeforeAdvice生成的输出。

通过使用 Before Advice 保护方法访问

在本节中,我们将实现 before advice,它在允许方法调用继续进行之前检查用户凭证。如果用户凭据无效,通知将引发异常,从而阻止方法执行。本节中的示例非常简单。它允许用户使用任何密码进行身份验证,并且只允许单个硬编码用户访问受保护的方法。然而,它确实说明了使用 AOP 来实现安全性这样的横切关注点是多么容易。

这只是一个演示如何使用 before 建议的例子。为了保护 Spring beans 的方法执行,Spring Security 项目已经提供了全面的支持;您不需要自己实现这些特性。

下面的代码片段展示了SecureBean类。这是我们将使用 AOP 保护的类。

package com.apress.prospring5.ch5;

public class SecureBean {
    public void writeSecureMessage() {
        System.out.println("Every time I learn something new, "
            + "it pushes some old stuff out of my brain");
    }
}

这门课传授了荷马·辛普森的智慧之珠,这是我们不想让每个人都看到的智慧。因为这个例子要求用户进行身份验证,所以我们需要存储他们的详细信息。下面的代码片段显示了我们可以用来存储用户凭证的UserInfo类:

package com.apress.prospring5.ch5;

public class UserInfo {
    private String userName;
    private String password;

    public UserInfo(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

    public String getPassword() {
        return password;
    }
    public String getUserName() {
        return userName;
    }
}

这个类只是保存关于用户的数据,这样我们就可以用它做一些有用的事情。下面的代码片段显示了SecurityManager类,它负责对用户进行身份验证,并存储他们的凭据以供以后检索:

package com.apress.prospring5.ch5;

public class SecurityManager {
    private static ThreadLocal<UserInfo>
                 threadLocal = new ThreadLocal<>();

    public void login(String userName, String password) {
        threadLocal.set(new UserInfo(userName, password));
    }

    public void logout() {
        threadLocal.set(null);
    }

    public UserInfo getLoggedOnUser() {
        return threadLocal.get();
    }
}

应用使用SecurityManager类对用户进行身份验证,然后检索当前已通过身份验证的用户的详细信息。应用通过使用login()方法来验证用户。这只是一个模拟实现。在实际的应用中,login()方法可能会根据数据库或 LDAP 目录检查提供的凭证,但是这里我们可以假设所有用户都被允许进行身份验证。login()方法为用户创建一个UserInfo对象,并使用ThreadLocal将其存储在当前线程中。logout()方法设置可能存储在ThreadLocalnull中的任何值。最后,getLoggedOnUser()方法返回当前认证用户的UserInfo对象。如果没有用户通过身份验证,该方法返回null

要检查用户是否通过身份验证,如果是,是否允许用户访问Secure-Bean上的方法,我们需要创建在方法之前执行的通知,并根据允许用户的凭证集检查由Security-Manager.getLoggedOnUser()返回的UserInfo对象。此建议的代码SecurityAdvice如下所示:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class SecurityAdvice implements MethodBeforeAdvice {
    private SecurityManager securityManager;

    public SecurityAdvice() {
        this.securityManager = new SecurityManager();
    }

    @Override
    public void before(Method method, Object[] args, Object target)
            throws Throwable {
        UserInfo user = securityManager.getLoggedOnUser();

        if (user == null) {
            System.out.println("No user authenticated");
            throw new SecurityException(
                "You must login before attempting to invoke the method: "
                + method.getName());
        } else if ("John".equals(user.getUserName())) {
            System.out.println("Logged in user is John - OKAY!");
        } else {
            System.out.println("Logged in user is " + user.getUserName()
                + " NOT GOOD :(");
            throw new SecurityException("User " + user.getUserName()
                + " is not allowed access to method " + method.getName());
        }
    }
}

SecurityAdvice类在其构造函数中创建一个SecurityManager的实例,然后将这个实例存储在一个字段中。你应该注意到应用和SecurityAdvice不需要共享同一个SecurityManager实例,因为所有的数据都通过使用ThreadLocal存储在当前线程中。在before()方法中,我们执行一个简单的检查来查看被认证用户的用户名是否是John。如果是,我们允许用户访问;否则,将引发异常。还要注意,我们检查了一个null UserInfo对象,这表明当前用户没有经过身份验证。

在下面的代码片段中,您可以看到一个使用SecurityAdvice类来保护SecureBean类的示例应用:

package com.apress.prospring5.ch5;

import org.springframework.aop.framework.ProxyFactory;

public class SecurityDemo {
    public static void main(String... args) {
        SecurityManager mgr = new SecurityManager();

        SecureBean bean = getSecureBean();

        mgr.login("John", "pwd");
        bean.writeSecureMessage();
        mgr.logout();

        try {
            mgr.login("invalid user", "pwd");
            bean.writeSecureMessage();
        } catch(SecurityException ex) {
            System.out.println("Exception Caught: " + ex.getMessage());
        } finally {
            mgr.logout();
        }

        try {
            bean.writeSecureMessage();
        } catch(SecurityException ex) {
            System.out.println("Exception Caught: " + ex.getMessage());
        }
    }

    private static SecureBean getSecureBean() {
        SecureBean target = new SecureBean();

        SecurityAdvice advice = new SecurityAdvice();

        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvice(advice);

        SecureBean proxy = (SecureBean)factory.getProxy();

        return proxy;
    }
}

getSecureBean()方法中,我们创建了一个SecureBean类的代理,使用SecurityAdvice的一个实例来通知它。这个代理被返回给调用者。当调用者调用这个代理上的任何方法时,调用首先被路由到SecurityAdvice的实例进行安全检查。在main()方法中,我们测试了三个场景,使用两组用户凭证调用SecureBean.writeSecureMessage()方法,然后完全不使用用户凭证。因为只有当当前认证的用户是JohnSecurityAdvice才允许方法调用继续,所以我们可以预期前面代码中唯一成功的场景是第一个。运行此示例会产生以下输出:

Logged in user is John - OKAY!
Every time I learn something new,  it pushes some old stuff out of my brain
Logged in user is invalid user NOT GOOD :(
Exception Caught: User  invalid user is not allowed access to method
              writeSecureMessage
No user authenticated
Exception Caught: You must login before attempting to invoke the method:
              writeSecureMessage

如您所见,只有第一次调用SecureBean.writeSecureMessage()才被允许进行。剩余的调用被SecurityAdvice抛出的SecurityException异常阻止。这个例子很简单,但是它突出了 before advice 的用处。安全性是 before advice 的一个典型例子,但是我们也发现当一个场景需要修改传递给方法的参数时,它很有用。

创建退货后通知

返回后通知在连接点的方法调用返回后执行。假设方法已经执行,你不能改变传递给它的参数。虽然可以读取这些参数,但是不能改变执行路径,也不能阻止方法执行。这些限制是意料之中的;然而,不期望的是,您不能在返回后的通知中修改返回值。使用退货后通知时,您只能添加处理。尽管返回后通知不能修改方法调用的返回值,但它可以抛出一个异常,该异常可以代替返回值被发送到堆栈上。

在这一节中,我们来看两个在应用中使用退货后通知的例子。第一个示例只是在调用方法后将一条消息写入控制台输出。第二个例子展示了如何使用返回后通知向方法中添加错误检查。考虑一个为加密目的生成密钥的类KeyGenerator。许多密码算法都存在少量密钥被认为是脆弱的问题。弱密钥是其特征使得在不知道密钥的情况下导出原始消息明显更容易的任何密钥。对于 DES 算法,总共有 256 个可能的密钥。在这个密钥空间中,4 个密钥被认为是弱的,另外 12 个被认为是半弱的。尽管随机生成这些密钥的几率很小(252 分之一),但是测试这些密钥非常简单,值得一试。在本节的第二个例子中,我们构建了返回后通知,检查由KeyGenerator生成的弱键,如果发现一个,就抛出一个异常。

A315511_5_En_5_Figa_HTML.jpg要了解更多关于弱密钥和加密技术的信息,我们推荐您访问 William Stallings 的网站 http://williamstallings.com/Cryptography/

在下面的代码片段中,您可以看到SimpleAfterReturningAdvice类,它通过在方法返回后向控制台输出写入一条消息来演示返回后通知的使用。前面介绍的类Guitarist在这里被重用。

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.framework.ProxyFactory;

public class SimpleAfterReturningAdvice implements
               AfterReturningAdvice {
    public static void main(String... args) {
         Guitarist target = new Guitarist();

        ProxyFactory pf = new ProxyFactory();

        pf.addAdvice(new SimpleAfterReturningAdvice());
        pf.setTarget(target);

        Guitarist proxy = (Guitarist) pf.getProxy();
        proxy.sing();
    }

    @Override
    public void afterReturning(Object returnValue, Method method,
             Object[] args, Object target) throws Throwable {

        System.out.println("After '" + method.getName()+ "' put down guitar.");
    }

}

注意,AfterReturningAdvice接口声明了一个方法afterReturning(),它被传递了方法调用的返回值、对被调用方法的引用、传递给方法的参数以及调用的目标。运行此示例会产生以下输出:

You're gonna live forever in me
After 'sing' put down guitar.

输出类似于通知之前的例子,除了如预期的那样,由通知编写的消息出现在由writeMessage()方法编写的消息之后。当一个方法有可能返回一个无效值时,使用返回后通知的一个好方法是执行一些额外的错误检查。

在我们前面描述的场景中,加密密钥生成器有可能生成一个对于特定算法来说被认为是脆弱的密钥。理想情况下,密钥生成器会检查这些弱密钥,但是由于这些密钥出现的机会通常非常小,所以许多生成器不会检查。通过使用返回后通知,我们可以通知生成密钥和执行这个附加检查的方法。下面是一个极其原始的密钥生成器:

package com.apress.prospring5.ch5;

import java.util.Random;

public class KeyGenerator {
    protected static final long WEAK_KEY = 0xFFFFFFF0000000L;
    protected static final long STRONG_KEY = 0xACDF03F590AE56L;

    private Random rand = new Random();

    public long getKey() {
        int x = rand.nextInt(3);

        if (x == 1) {
            return WEAK_KEY;
        }

        return STRONG_KEY;
    }
}

这个密钥生成器不应该被视为安全的。对于这个例子来说,它是故意简单的,并且有三分之一的机会产生一个弱密钥。在下面的代码片段中,您可以看到WeakKeyCheckAdvice,它检查getKey()方法的结果是否是一个弱键:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;

import static com.apress.prospring5.ch5.KeyGenerator.WEAK_KEY;

public class WeakKeyCheckAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method,
            Object args,Object target) throws Throwable {

        if ((target instanceof KeyGenerator)
                && ("getKey".equals(method.getName()))) {
            long key = ((Long) returnValue).longValue();

            if (key == WEAK_KEY) {
                throw new SecurityException(
                    "Key Generator generated a weak key. Try again");
            }
        }
    }
}

afterReturning()方法中,我们首先检查在连接点执行的方法是否是getKey()方法。如果是,那么我们检查结果值,看它是否是弱键。如果我们发现getKey()方法的结果是一个弱键,那么我们抛出一个SecurityException来通知调用代码这一点。下面的代码片段显示了一个简单的应用,演示了该建议的用法:

package com.apress.prospring5.ch5;

import org.springframework.aop.framework.ProxyFactory;

public class AfterAdviceDemo {
    private static KeyGenerator getKeyGenerator() {
        KeyGenerator target = new KeyGenerator();

        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvice(new WeakKeyCheckAdvice());

        return (KeyGenerator)factory.getProxy();
    }

    public static void main(String... args) {
        KeyGenerator keyGen = getKeyGenerator();

        for(int x = 0; x < 10; x++) {
            try {
                long key = keyGen.getKey();
                System.out.println("Key: " + key);
            } catch(SecurityException ex) {
                System.out.println("Weak Key Generated!");
            }
        }
    }
}

在创建了KeyGenerator目标的建议代理后,AfterAdviceDemo类试图生成十个密钥。如果在单个生成过程中抛出了一个SecurityException,一条消息将被写入控制台,通知用户生成了一个弱密钥;否则,将显示生成的密钥。在我们的机器上运行一次会生成以下输出:

Key: 48658904092028502
Weak Key Generated!
Key: 48658904092028502
Weak Key Generated!
Weak Key Generated!
Weak Key Generated!
Key: 48658904092028502
Key: 48658904092028502
Key: 48658904092028502
Key: 48658904092028502

如您所见,KeyGenerator类有时会生成弱键,正如所料,WeakKeyCheckAdvice确保每当遇到弱键时都会引发SecurityException

围绕建议创建

Around advice 的功能类似于 before 和 after advice 的组合,有一个很大的区别:您可以修改返回值。不仅如此,您还可以阻止该方法的执行。这意味着通过使用 around advice,您可以用新代码替换方法的整个实现。Spring 中的 Around advice 被建模为一个使用MethodInterceptor接口的拦截器。around advice 有许多用途,您会发现 Spring 的许多特性都是通过使用方法拦截器创建的,比如远程代理支持和事务管理特性。方法拦截也是一种很好的分析应用执行的机制,它构成了本节中示例的基础。

我们不会看到如何为方法拦截构建一个简单的例子;相反,我们参考第一个使用Agent类的例子,它展示了如何使用一个基本的方法拦截器在方法调用的任何一端编写消息。从前面的例子中可以注意到,MethodInterceptor接口的invoke()方法没有提供与MethodBeforeAdviceAfterReturningAdvice相同的参数集。不会将调用的目标、被调用的方法或使用的参数传递给该方法。但是,您可以通过使用传递给invoke()MethodInvocation对象来访问这些数据。您将在下面的示例中看到这方面的演示。

对于这个例子,我们想要实现一些方法来通知一个类,这样我们就可以获得关于它的方法的运行时性能的基本信息。具体来说,我们想知道该方法执行了多长时间。为了实现这一点,我们可以使用 Spring 中包含的StopWatch类,我们显然需要一个MethodInterceptor,因为我们需要在方法调用之前启动StopWatch,然后立即停止。

下面的代码片段显示了我们将通过使用StopWatch类和 around advice 来分析的WorkerBean类:

package com.apress.prospring5.ch5;

public class WorkerBean {
    public void doSomeWork(int noOfTimes) {
        for(int x = 0; x < noOfTimes; x++) {
            work();
        }
    }

    private void work() {
        System.out.print("");
    }
}

这是一个简单的类。doSomeWork()方法接受一个参数noOfTimes,并精确地调用work()方法指定的次数。work()方法只是有一个对System.out.print()的虚拟调用,它传入一个空的String。这阻止了编译器优化work()方法以及对work()的调用。

在下面的代码片段中,您可以看到使用StopWatch类来分析方法调用时间的ProfilingInterceptor类。我们使用这个拦截器来分析前面显示的WorkerBean类。

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.util.StopWatch;

public class ProfilingInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch sw = new StopWatch();
        sw.start(invocation.getMethod().getName());

        Object returnValue = invocation.proceed();

        sw.stop();
        dumpInfo(invocation, sw.getTotalTimeMillis());
        return returnValue;
    }

    private void dumpInfo(MethodInvocation invocation, long ms) {
        Method m = invocation.getMethod();
        Object target = invocation.getThis();
        Object args = invocation.getArguments();

        System.out.println("Executed method: " + m.getName());
        System.out.println("On object of type: " +
                target.getClass().getName());

        System.out.println("With arguments:");
        for (int x = 0; x < args.length; x++) {
            System.out.print("       > " + argsx);
        }
        System.out.print("\n");

        System.out.println

("Took: " + ms + " ms");
    }
}

invoke()方法中,这是MethodInterceptor接口中唯一的方法,我们创建了一个StopWatch的实例,然后立即开始运行,允许方法调用继续调用MethodInvocation.proceed()。一旦方法调用结束并且返回值被捕获,我们就停止StopWatch并将花费的总毫秒数连同MethodInvocation对象一起传递给dumpInfo()方法。最后,我们返回由MethodInvocation.proceed()返回的Object,以便调用者获得正确的返回值。在这种情况下,我们不想以任何方式中断调用堆栈;我们只是充当方法调用的窃听者。如果我们愿意,我们可以完全改变调用堆栈,将方法调用重定向到另一个对象或远程服务,或者我们可以简单地在拦截器中重新实现方法逻辑并返回不同的返回值。

dumpInfo()方法只是将一些关于方法调用的信息写入控制台输出,以及方法执行所用的时间。在dumpInfo()的前三行中,您可以看到如何使用MethodInvocation对象来确定被调用的方法、调用的原始目标和使用的参数。

下面的代码示例显示了ProfilingDemo类,它首先用ProfilingInterceptor通知WorkerBean的一个实例,然后分析doSomeWork()方法。

package com.apress.prospring5.ch5;

import org.springframework.aop.framework.ProxyFactory;

public class ProfilingDemo {
    public static void main(String... args) {
        WorkerBean bean = getWorkerBean();
        bean.doSomeWork(10000000);
    }

    private static WorkerBean getWorkerBean() {
        WorkerBean target = new WorkerBean();

        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvice(new ProfilingInterceptor());

        return (WorkerBean)factory.getProxy();
    }
}

在我们的机器上运行这个示例会产生以下输出:

Executed method: doSomeWork
On object of type: com.apress.prospring5.ch5.WorkerBean
With arguments:
       > 10000000
Took: 1139 ms

从这个输出中,您可以看到执行了哪个方法,目标的类是什么,传入了什么参数,以及调用花费了多长时间。

创建抛出建议

Throws advice 类似于 after-return advice,因为它在 joinpoint 之后执行,这总是一个方法调用,但是 throws advice 仅在方法抛出异常时执行。抛出建议也类似于返回后建议,因为它对程序执行几乎没有控制。如果您使用 throws 建议,您不能选择忽略引发的异常,而是为方法返回值。您可以对程序流进行的唯一修改是更改引发的异常的类型。这是一个非常强大的概念,可以使应用开发更加简单。考虑这样一种情况,您有一个 API,它抛出了一系列定义不当的异常。使用 throws 建议,您可以建议该 API 中的所有类,并将异常层次结构重新分类,使之更易于管理和描述。当然,您也可以使用 throws 建议在应用中提供集中的错误日志记录,从而减少应用中错误日志记录代码的数量。

如图 5-2 所示,抛出建议是由ThrowsAdvice接口实现的。与你目前看到的接口不同,ThrowsAdvice没有定义任何方法;相反,它只是 Spring 使用的一个标记接口。这样做的原因是 Spring 允许类型化的 throws 建议,这允许您准确地定义您的 throws 建议应该捕捉哪些Exception类型。Spring 通过使用反射检测带有特定签名的方法来实现这一点。Spring 寻找两个不同的方法签名。一个简单的例子很好地说明了这一点。下面的代码片段显示了一个简单的 bean,它有两个方法,这两个方法都抛出不同类型的异常:

package com.apress.prospring5.ch5;

public class ErrorBean {
    public void errorProneMethod() throws Exception {
        throw new Exception("Generic Exception");
    }

    public void otherErrorProneMethod() throws IllegalArgumentException {
        throw new IllegalArgumentException("IllegalArgument Exception");
    }
}

在这里,您可以看到SimpleThrowsAdvice类演示了 Spring 在 throws 建议中寻找的两个方法签名:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;

public class SimpleThrowsAdvice implements ThrowsAdvice {
    public static void main(String... args) throws Exception {
        ErrorBean errorBean = new ErrorBean();

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(errorBean);
        pf.addAdvice(new SimpleThrowsAdvice());

        ErrorBean proxy = (ErrorBean) pf.getProxy();
        try {
            proxy.errorProneMethod();
        } catch (Exception ignored) {

        }

        try {
            proxy.otherErrorProneMethod();
        } catch (Exception ignored) {

        }
    }

    public void afterThrowing(Exception ex) throws Throwable {
        System.out.println("***");
        System.out.println("Generic Exception Capture");
        System.out.println("Caught: " + ex.getClass().getName());
        System.out.println("***\n");
    }

    public void afterThrowing(Method method, Object args, Object target,
            IllegalArgumentException ex) throws Throwable {
        System.out.println("***");
        System.out.println("IllegalArgumentException Capture");
        System.out.println("Caught: " + ex.getClass().getName());
        System.out.println("Method: " + method.getName());
        System.out.println("***\n");
    }
}

Spring 在 throws advice 中首先寻找的是一个或多个名为afterThrowing()的公共方法。方法的返回类型并不重要,尽管我们发现最好坚持使用void,因为这个方法不能返回任何有意义的值。SimpleThrowsAdvice类中的第一个afterThrowing()方法有一个类型为Exception的参数。您可以指定任何类型的Exception作为参数,当您不关心抛出异常的方法或传递给它的参数时,这个方法是理想的。注意,这个方法捕捉ExceptionException的任何子类型,除非所讨论的类型有自己的afterThrowing()方法。

在第二个afterThrowing()方法中,我们声明了四个参数来捕捉抛出异常的方法、传递给该方法的参数以及方法调用的目标。此方法中参数的顺序很重要,您必须指定所有四个参数。注意,第二个afterThrowing()方法捕获类型IllegalArgumentException(或其子类型)的异常。运行此示例会产生以下输出:

***
Generic Exception Capture
Caught: java.lang.Exception
***

***
IllegalArgumentException Capture
Caught: java.lang.IllegalArgumentException
Method: otherErrorProneMethod
***

如您所见,当抛出一个普通的旧Exception时,第一个afterThrowing()方法被调用,但是当抛出一个IllegalArgumentException时,第二个afterThrowing()方法被调用。Spring 只为每个Exception调用一个afterThrowing()方法,正如您在SimpleThrowsAdvice类的例子中看到的,Spring 使用其签名包含与Exception类型最佳匹配的方法。如果您的抛出后通知有两个afterThrowing()方法,都用相同的Exception类型声明,但一个只有一个参数,另一个有四个参数,Spring 调用四参数afterThrowing()方法。

抛后建议在各种情况下都很有用;它允许您重新分类整个Exception层次结构,并为您的应用构建集中式Exception日志记录。我们发现,当我们调试一个正在运行的应用时,抛出后建议特别有用,因为它允许我们添加额外的日志记录代码,而不需要修改应用的代码。

选择建议类型

一般来说,选择建议类型是由应用的需求决定的,但是您应该根据自己的需要选择最具体的建议类型。也就是说,当 before advice 可以使用时,不要使用 around advice。在大多数情况下,around advice 可以完成其他三种类型的建议所能完成的一切,但是对于你想要达到的目标来说,它可能是多余的。通过使用最具体的建议类型,您可以使代码的意图更加清晰,同时也减少了出错的可能性。考虑计算方法调用的建议。当您使用 before advice 时,您需要编写的只是计数器,但是对于 around advice,您需要记住调用方法并将值返回给调用者。这些小事可能会让虚假的错误潜入您的应用。通过使建议类型尽可能集中,您减少了错误的范围。

Spring 中的顾问和切入点

到目前为止,你看到的所有例子都使用了ProxyFactory类。这个类提供了一种简单的方法来获取和配置自定义用户代码中的 AOP 代理实例。ProxyFactory.addAdvice()方法是为代理配置建议。这个方法在幕后委托给addAdvisor(),创建一个DefaultPointcutAdvisor的实例,并用一个指向所有方法的切入点来配置它。这样,通知被认为适用于目标上的所有方法。在某些情况下,比如当您使用 AOP 进行日志记录时,这可能是可取的,但是在其他情况下,您可能希望限制建议适用的方法。

当然,您可以简单地在通知本身中执行检查,确认被通知的方法是正确的,但是这种方法有几个缺点。首先,将可接受的方法列表硬编码到建议中会降低建议的可重用性。通过使用切入点,您可以配置通知适用的方法,而不需要将这些代码放在通知中;这显然增加了建议的重用价值。将方法列表硬编码到通知中的其他缺点与性能有关。要检查通知中被通知的方法,您需要在每次调用目标上的任何方法时执行检查。这显然会降低应用的性能。当您使用切入点时,会对每个方法执行一次检查,并将结果缓存起来供以后使用。不使用切入点来限制 list-advised 方法的另一个与性能相关的缺点是,Spring 可以在创建代理时对非高级方法进行优化,这导致了对非高级方法的更快调用。当我们在本章后面讨论代理时,会更详细地介绍这些优化。

我们强烈建议您避免将方法检查硬编码到通知中,而是尽可能使用切入点来控制通知对目标方法的适用性。也就是说,在某些情况下,有必要将检查硬编码到您的建议中。考虑早先的返回后通知的例子,它被设计来捕捉由KeyGenerator类生成的弱键。这种通知与它所通知的类紧密相关,明智的做法是检查通知内部以确保它被应用于正确的类型。我们将建议和目标之间的这种耦合称为目标关联性。一般来说,当您的建议很少或没有目标关联性时,您应该使用切入点。也就是说,它可以应用于任何类型或广泛的类型。当你的建议有很强的目标亲和力时,试着检查建议本身是否被正确使用;当建议被误用时,这有助于减少令人头疼的错误。我们还建议您避免不必要的建议方法。正如您将看到的,这会导致调用速度明显下降,这对应用的整体性能有很大影响。

切入点接口

Spring 中的切入点是通过实现Pointcut接口创建的,如下所示:

package org.springframework.aop;

public interface Pointcut {
    ClassFilter getClassFilter ();
    MethodMatcher getMethodMatcher();
}

从这段代码中可以看出,Pointcut接口定义了两个方法,getClassFilter()getMethodMatcher(),分别返回ClassFilterMethodMatcher的实例。显然,如果选择实现Pointcut接口,就需要实现这些方法。幸运的是,正如您将在下一节中看到的,这通常是不必要的,因为 Spring 提供了一系列的Pointcut实现,涵盖了您的大部分(如果不是全部)用例。

当确定一个Pointcut是否适用于一个特定的方法时,Spring 首先通过使用由Pointcut.getClassFilter()返回的ClassFilter实例来检查Pointcut接口是否适用于该方法的类。这里是ClassFilter界面:

org.springframework.aop;

public interface ClassFilter {
    boolean matches(Class<?> clazz);
}

如您所见,ClassFilter接口定义了一个方法matches(),它被传递了一个代表要检查的类的Class实例。正如您已经确定的,如果切入点适用于类,那么matches()方法返回true,否则返回false

MethodMatcher接口比ClassFilter接口更复杂,如下所示:

package org.springframework.aop;

public interface MethodMatcher {
    boolean matches(Method m, Class<?> targetClass);
    boolean isRuntime();
    boolean matches(Method m, Class<?> targetClass, Object[] args);
}

Spring 支持两种类型的MethodMatcher,静态和动态,由isRuntime()的返回值决定。在使用MethodMatcher之前,Spring 调用isRuntime()来确定MethodMatcher是静态的,由返回值false表示,还是动态的,由返回值true表示。

对于静态切入点,Spring 为目标上的每个方法调用一次MethodMatchermatches(Method, Class<T>)方法,缓存这些方法的后续调用的返回值。通过这种方式,对每个方法只执行一次方法适用性检查,并且方法的后续调用不会导致matches()的调用。

使用动态切入点,Spring 仍然通过在第一次调用方法时使用matches(Method, Class<T>)来执行静态检查,以确定方法的整体适用性。然而,除此之外,假设静态检查返回了true,Spring 通过使用matches(Method, Class<T>, Object[])方法对方法的每次调用执行进一步的检查。这样,动态的MethodMatcher可以根据方法的特定调用,而不仅仅是方法本身,来决定切入点是否应该应用。例如,只有当参数是值大于 100 的Integer时,才需要应用切入点。在这种情况下,可以对matches(Method, Class<T>, Object[])方法进行编码,以便对每次调用的参数进行进一步检查。

显然,静态切入点的性能比动态切入点好得多,因为它们避免了每次调用都需要额外的检查。动态切入点为决定是否应用建议提供了更大的灵活性。一般来说,我们建议您尽可能使用静态切入点。然而,如果您的建议增加了大量的开销,通过使用动态切入点来避免任何不必要的建议调用可能是明智的。

一般来说,您很少从头开始创建自己的Pointcut实现,因为 Spring 为静态和动态切入点都提供了抽象基类。在接下来的几节中,我们将看看这些基类,以及其他的Pointcut实现。

可用的切入点实现

从 4.0 版本开始,Spring 提供了八个Pointcut接口的实现:两个抽象类,旨在作为创建静态和动态切入点的便利类,以及六个具体类,分别用于以下每一个:

  • 将多个切入点组合在一起
  • 处理控制流切入点
  • 执行简单的基于名称的匹配
  • 使用正则表达式定义切入点
  • 使用 AspectJ 表达式定义切入点
  • 定义在类或方法级别寻找特定注释的切入点

表 5-2 总结了八个Pointcut接口实现。

表 5-2。

Summary of Spring Pointcut Implementations

| 实现类 | 描述 | | --- | --- | | `org.springframework.aop.support.annotation.AnnotationMatchingPointcut` | 该实现在类或方法上寻找特定的 Java 注释。此类要求 JDK 5 或更高。 | | `org.springframework.aop.aspectj.AspectJExpressionPointcut` | 这个实现使用 AspectJ weaver 来评估 AspectJ 语法中的切入点表达式。 | | `org.springframework.aop.support.ComposablePointcut` | `ComposablePointcut`类用于通过`union()`和`intersection()`等操作组合两个或多个切入点。 | | `org.springframework.aop.support.ControlFlowPointcut` | `ControlFlowPointcut`是一个特例切入点,它匹配另一个方法的控制流中的所有方法,也就是说,作为另一个方法被调用的结果而被直接或间接调用的任何方法。 | | `org.springframework.aop.support.DynamicMethodMatcherPointcut` | 该实现旨在作为构建动态切入点的基类。 | | `org.springframework.aop.support.JdkRegexpMethodPointcut` | 这个实现允许您使用 JDK 1.4 正则表达式支持来定义切入点。此类要求 JDK 1.4 或更高版本。 | | `org.springframework.aop.support.NameMatchMethodPointcut` | 使用`NameMatchMethodPointcut`,您可以创建一个切入点,根据方法名列表执行简单的匹配。 | | `org.springframework.aop.support.StaticMethodMatcherPointcut` | `StaticMethodMatcherPointcut`类旨在作为构建静态切入点的基础。 |

图 5-3 显示了Pointcut实现类的统一建模语言(UML) 2 图。

A315511_5_En_5_Fig3_HTML.jpg

图 5-3。

Pointcut implementation classes represented as an UML diagram in Intellij IDEA

使用 DefaultPointcutAdvisor

在使用任何Pointcut实现之前,您必须首先创建一个Advisor接口的实例,或者更具体地说是一个PointcutAdvisor接口。在我们之前的讨论中,请记住Advisor是 Spring 的一个方面的表示(参见上一节“Spring 中的方面”),它是建议和切入点的结合,决定了应该建议哪些方法以及如何建议。Spring 提供了许多PointcutAdvisor的实现,但是现在我们只关注一个DefaultPointcutAdvisor。这是一个简单的PointcutAdvisor,用于将单个Pointcut与单个Advice相关联。

使用 StaticMethodMatcherPointcut 创建静态切入点

在这一节中,我们将通过扩展抽象的StaticMethodMatcherPointcut类来创建一个简单的静态切入点。因为StaticMethodMatcherPointcut类扩展了StaticMethodMatcher类(也是一个抽象类),后者实现了MethodMatcher接口,所以您需要实现方法matches(Method, Class<?>)。其余的Pointcut实现是自动处理的。尽管这是您需要实现的唯一方法(当扩展StaticMethodMatcherPointcut类时),您可能想要覆盖getClassFilter()方法,如本例所示,以确保只通知正确类型的方法。

对于这个例子,我们有两个类,GoodGuitaristGreatGuitarist,它们都定义了相同的方法,是接口Singermethod的实现。

package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Singer;

public class GoodGuitarist implements Singer {

        @Override public void sing() {
                System.out.println("Who says  I can't be free \n" +
                                "From  all of the things that I used to be");
        }
}

public class GreatGuitarist implements Singer {

        @Override public void sing() {
                System.out.println("I shot the sheriff, \n" +
                                "But I did not shoot the deputy");
        }
}

在这个例子中,我们希望能够通过使用相同的DefaultPointcutAdvisor来创建两个类的代理,但是让建议只应用于GoodGuitarist类的sing()方法。为此,我们创建了如下所示的SimpleStaticPointcut类:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.StaticMethodMatcherPointcut;

public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
    @Override

    public boolean matches(Method method, Class<?> cls) {
        return ("sing".equals(method.getName()));
    }

    @Override
    public ClassFilter getClassFilter() {
        return cls -> (cls == GoodGuitarist.class);
    }
}

这里你可以看到我们按照StaticMethodMatcher抽象类的要求实现了matches(Method, Class<?>)方法。如果方法的名称是sing,实现简单地返回true;否则,它返回false。使用 lambda 表达式,在前面的代码示例中隐藏了在getClassFilter()方法中实现ClassFilter的匿名类的创建。扩展的 lambda 表达式如下所示:

public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> cls) {
                return (cls == GoodGuitarist.class);
            }
        };
    }

注意,我们还覆盖了getClassFilter()方法来返回一个ClassFilter实例,该实例的matches()方法只为GoodGuitarist类返回true。对于这个静态切入点,我们说只有GoodGuitarist类的方法会被匹配,而且,只有那个类的sing()方法会被匹配。

下面的代码片段显示了SimpleAdvice类,它只是在方法调用的任何一端写出一条消息:

package com.apress.prospring5.ch5;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class SimpleAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println(">> Invoking " + invocation.getMethod().getName());
        Object retVal = invocation.proceed();
        System.out.println(">> Done\n");
        return retVal

;
    }
}

在下面的代码片段中,您可以看到一个简单的驱动程序应用,它通过使用SimpleAdviceSimpleStaticPointcut类创建了一个DefaultPointcutAdvisor的实例。此外,因为两个类实现了相同的接口,所以您可以看到代理可以基于接口而不是具体的类来创建。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Singer;
import org.aopalliance.aop.Advice; import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class StaticPointcutDemo {
    public static void main(String... args) {
        GoodGuitarist johnMayer = new GoodGuitarist();
        GreatGuitarist ericClapton = new GreatGuitarist();

        Singer proxyOne;
        Singer proxyTwo;

        Pointcut pc = new SimpleStaticPointcut();
        Advice advice = new SimpleAdvice();
        Advisor advisor = new DefaultPointcutAdvisor(pc, advice);

        ProxyFactory pf = new ProxyFactory();
        pf.addAdvisor(advisor);
        pf.setTarget(johnMayer);
        proxyOne = (Singer)pf.getProxy();

        pf = new ProxyFactory();
        pf.addAdvisor(advisor);
        pf.setTarget(ericClapton);
        proxyTwo = (Singer)pf.getProxy();

        proxyOne.sing();
        proxyTwo.sing();
    }
}

注意,DefaultPointcutAdvisor实例随后被用来创建两个代理:一个用于GoodGuitarist的实例,另一个用于EricClapton的实例。最后,在两个代理上调用sing()方法。运行此示例会产生以下输出:

>> Invoking sing
Who says I can't be free
From all of the things that I used to be
>> Done

I shot the sheriff,
But I did not shoot the deputy

正如你所看到的,实际上唯一被调用的SimpleAdvice方法是GoodGuitarist类的sing()方法,正如预期的那样。限制建议应用的方法非常简单,而且,正如您将在我们讨论代理选项时看到的,这是让您的应用获得最佳性能的关键。

使用 DyanmicMethodMatcherPointcut 创建动态切入点

创建一个动态切入点和创建一个静态切入点没有太大的不同,所以对于这个例子,我们将为下面显示的类创建一个动态切入点:

package com.apress.prospring5.ch5;

public class SampleBean {
    public void foo(int x) {
        System.out.println("Invoked foo() with: " + x);
    }

    public void bar() {
        System.out.println("Invoked bar()");
    }
}

对于这个例子,我们只想通知foo()方法,但是与前面的例子不同,我们只想在传递给它的int参数大于或小于 100 时通知这个方法。

与静态切入点一样,Spring 为创建动态切入点提供了一个方便的基类:DynamicMethodMatcherPointcutDynamicMethodMatcherPointcut类有一个您必须实现的抽象方法matches(Method, Class<?>, Object[])(通过它实现的MethodMatcher接口),但是正如您将看到的,实现matches(Method, Class<?>)方法来控制静态检查的行为也是谨慎的。下面的代码片段显示了SimpleDynamicPointcut类:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;

public class SimpleDynamicPointcut
              extends DynamicMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> cls) {
        System.out.println("Static check for " + method.getName());
        return ("foo".equals(method.getName()));
    }

    @Override
    public boolean matches(Method method, Class<?> cls, Object args) {
        System.out.println("Dynamic check for " + method.getName());

        int x = ((Integer) args0).intValue();

        return (x != 100);
    }

    @Override
    public ClassFilter getClassFilter() {
        return cls -> (cls == SampleBean.class);
    }
}

正如您在前面的代码示例中看到的,我们以与上一节类似的方式覆盖了getClassFilter()方法。这消除了在方法匹配方法中检查类的需要,而这对于动态检查是特别重要的。虽然只要求您实现动态检查,但是我们也实现静态检查。这样做的原因是你知道bar()方法永远不会被推荐。通过使用静态检查来表明这一点,Spring 永远不必为此方法执行动态检查。这是因为当实现静态检查方法时,Spring 将首先检查它,如果检查结果不匹配,Spring 将停止任何进一步的动态检查。此外,静态检查的结果将被缓存以获得更好的性能。但是如果我们忽略了静态检查,Spring 会在每次调用bar()方法时执行一次动态检查。推荐的做法是,在getClassFilter()方法中执行类检查,在matches(Method, Class<?>)方法中执行方法检查,在matches(Method, Class<?>, Object[])方法中执行参数检查。这将使您的切入点更容易理解和维护,性能也会更好。

matches(Method, Class<?>, Object[])方法中,你可以看到如果传递给foo()方法的int参数的值不等于 100,我们就返回false;否则,我们返回true。注意,在动态检查中,我们知道我们正在处理foo()方法,因为没有其他方法通过静态检查。在下面的代码片段中,您可以看到这个切入点的一个实例:

package com.apress.prospring5.ch5;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class DynamicPointcutDemo {
    public static void main(String... args) {
        SampleBean target = new SampleBean();

        Advisor advisor = new DefaultPointcutAdvisor(
            new SimpleDynamicPointcut(), new SimpleAdvice());

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        SampleBean proxy = (SampleBean)pf.getProxy();

        proxy.foo(1);
        proxy.foo(10);
        proxy.foo(100);

        proxy.bar();
        proxy.bar();
        proxy.bar();

    }
}

请注意,我们使用了与静态切入点示例中相同的通知类。然而,在这个例子中,应该只通知对foo()的前两次调用。动态检查防止第三次调用foo()被告知,静态检查防止bar()方法被告知。运行此示例会产生以下输出:

Static check for bar

Static check for foo
Static check for toString
Static check for clone
Static check for foo
Dynamic check for foo
>> Invoking foo
Invoked foo() with: 1
>> Done

Dynamic check for foo
>> Invoking foo
Invoked foo() with: 10
>> Done

Dynamic check for foo
Invoked foo() with: 100
Static check for bar
Invoked bar()
Invoked bar()
Invoked bar()

正如我们所料,只建议了前两次调用foo()方法。注意,由于对bar()的静态检查,没有一个bar()调用受到动态检查。这里值得注意的一点是,foo()方法受到两次静态检查:一次是在初始阶段检查所有方法,另一次是在第一次调用时。

正如您所看到的,动态切入点比静态切入点提供了更大程度的灵活性,但是由于它们需要额外的运行时开销,您应该只在绝对必要的时候使用动态切入点。

使用简单的名称匹配

通常当创建一个切入点时,我们希望仅仅基于方法的名字来匹配,忽略方法签名和返回类型。在这种情况下,您可以避免创建StaticMethodMatcherPointcut的子类,而是使用NameMatchMethodPointcut(它是StaticMethodMatcherPointcut的子类)来匹配方法名列表。当你使用NameMatchMethodPointcut时,不考虑方法的签名,所以如果你有方法sing()sing(guitar),它们都匹配名字foo

在下面的代码片段中,您可以看到GrammyGuitarist类,它是Singer的另一个实现,因为这位格莱美奖歌手用他的声音唱歌,用吉他,并且作为人类,偶尔会说话和休息。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;

public class GrammyGuitarist implements Singer {

        @Override public void sing() {
                System.out.println("sing: Gravity is working against me\n" +
                                "And gravity wants to bring me down");
        }

        public void sing(Guitar guitar) {
                System.out.println("play: " + guitar.play());
        }

        public void rest(){
                System.out.println("zzz");
        }

        public void talk(){
                System.out.println("talk");
        }
}

//chapter02/hello-world/src/main/java/com/apress/prospring5/ch2/common/Guitar.java
package com.apress.prospring5.ch2.common;

public class Guitar {

        public String play(){
                return "G C G C Am D7";
        }
}

对于这个例子,我们想通过使用NameMatchMethodPointcut;来匹配sing()sing(Guitar)rest()方法。这转化为匹配名字foobar。这显示在下面的代码片段中:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;

public class NamePointcutDemo {

    public static void main(String... args) {
        GrammyGuitarist johnMayer = new GrammyGuitarist();

        NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
        pc.addMethodName("sing");
        pc.addMethodName("rest");

        Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(johnMayer);
        pf.addAdvisor(advisor);

        GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();
        proxy.sing();
        proxy.sing(new Guitar());
        proxy.rest();
        proxy.talk();
    }
}

没有必要为切入点创建一个类;您可以简单地创建一个NameMatchMethodPointcut的实例,然后您就上路了。注意,我们已经使用addMethodName()方法向切入点添加了两个方法名,singrest。运行此示例会产生以下输出:

>> Invoking sing
sing: Gravity is working against me
And gravity wants to bring me down
>> Done

>> Invoking sing
play: G C G C Am D7
>> Done

>> Invoking rest
zzz
>> Done

talk

正如预期的那样,由于切入点的原因,singsing(Guitar)rest方法被推荐,但是talk()方法没有被推荐。

用正则表达式创建切入点

在上一节中,我们讨论了如何根据预定义的方法列表执行简单的匹配。但是,如果您事先不知道所有方法的名称,而是知道名称遵循的模式,该怎么办呢?例如,如果您想匹配名称以get开头的所有方法,该怎么办?在这种情况下,您可以使用正则表达式切入点JdkRegexpMethodPointcut来匹配基于正则表达式的方法名。这里您可以看到另一个Guitarist类,它包含三个方法:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Singer;

public class Guitarist implements Singer {

        @Override public void sing() {
                System.out.println("Just keep me where  the light is");
        }

        public void sing2() {
                System.out.println("Just keep me where  the light is");
        }

        public void rest() {
                System.out.println("zzz");
        }
}

使用基于正则表达式的切入点,我们可以匹配这个类中名称以string开头的所有方法。如下所示:

package com.apress.prospring5.ch5;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;

public class RegexpPointcutDemo {
    public static void main(String... args) {
         Guitarist johnMayer = new Guitarist();

         JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
         pc.setPattern(".*sing.*");
         Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());

         ProxyFactory pf = new ProxyFactory();
          pf.setTarget(johnMayer);
         pf.addAdvisor(advisor);
         Guitarist proxy = (Guitarist) pf.getProxy();

         proxy.sing();
         proxy.sing2();
         proxy.rest();
    }
}

注意,我们不需要为切入点创建一个类;相反,我们只需创建一个JdkRegexpMethodPointcut的实例,并指定要匹配的模式,我们就完成了。要注意的有趣的事情是模式。当匹配方法名时,Spring 匹配方法的完全限定名,所以对于sing1(),Spring 匹配com.apress.prospring5.ch5.Guitarist.sing1,这就是为什么模式中有前导.*。这是一个强大的概念,因为它允许您匹配给定包中的所有方法,而不需要确切地知道包中有哪些类以及方法的名称是什么。运行此示例会产生以下输出:

>> Invoking sing
Just keep me where the light is
>> Done

>> Invoking sing2
Oh gravity, stay the hell away from me
>> Done

zzz

如您所料,只建议使用sing()sing2()方法,因为rest()方法与正则表达式模式不匹配。

用 AspectJ 切入点表达式创建切入点

除了 JDK 正则表达式,还可以使用 AspectJ 的切入点表达式语言进行切入点声明。在本章的后面,你会看到当我们使用aop名称空间在 XML 配置中声明切入点时,Spring 默认使用 AspectJ 的切入点语言。而且,在使用 Spring 的@AspectJ注释式 AOP 支持时,需要使用 AspectJ 的切入点语言。所以当使用表达式语言声明切入点时,使用 AspectJ 切入点表达式是最好的方法。Spring 提供了通过 AspectJ 的表达式语言定义切入点的类AspectJExpressionPointcut。要在 Spring 中使用 AspectJ 切入点表达式,需要在项目的类路径中包含两个 AspectJ 库文件,aspectjrt.jaraspectjweaver.jar。依赖项及其版本在主build.gradle配置文件中配置(并配置为chapter05项目所有模块的依赖项)。

ext {
    aspectjVersion = '1.9.0.BETA-5'
...
    misc = [
        ...
        Aspectjweaver     : "org.aspectj:aspectjweaver:$aspectjVersion",
        Aspectjrt         : "org.aspectj:aspectjrt:$aspectjVersion"
    ]
...

考虑到前面的Guitarist类的实现,用 JDK 正则表达式实现的相同功能可以用 AspectJ 表达式来实现。下面是代码:

package com.apress.prospring5.ch5;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class AspectjexpPointcutDemo {

    public static void main(String... args) {
         Guitarist johnMayer = new Guitarist();

        AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
        pc.setExpression("execution(* sing*(..))");
        Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(johnMayer);
        pf.addAdvisor(advisor);
        Guitarist proxy = (Guitarist) pf.getProxy();

        proxy.sing();
        proxy.sing2();
        proxy.rest();
    }
}

注意,我们使用AspectJExpressionPointcutsetExpression()方法来设置匹配标准。表达式execution(* sing*(..))意味着通知应该应用于任何以sing开始、有任何参数并返回任何类型的方法的执行。运行该程序将得到与前面使用 JDK 正则表达式的例子相同的结果。

创建匹配切入点的注释

如果您的应用是基于注释的,您可能希望使用您自己指定的注释来定义切入点,也就是说,将通知逻辑应用到所有具有特定注释的方法或类型。Spring 提供了使用注释定义切入点的类AnnotationMatchingPointcut。同样,让我们重用前面的例子,看看在使用注释作为切入点时如何做。

首先我们定义一个名为AdviceRequired的注释,这是一个我们将用来声明切入点的注释。以下代码片段显示了批注类:

package com.apress.prospring5.ch5;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AdviceRequired {
}

在前面的代码示例中,您可以看到我们通过使用@interface作为类型将接口声明为注释,并且@Target注释定义了注释可以应用于类型或方法级别。下面的代码片段显示了另一个Guitarist类的实现,其中一个方法带有您的注释:

package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;

public class Guitarist implements Singer {

        @Override public void sing() {
                System.out.println("Dream of ways to throw it all away");
        }

        @AdviceRequired

        public void sing(Guitar guitar) {
                System.out.println("play: " + guitar.play());
        }

        public void rest(){
                System.out.println("zzz");
        }
}

以下代码片段显示了测试程序:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;

public class AnnotationPointcutDemo {
    public static void main(String... args) {
        Guitarist johnMayer = new Guitarist();

        AnnotationMatchingPointcut pc = AnnotationMatchingPointcut
            .forMethodAnnotation(AdviceRequired.class);
        Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice())

;

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(johnMayer);
        pf.addAdvisor(advisor);
        Guitarist proxy = (Guitarist) pf.getProxy();

        proxy.sing(new Guitar());
        proxy.rest();
    }
}

在前面的清单中,AnnotationMatchingPointcut的一个实例是通过调用它的静态方法forMethodAnnotation()并传入注释类型获得的。这表明我们希望将建议应用到所有用给定注释注释的方法中。也可以通过调用forClassAnnotation()方法来指定在类型级别应用的注释。下面显示了程序运行时的输出:

>> Invoking sing
play: G C G C Am D7
>> Done

zzz

如您所见,因为我们注释了sing()方法,所以只建议使用该方法。

便利顾问实现

对于许多Pointcut实现,Spring 还提供了一个方便的Advisor实现作为切入点。例如,在前面的例子中,我们没有使用NameMatchMethodPointcutDefaultPointcutAdvisor,而是简单地使用了NameMatchMethodPointcutAdvisor,如下面的代码片段所示:

package com.apress.prospring5.ch5;
...
import org.springframework.aop.support.NameMatchMethodPointcutAdvisor;

public class NamePointcutUsingAdvisor {
        public static void main(String... args) {
                GrammyGuitarist johnMayer = new GrammyGuitarist();

                NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
                pc.addMethodName("sing");
                pc.addMethodName("rest");

                Advisor advisor =
                  new NameMatchMethodPointcutAdvisor(new SimpleAdvice());
                ProxyFactory pf = new ProxyFactory();
                pf.setTarget(johnMayer);
                pf.addAdvisor(advisor);

                GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();
                proxy.sing();
                proxy.sing(new Guitar());
                proxy.rest();
                proxy.talk();
        }
}

注意,我们没有创建NameMatchMethodPointcut的实例,而是在NameMatchMethodPointcutAdvisor的实例上配置了切入点细节。这样,NameMatchMethodPointcutAdvisor既是顾问又是切入点。

通过研究org.springframework.aop.support包的 Javadoc,您可以找到不同Advisor实现的全部细节。这两种方法之间没有明显的性能差异,除了第二个示例中的代码稍微少一些之外,实际的编码方法几乎没有什么不同。我们更喜欢坚持第一种方法,因为我们觉得代码中的意图稍微清晰一些。归根结底,你选择的风格取决于个人喜好。

了解代理

到目前为止,我们只是粗略地看了一下由ProxyFactory生成的代理。我们提到过 Spring 中有两种类型的代理:使用 JDK Proxy类创建的 JDK 代理和使用 CGLIB Enhancer类创建的基于 CGLIB 的代理。您可能想知道这两种代理之间到底有什么区别,为什么 Spring 需要两种类型的代理。在本节中,我们将详细了解代理之间的差异。

代理的核心目标是拦截方法调用,并在必要时执行适用于特定方法的通知链。通知的管理和调用在很大程度上是独立于代理的,由 Spring AOP 框架管理。然而,代理负责拦截对所有方法的调用,并在必要时将它们传递给 AOP 框架,以便应用建议。

除了这个核心功能之外,代理还必须支持一组附加功能。可以将代理配置为通过AopContext类(一个抽象类)公开自己,这样就可以从目标对象中检索代理并调用代理上的建议方法。代理负责确保当通过ProxyFactory.setExposeProxy()启用该选项时,代理类被适当地公开。此外,默认情况下,所有代理类都实现了Advised接口,这允许在代理创建后修改通知链。代理还必须确保任何返回这个(即返回被代理的目标)的方法确实返回代理而不是目标。

如您所见,一个典型的代理有相当多的工作要做,所有这些逻辑都在 JDK 和 CGLIB 代理中实现。

使用 JDK 动态代理

JDK 代理是 Spring 中最基本的代理类型。与 CGLIB 代理不同,JDK 代理只能生成接口的代理,而不能生成类的代理。这样,您想要代理的任何对象都必须实现至少一个接口,并且生成的代理将是实现该接口的对象。图 5-4 显示了这种代理的抽象模式。

A315511_5_En_5_Fig4_HTML.jpg

图 5-4。

JDK proxy abstract schema

一般来说,为类使用接口是一个好的设计,但这并不总是可行的,尤其是当您使用第三方或遗留代码时。在这种情况下,您必须使用 CGLIB 代理。当您使用 JDK 代理时,所有的方法调用都被 JVM 截获,并被路由到代理的invoke()方法。然后,该方法确定所讨论的方法是否被建议(由切入点定义的规则),如果是,它调用建议链,然后通过使用反射调用方法本身。除此之外,invoke()方法执行上一节讨论的所有逻辑。

JDK 代理在进入invoke()方法之前,不会在建议方法和未建议方法之间做出决定。这意味着对于代理上未修改的方法,仍然调用invoke()方法,仍然执行所有检查,并且仍然通过使用反射调用该方法。显然,每次调用方法时,这都会导致运行时开销,即使代理除了通过反射调用未经修改的方法之外,通常不执行任何额外的处理。

您可以通过使用setInterfaces()(在ProxyFactory类间接扩展的AdvisedSupport类中)指定要代理的接口列表来指示ProxyFactory使用 JDK 代理。

使用 CGLIB 代理

使用 JDK 代理,每次调用方法时,所有关于如何处理特定方法调用的决定都在运行时处理。当您使用 CGLIB 时,CGLIB 会为每个代理动态生成新类的字节码,尽可能重用已经生成的类。在这种情况下,产生的代理类型将是目标对象类的子类。图 5-5 显示了这种代理的抽象模式。

A315511_5_En_5_Fig5_HTML.jpg

图 5-5。

CGLIB proxy abstract schema

当第一次创建 CGLIB 代理时,CGLIB 询问 Spring 它希望如何处理每个方法。这意味着在 JDK 代理上每次调用invoke()时执行的许多决策对于 CGLIB 代理只执行一次。因为 CGLIB 生成实际的字节码,所以处理方法的方式也更加灵活。例如,CGLIB 代理生成适当的字节码来直接调用任何未修改的方法,减少了代理带来的开销。此外,CGLIB 代理确定一个方法是否有可能返回这个;如果没有,它允许直接调用方法调用,再次减少了运行时开销。

CGLIB 代理处理固定通知链的方式也不同于 JDK 代理。固定通知链是在代理生成后保证不会改变的链。默认情况下,即使在代理创建之后,您也可以更改代理上的顾问和建议,尽管这很少是必需的。CGLIB 代理以一种特殊的方式处理固定通知链,减少了执行通知链的运行时开销。

比较代理性能

到目前为止,我们所做的只是泛泛地讨论了代理类型之间在实现上的差异。在本节中,我们将运行一个简单的测试来比较 CGLIB 代理和 JDK 代理的性能。

让我们创建一个名为DefaultSimpleBean的类,我们将使用它作为代理的目标对象。下面是SimpleBean接口和DefaultSimpleBean类:

ppackage com.apress.prospring5.ch5;

public interface SimpleBean {
    void advised();
    void unadvised();
}

public class DefaultSimpleBean implements SimpleBean {
    private long dummy = 0;

    @Override
    public void advised() {
        dummy = System.currentTimeMillis();
    }

    @Override
    public void unadvised() {
        dummy = System.currentTimeMillis();
    }

}

在下面的例子中,TestPointcut类提供了对建议方法的静态检查:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.support.StaticMethodMatcherPointcut;

public class TestPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class cls) {
        return ("advise".equals(method.getName()));
    }
}

下一个代码片段描述了NoOpBeforeAdvice类,它只是在没有任何操作的通知之前很简单:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class NoOpBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object args, Object target)
        throws Throwable {
        // no-op
    }
}

在下面的代码片段中,您可以看到用于测试不同类型代理的代码:

package com.apress.prospring5.ch5;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class ProxyPerfTest {
    public static void main(String... args) {
        SimpleBean target = new DefaultSimpleBean();

        Advisor advisor = new DefaultPointcutAdvisor(new TestPointcut(),
                new NoOpBeforeAdvice());

        runCglibTests(advisor, target);
        runCglibFrozenTests(advisor, target);
        runJdkTests(advisor, target);
    }

    private static void runCglibTests(Advisor advisor, SimpleBean target) {
        ProxyFactory pf = new ProxyFactory();
        pf.setProxyTargetClass(true);
        pf.setTarget(target);
        pf.addAdvisor(advisor);

        SimpleBean proxy = (SimpleBean)pf.getProxy();
        System.out.println("Running CGLIB (Standard) Tests");
        test(proxy);
    }

    private static void runCglibFrozenTests(Advisor advisor, SimpleBean target) {
        ProxyFactory pf = new ProxyFactory();
        pf.setProxyTargetClass(true);
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        pf.setFrozen(true);

        SimpleBean proxy = (SimpleBean) pf.getProxy();
        System.out.println("Running CGLIB (Frozen) Tests");
        test(proxy);
    }

    private static void runJdkTests(Advisor advisor, SimpleBean target) {
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        pf.setInterfaces(new Class{SimpleBean.class});

        SimpleBean proxy = (SimpleBean)pf.getProxy();
        System.out.println("Running JDK Tests");
        test(proxy);
    }

    private static void test(SimpleBean bean) {
        long before = 0;
        long after = 0;

        System.out.println("Testing Advised Method");
        before = System.currentTimeMillis();
        for(int x = 0; x < 500000; x++) {
            bean.advised();
        }
        after = System.currentTimeMillis();

        System.out.println("Took " + (after - before) + " ms");

        System.out.println("Testing Unadvised Method");
        before = System.currentTimeMillis();
        for(int x = 0; x < 500000; x++) {
            bean.unadvised();
        }
        after = System.currentTimeMillis();

        System.out.println("Took " + (after - before) + " ms");

        System.out.println("Testing equals() Method");
        before = System.currentTimeMillis();
        for(int x = 0; x < 500000; x++) {
            bean.equals(bean);
        }
        after = System.currentTimeMillis();

        System.out.println("Took " + (after - before) + " ms");

        System.out.println("Testing hashCode() Method");
        before = System.currentTimeMillis();
        for(int x = 0; x < 500000; x++) {
            bean.hashCode();
        }
        after = System.currentTimeMillis();

        System.out.println("Took " + (after - before) + " ms");

        Advised advised = (Advised)bean;

        System.out.println("Testing Advised.getProxyTargetClass() Method");
        before = System.currentTimeMillis();
        for(int x = 0; x < 500000; x++) {
            advised.getTargetClass();
        }
        after = System.currentTimeMillis();

        System.out.println("Took " + (after - before) + " ms");

        System.out.println(">>>\n");
    }
}

在这段代码中,您可以看到您正在测试三种代理:

  • 标准的 CGLIB 代理
  • 一个冻结通知链的 CGLIB 代理(即当一个代理通过调用ProxyFactory间接扩展的ProxyConfig类中的setFrozen()方法被冻结时,CGLIB 会进行进一步优化;但是,不允许进一步更改建议)
  • JDK 代理人

对于每种代理类型,您运行以下五个测试用例:

  • 建议的方法(测试 1):这是一种建议的方法。测试中使用的通知类型在不执行任何处理的通知之前,因此它减少了通知对性能测试的影响。
  • 未修改的方法(测试 2):这是代理上未修改的方法。通常你的代理有很多不被推荐的方法。这个测试着眼于未修改的方法在不同代理上的表现。
  • equals()方法(测试 3):这个测试查看调用equals()方法的开销。当您使用代理作为HashMap或类似集合中的键时,这尤其重要。
  • hashCode()方法(测试 4):与equals()方法一样,当您使用HashMap或类似的集合时,hashCode()方法很重要。
  • 在 Advised 接口上执行方法(测试 5):正如我们前面提到的,默认情况下,代理实现了Advised接口,允许您在创建后修改代理并查询关于代理的信息。这个测试着眼于使用不同的代理类型访问Advised接口上的方法有多快。

表 5-3 显示了这些测试的结果。

表 5-3。

Proxy Performance Test Results (in Milliseconds)

|   | CGLIB(标准) | CGLIB(冻结) | java 开发工具包 | | --- | --- | --- | --- | | 建议的方法 | Two hundred and forty-five | One hundred and thirty-five | Two hundred and twenty-four | | 非修正方法 | Ninety-two | forty-two | seventy-eight | | `equals()` | nine | six | Seventy-seven | | `hashCode()` | Twenty-nine | Thirteen | Twenty-three | | `Advised.getProxyTargetClass()` | nine | six | Fifteen |

正如你所看到的,标准 CGLIB 和 JDK 动态代理对于建议的和未建议的方法的性能差别不大。与往常一样,这些数字会因硬件和所用的 JDK 而异。

然而,当您使用带有冻结通知链的 CGLIB 代理时,有一个明显的区别。类似的数字也适用于equals()hashCode()方法,当您使用 CGLIB 代理时,这两个方法明显更快。对于Advised接口上的方法,你会注意到它们在 CGLIB 冻结代理上也更快。原因是Advised方法在intercept()方法的早期被处理,因此它们避免了其他方法所需的许多逻辑。

选择要使用的代理

决定使用哪个代理通常很容易。CGLIB 代理可以代理类和接口,而 JDK 代理只能代理接口。在性能方面,JDK 和 CGLIB 标准模式之间没有明显的区别(至少在运行建议的和未建议的方法时),除非您在冻结模式下使用 CGLIB,在这种情况下,建议链不能更改,CGLIB 在冻结模式下执行进一步的优化。代理类时,CGLIB 代理是默认选择,因为它是唯一能够生成类代理的代理。要在代理接口时使用 CGLIB 代理,您必须通过使用setOptimize()方法将ProxyFactory中的optimize标志的值设置为true

切入点的高级使用

在本章的前面,我们看了 Spring 提供的六个基本的Pointcut实现;在很大程度上,我们发现这些满足了我们应用的需求。然而,在定义切入点时,有时您可能需要更多的灵活性。Spring 提供了两个额外的Pointcut实现,ComposablePointcutControlFlowPointcut,它们提供了您所需要的灵活性。

使用控制流切入点

ControlFlowPointcut类实现的 Spring 控制流切入点类似于许多其他 AOP 实现中可用的cflow构造,尽管它们没有那么强大。本质上,Spring 中的控制流切入点适用于一个给定方法下或一个类中所有方法下的所有方法调用。这很难想象,最好用一个例子来解释。

下面的代码片段显示了一个SimpleBeforeAdvice类,它写出一条描述它所建议的方法的消息:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object args, Object target)
            throws Throwable {
        System.out.println("Before method: " + method);
    }
}

这个通知类允许我们查看ControlFlowPointcut适用于哪些方法。在这里,您可以看到简单的TestBean类:

package com.apress.prospring5.ch5;

public class TestBean {
    public void foo() {
        System.out.println("foo()");
    }
}

可以看到我们要建议的简单foo()方法。然而,我们有一个特殊的要求:只有当从另一个特定的方法调用这个方法时,我们才希望通知这个方法。以下代码片段显示了该示例的简单驱动程序:

package com.apress.prospring5.ch5;

import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ControlFlowPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class ControlFlowDemo {
    public static void main(String... args) {
        ControlFlowDemo ex = new ControlFlowDemo();
        ex.run();
    }

    public void run() {
        TestBean target = new TestBean();

        Pointcut pc = new ControlFlowPointcut(ControlFlowDemo.class,
            "test");
        Advisor advisor = new DefaultPointcutAdvisor(pc,
            new SimpleBeforeAdvice());

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);

        TestBean proxy = (TestBean) pf.getProxy();

        System.out.println("\tTrying normal invoke");
        proxy.foo();
        System.out.println("\n\tTrying under ControlFlowDemo.test()");
        test(proxy);
    }
    private void test(TestBean bean) {
        bean.foo();
    }
}

在前面的代码片段中,advised 代理与ControlFlowPointcut组装在一起,然后foo()方法被调用两次,一次直接从main()方法调用,一次从test()方法调用。下面是特别有趣的一行:

        Pointcut pc = new ControlFlowPointcut(ControlFlowDemo.class, "test");

在这一行中,我们为ControlFlowDemo类的test()方法创建了一个ControlFlowPointcut实例。本质上,这意味着,“切入点是从ControlFlowExample.test()方法调用的所有方法。”注意,尽管我们说“切入点所有方法”,实际上这实际上意味着“切入点代理对象上的所有方法,该代理对象被建议使用对应于这个ControlFlowPointcut实例的Advisor运行前面的示例会在控制台中产生以下结果:

        Trying normal invoke
foo()

        Trying under ControlFlowDemo.test()
Before method: public void com.apress.prospring5.ch5.TestBean.foo()
foo()

如您所见,当第一次在test()方法的控制流之外调用sing()方法时,它是未修改的。当它第二次执行时,这一次是在test()方法的控制流中,ControlFlowPointcut指示它的相关通知适用于该方法,因此该方法被通知。注意,如果我们从test()方法中调用了另一个方法,一个不在被通知的代理上的方法,它就不会被通知。

控制流切入点非常有用,允许您仅在一个对象在另一个对象的上下文中执行时选择性地通知该对象。但是,请注意,与其他切入点相比,使用控制流切入点会对性能造成很大影响。

让我们考虑一个例子。假设我们有一个事务处理系统,它包含一个TransactionService接口和一个AccountService接口。我们希望在通知后应用,以便当TransactionService.reverseTransaction()调用AccountService.updateBalance()方法时,在帐户余额更新后,向客户发送电子邮件通知。但是,在任何其他情况下都不会发送电子邮件。在这种情况下,控制流切入点将会很有用。图 5-6 显示了这个场景的 UML 序列图。

A315511_5_En_5_Fig6_HTML.jpg

图 5-6。

UML sequence diagram for a control flow pointcut

使用可组合的切入点

在前面的切入点例子中,我们只为每个Advisor使用了一个切入点。在大多数情况下,这通常就足够了,但是在某些情况下,您可能需要将两个或更多的切入点组合在一起,以实现期望的目标。假设您想要横切 bean 上的所有 getter 和 setter 方法。您有一个 getters 的切入点和一个 setters 的切入点,但是您没有两个切入点。当然,您可以用新的逻辑创建另一个切入点,但是更好的方法是通过使用ComposablePointcut将两个切入点合并成一个切入点。

ComposablePointcut支持两种方式:union()intersection()。默认情况下,ComposablePointcut是用匹配所有类的ClassFilter和匹配所有方法的MethodMatcher创建的,尽管您可以在构建期间提供自己的初始ClassFilterMethodMatcherunion()intersection()方法都被重载以接受ClassFilterMethodMatcher参数。

可以通过传入ClassFilterMethodMatcherPointcut接口的实例来调用ComposablePointcut.union()方法。联合操作的结果是ComposablePointcut将在它的调用链中添加一个“或”条件来匹配连接点。对于ComposablePointcut.intersection()方法也是如此,但是这次将添加一个“and”条件,这意味着ComposablePointcut中的所有ClassFilterMethodMatcherPointcut定义都应该匹配以应用一个建议。您可以将它想象成 SQL 查询中的WHERE子句,其中的union()方法类似于“or”操作符,而intersection()方法类似于“and”操作符。

与控制流切入点一样,这很难可视化,但通过一个例子就更容易理解了。以下示例显示了前一示例中使用的GrammyGuitarist类及其四个方法:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;

public class GrammyGuitarist implements Singer {

        @Override public void sing() {
                System.out.println("sing: Gravity is working against me\n" +
                                "And gravity wants to bring me down");
        }

        public void sing(Guitar guitar) {
                System.out.println("play: " + guitar.play());
        }

        public void rest(){
                System.out.println("zzz");
        }

        public void talk(){
                System.out.println("talk");
        }
}

在这个例子中,我们将使用同一个ComposablePointcut实例生成三个代理,但是每次,我们都将使用union()intersection()方法来修改ComposablePointcut。接下来,我们将调用target bean 代理上的所有三个方法,并查看哪些方法已经被通知。以下代码示例对此进行了描述:

package com.apress.prospring5.ch5;

import java.lang.reflect.Method;

import org.springframework.aop.Advisor;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcher;

public class ComposablePointcutExample {
    public static void main(String... args) {
        GrammyGuitarist johnMayer = new GrammyGuitarist();

        ComposablePointcut pc = new ComposablePointcut(ClassFilter.TRUE,
            new SingMethodMatcher());

        System.out.println("Test 1 >> ");
         GrammyGuitarist proxy = getProxy(pc, johnMayer);
        testInvoke(proxy);

        System.out.println();

        System.out.println("Test 2 >> ");
        pc.union(new TalkMethodMatcher());
        proxy = getProxy(pc, johnMayer);
        testInvoke(proxy);
        System.out.println();

        System.out.println("Test 3 >> ");
        pc.intersection(new RestMethodMatcher());
        proxy = getProxy(pc, johnMayer);
        testInvoke(proxy);
    }

    private static GrammyGuitarist getProxy(ComposablePointcut pc,
            GrammyGuitarist target) {
        Advisor advisor = new DefaultPointcutAdvisor(pc,
            new SimpleBeforeAdvice());

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        return (GrammyGuitarist) pf.getProxy();
    }

    private static void testInvoke(GrammyGuitarist proxy) {
        proxy.sing();
        proxy.sing(new Guitar());
        proxy.talk();
        proxy.rest();
    }

    private static class SingMethodMatcher extends StaticMethodMatcher {
        @Override
        public boolean matches(Method method, Class<?> cls) {
            return (method.getName().startsWith("si"));
        }
    }

    private static class TalkMethodMatcher extends StaticMethodMatcher {
        @Override
        public boolean matches(Method method, Class<?> cls) {
            return "talk".equals(method.getName());
        }
    }

    private static class RestMethodMatcher extends StaticMethodMatcher {
    @Override
        public boolean matches(Method method, Class<?> cls) {
            return (method.getName().endsWith("st"));
        }
    }
}

在这个例子中首先要注意的是三个私有的MethodMatcher实现。SingMethodMatcher匹配所有以get开头的方法。这是我们用来组装ComposablePointcut的默认MethodMatcher。因此,我们预计对GrammyGuitarist方法的第一轮调用将导致只建议sing()方法。

TalkMethodMatcher匹配所有名为talk的方法,并通过使用union()进行第二轮调用来与ComposablePointcut结合。此时,我们有了两个MethodMatcher的并集——一个匹配所有以si开头的方法,另一个匹配所有名为talk的方法。我们现在期望第二轮中的所有调用都将被通知。TalkMethodMatcher非常具体,只匹配talk()方法。通过使用第三轮调用的intersection()将此MethodMatcherComposablePointcut结合。

因为RestMethodMatcher是通过使用intersection()来组合的,所以我们预计在第三轮中不会推荐任何方法,因为没有方法匹配所有组合的MethodMatcher

运行此示例会产生以下输出:

Test 1 >>
Before method: public void
       com.apress.prospring5.ch5.GrammyGuitarist.sing()

sing: Gravity is working against me
And gravity wants to bring me down
Before method: public void com.apress.prospring5.ch5.
    GrammyGuitarist.sing(com.apress.prospring5.ch2.common.Guitar)
play: G C G C Am D7
talk
zzz

Test 2 >>
Before method: public void
       com.apress.prospring5.ch5.GrammyGuitarist.sing()

sing: Gravity is working against me
And gravity wants to bring me down
Before method: public void
      com.apress.prospring5.ch5.GrammyGuitarist.talk()

Before method: public void com.apress.prospring5.ch5.
      GrammyGuitarist.sing(com.apress.prospring5.ch2.common.Guitar)
play: G C G C Am D7
talk
zzz

Test 3 >>
sing: Gravity is working against me
And gravity wants to bring me down
talk
zzz

虽然这个例子只演示了在合成过程中使用MethodMatcher s,但是在构建切入点时使用ClassFilter也同样简单。事实上,在构建复合切入点时,您可以使用MethodMatcherClassFilter的组合。

组合和切入点接口

在上一节中,您看到了如何通过使用多个MethodMatchersClassFilter来创建复合切入点。您还可以通过使用实现了Pointcut接口的其他对象来创建复合切入点。

构建复合切入点的另一种方法是使用org.springframework.aop.support.Pointcuts类。该类提供了三个静态方法。intersection()union()方法都以两个切入点作为参数来构造一个复合切入点。另一方面,提供了一个matches(Pointcut, Method, Class, Object[])方法来快速检查切入点是否与提供的方法、类和方法参数匹配。

Pointcuts类只支持两个切入点的操作。所以,如果你需要将MethodMatcherClassFilterPointcut结合起来,你需要使用ComposablePointcut类。然而,当您只需要组合两个切入点时,Pointcuts类会更方便。

切入点摘要

Spring 提供了一组强大的Pointcut实现,可以满足应用的大部分需求。请记住,如果您找不到适合您需求的切入点,您可以通过实现PointcutMethodMatcherClassFilter从头开始创建您自己的实现。

您可以使用两种模式来组合切入点和顾问。第一种模式,也是我们到目前为止使用的模式,涉及到将切入点实现从顾问中分离出来。在我们到目前为止看到的代码中,我们创建了Pointcut实现的实例,然后使用DefaultPointcutAdvisor实现将通知和Pointcut一起添加到代理中。

第二种选择是将Pointcut封装在您自己的Advisor实现中,Spring 文档中的许多示例都采用了这种选择。这样,你就有了一个同时实现了PointcutPointcutAdvisor的类,而PointcutAdvisor.getPointcut()方法只是简单地返回这个。这是许多班级,比如StaticMethodMatcherPointcutAdvisor,在 Spring 中使用的方法。我们发现第一种方法是最灵活的,允许您使用不同的Pointcut实现和不同的Advisor实现。然而,第二种方法在您将在应用的不同部分或者跨许多应用使用相同的PointcutAdvisor组合的情况下是有用的。

当每个Advisor必须有一个单独的Pointcut实例时,第二种方法是有用的;通过让Advisor负责创建Pointcut,您可以确保这一点。如果您还记得上一章中关于代理性能的讨论,您会记得未经修改的方法比被建议的方法性能好得多。出于这个原因,你应该确保,通过使用Pointcuts,你只建议绝对必要的方法。这样,您可以通过使用 AOP 来减少应用中不必要的开销。

开始介绍

介绍是 Spring 中可用的 AOP 特性集的重要部分。通过使用引入,您可以动态地向现有对象引入新功能。在 Spring 中,您可以向现有对象引入任何接口的实现。您很可能想知道这到底为什么有用。当您可以在开发时简单地添加功能时,为什么要在运行时动态添加功能呢?这个问题的答案很简单。当功能是横切的,并且使用传统的建议不容易实现时,您可以动态地添加功能。

简介基础

Spring 将介绍视为一种特殊类型的建议,更确切地说,是一种特殊类型的迂回建议。因为介绍只适用于类级别,所以不能在介绍中使用切入点;语义上,两者不匹配。简介向类中添加新的接口实现,切入点定义通知应用于哪些方法。您通过实现IntroductionInterceptor接口来创建一个介绍,该接口扩展了MethodInterceptorDynamicIntroductionAdvice接口。图 5-7 显示了这个结构以及两个接口的方法,正如 IntelliJ IDEA UML 插件所描述的。如您所见,MethodInterceptor接口定义了一个invoke()方法。使用此方法,您可以为正在引入的接口提供实现,并根据需要对任何其他方法执行拦截。在一个方法中实现一个接口的所有方法可能会很麻烦,而且很可能会产生大量代码,您不得不费力地去决定调用哪个方法。幸运的是,Spring 提供了一个默认的IntroductionInterceptor实现,称为DelegatingIntroductionInterceptor,这使得创建介绍更加简单。要使用DelegatingIntroductionInterceptor构建一个介绍,您需要创建一个类,它继承了DelegatingIntroductionInterceptor并实现了您想要介绍的接口。然后,DelegatingIntroductionInterceptor实现简单地将所有对引入方法的调用委托给自身的相应方法。如果这看起来有点不清楚,不要担心;您将在下一节看到它的一个例子。

A315511_5_En_5_Fig7_HTML.jpg

图 5-7。

Interface structure for introductions

正如您在处理切入点建议时需要使用PointcutAdvisor一样,您需要使用IntroductionAdvisor向代理添加介绍。IntroductionAdvisor的默认实现是DefaultIntroductionAdvisor,这应该满足您的大部分(如果不是全部)介绍需求。你应该知道使用ProxyFactory.addAdvice()添加介绍是不允许的,会导致AopConfigException被抛出。相反,您应该使用addAdvisor()方法并传递一个IntroductionAdvisor接口的实例。

当使用标准建议时——也就是说,不是介绍——同一个建议实例有可能用于许多对象。Spring 文档称之为每个类的生命周期,尽管您可以为许多类使用一个 advice 实例。对于介绍,介绍通知构成了被通知对象状态的一部分,因此,对于每个被通知对象,您必须有一个不同的通知实例。这称为每个实例的生命周期。因为您必须确保每个被通知的对象都有一个不同的介绍实例,所以创建一个负责创建介绍通知的子类DefaultIntroductionAdvisor通常更好。这样,您只需要确保为每个对象创建一个 advisor 类的新实例,因为它会自动创建一个新的介绍实例。例如,假设您想在Contact类的所有实例上对setFirstName()方法应用 before advice。图 5-8 显示了适用于所有Contact类型对象的相同建议。现在让我们假设您想要将一个介绍混合到Contact类的所有实例中,并且该介绍将携带每个Contact实例的信息(例如,一个属性isModified指示特定实例是否被修改)。

A315511_5_En_5_Fig8_HTML.jpg

图 5-8。

Per-class life cycle of advice

在这种情况下,将为Contact的每个实例创建一个介绍,并绑定到该特定实例,如图 5-9 所示。这涵盖了创建简介的基础。我们现在将讨论如何使用介绍来解决对象修改检测的问题。

A315511_5_En_5_Fig9_HTML.jpg

图 5-9。

Per-instance introduction

带有介绍的对象修改检测

由于许多原因,对象修改检测是一种有用的技术。通常,在保存对象数据时,应用修改检测来防止不必要的数据库访问。如果一个对象被传递给一个方法进行修改,但是它没有被修改就返回,那么向数据库发出 update 语句就没有什么意义了。以这种方式使用修改检查确实可以增加应用的吞吐量,特别是当数据库已经处于相当大的负载之下或者位于远程网络上,使得通信成为一种昂贵的操作时。

不幸的是,这种功能很难手工实现,因为它需要您添加到每个可以修改对象状态的方法中,以检查对象状态是否真的被修改了。当您考虑所有必须进行的null检查以及查看值是否真正改变的检查时,您会看到每个方法大约有八行代码。您可以将其重构为一个方法,但是每次需要执行检查时,您仍然需要调用这个方法。如果将这种情况扩散到一个有许多需要修改检查的类的典型应用中,灾难就要发生了。

这显然是一个介绍会有所帮助的地方。我们不希望每个需要修改检查的类都从某个基实现继承,从而失去继承的唯一机会,我们也不希望为每个状态改变方法添加检查代码。使用简介,我们可以为修改检测问题提供灵活的解决方案,而不必编写一堆重复的、容易出错的代码。

在这个例子中,我们将使用简介构建一个完整的修改检查框架。修改检查逻辑被封装在IsModified接口中,该接口的实现将被引入到适当的对象中,与拦截逻辑一起自动执行修改检查。出于这个例子的目的,我们使用 JavaBeans 约定,因为我们认为修改是对 setter 方法的任何调用。当然,我们不只是把对 setter 方法的所有调用都视为修改;我们检查传递给 setter 的值是否不同于当前存储在对象中的值。这种解决方案的唯一缺陷是,如果对象上的任何一个值发生了变化,将对象设置回其原始状态仍然会反映出修改。例如,您有一个带有firstName属性的Contact对象。假设在处理过程中,firstName属性从Peter更改为John。因此,该对象被标记为已修改。然而,它仍将被标记为已修改,即使该值在随后的处理中从John变回其原始值Peter。跟踪此类变更的一种方法是存储对象整个生命周期中的全部变更历史。然而,这里的实现并不简单,可以满足大多数需求。实现更完整的解决方案会导致一个过于复杂的例子。

使用 IsModified 接口

修改检查解决方案的核心是IsModified接口,虚拟应用使用它来做出关于对象持久性的智能决策。我们不讨论应用如何使用IsModified;相反,我们将把重点放在介绍的实现上。下面的代码片段显示了IsModified界面:

package com.apress.prospring5.ch5.introduction;

public interface IsModified {
    boolean isModified();
}

这里没有什么特别的—只有一个方法,isModified(),指示对象是否被修改。

创建 Mixin

下一步是创建实现IsModified并被引入到对象中的代码;这被称为 mixin。正如我们前面提到的,通过子类化DelegatingIntroductionInterceptor来创建 mixinss 比通过直接实现IntroductionInterceptor接口来创建 mixin 要简单得多。mixin 类IsModifiedMixinDelegatingIntroductionInterceptor的子类,也实现了IsModified接口。如下所示:

package com.apress.prospring5.ch5.introduction;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;

public class IsModifiedMixin extends DelegatingIntroductionInterceptor
        implements IsModified {
    private boolean isModified = false;

    private Map<Method, Method> methodCache = new HashMap<>();

    @Override
    public boolean isModified() {
        return isModified;
    }

    @Override

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (!isModified) {
            if ((invocation.getMethod().getName().startsWith("set"))
                && (invocation.getArguments().length == 1)) {

                Method getter = getGetter(invocation.getMethod());

                if (getter != null) {
                    Object newVal = invocation.getArguments()0;
                    Object oldVal = getter.invoke(invocation.getThis(), null);
                    if((newVal == null) && (oldVal == null)) {
                        isModified = false;
                    } else if((newVal == null) && (oldVal != null)) {
                        isModified = true;
                    } else if((newVal != null) && (oldVal == null)) {
                        isModified = true;
                    } else {
                        isModified = !newVal.equals(oldVal);
                    }
                }
            }
        }

        return super.invoke(invocation);
    }

    private Method getGetter(Method setter) {
        Method getter = methodCache.get(setter);

        if (getter != null) {
            return getter;
        }

        String getterName = setter.getName().replaceFirst("set", "get");
        try {
            getter = setter.getDeclaringClass().getMethod(getterName, null);
            synchronized (methodCache) {
                methodCache.put(setter, getter);
            }
            return getter;
        } catch (NoSuchMethodException ex) {
            return null;
        }
    }
}

这里首先要注意的是IsModified的实现,它由私有的 modified 字段和isModified()方法组成。这个例子强调了为什么每个被通知的对象必须有一个 mixin 实例——mixin 不仅向对象引入了方法,还引入了状态。如果您在许多对象之间共享这个 mixin 的单个实例,那么您也在共享状态,这意味着当单个对象第一次被修改时,所有对象都显示为已修改。

您实际上不必为 mixin 实现invoke()方法,但是在这种情况下,这样做允许我们在修改发生时自动检测。我们首先只在对象仍未被修改的情况下执行检查;一旦我们知道对象已经被修改,我们就不需要检查修改。接下来,我们检查该方法是否是 setter,如果是,我们检索相应的 getter 方法。注意,我们缓存了 getter/setter 对,以便将来更快地检索。最后,我们将 getter 返回的值与传递给 setter 的值进行比较,以确定是否发生了修改。请注意,我们检查了null的不同可能组合,并适当地设置了修改。重要的是要记住,当您使用DelegatingIntroductionInterceptor时,您必须在覆盖invoke()时调用super.invoke(),因为是DelegatingIntroductionInterceptor将调用分派到正确的位置,要么是被通知的对象,要么是 mixin 本身。

您可以在 mixin 中实现任意多的接口,每个接口都会被自动引入到 advised 对象中。

创建顾问

下一步是创建一个Advisor来包装 mixin 类的创建。这一步是可选的,但是它确实有助于确保 mixin 的一个新实例被用于每个被通知的对象。下面的代码片段显示了IsModifiedAdvisor类:

package com.apress.prospring5.ch5.introduction;

import org.springframework.aop.support.DefaultIntroductionAdvisor;

public class IsModifiedAdvisor extends DefaultIntroductionAdvisor {
    public IsModifiedAdvisor() {
        super(new IsModifiedMixin());
    }
}

注意,我们已经扩展了DefaultIntroductionAdvisor来创建您的IsModifiedAdvisor。这个 advisor 的实现是简单明了的。

把所有的放在一起

现在我们有了一个 mixin 类和一个Advisor类,我们可以测试修改检查框架了。我们将要使用的类是前面提到的Contact类,它是common包的一部分。出于可重用性的原因,这个类经常被用作本书中项目的依赖项。此类的内容如下所示:

package com.apress.prospring5.ch2.common;

public class Contact {

        private String name;
        private String phoneNumber;
        private String email;

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }

        // getters and setter for other fields
        ...
}

这个 bean 有一组属性,但是只有用于测试修改检查 mixin 的name属性。以下代码片段显示了如何组装 advised 代理,然后测试修改检查代码:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Contact;
import com.apress.prospring5.ch5.introduction.IsModified;
import com.apress.prospring5.ch5.introduction.IsModifiedAdvisor;
import org.springframework.aop.IntroductionAdvisor;
import org.springframework.aop.framework.ProxyFactory;

public class IntroductionDemo {
    public static void main(String... args) {
        Contact target = new Contact();
        target.setName("John Mayer");

        IntroductionAdvisor advisor = new IsModifiedAdvisor();

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        pf.setOptimize(true

);

        Contact proxy = (Contact) pf.getProxy();
        IsModified proxyInterface = (IsModified)proxy;

        System.out.println("Is Contact?: " + (proxy instanceof Contact));
        System.out.println("Is IsModified?: " + (proxy instanceof IsModified));

        System.out.println("Has been modified?: " +
            proxyInterface.isModified());

        proxy.setName("John Mayer");

        System.out.println("Has been modified?: " +
            proxyInterface.isModified());

        proxy.setName("Eric Clapton");

        System.out.println("Has been modified?: " +
            proxyInterface.isModified());
    }
}

注意,当我们创建代理时,我们将optimize标志设置为true来强制使用 CGLIB 代理。这样做的原因是,当你使用 JDK 代理引入一个 mixin 时,产生的代理将不会是 object 类的一个实例(在这个例子中是Contact);代理只实现 mixin 接口,不实现原始类。使用 CGLIB 代理,原始类由代理和 mixin 接口一起实现。

注意,在代码中,我们首先测试代理是否是Contact的实例,然后测试它是否是IsModified的实例。当您使用 CGLIB 代理时,两个测试都返回true,但是对于 JDK 代理,只有IsModified测试返回true。最后,我们测试修改检查代码,首先将name属性设置为其当前值,然后设置为新值,每次检查isModified标志的值。运行此示例会产生以下输出:

Is Contact?: true
Is IsModified?: true
Has been modified?: false
Has been modified?: false
Has been modified?: true

正如所料,两个instanceof测试都返回true。注意,在任何修改发生之前,对isModified()的第一次调用返回false。在我们将name的值设置为相同的值之后,下一个调用也返回false。然而,对于最后一次调用,在我们将 name 的值设置为一个新值之后,isModified()方法返回true,表明该对象实际上已经被修改。

简介摘要

介绍是 Spring AOP 最强大的特性之一;它们不仅允许您扩展现有方法的功能,还允许您动态扩展一组接口和对象实现。使用引入是实现横切逻辑的最佳方式,应用通过定义良好的接口与横切逻辑进行交互。通常,这是您希望以声明方式而不是以编程方式应用的那种逻辑。通过使用本例中定义的IsModifiedMixin和下一节讨论的框架服务,我们可以声明性地定义哪些对象能够进行修改检查,而不需要修改这些对象的实现。

显然,因为介绍是通过代理进行的,所以会增加一定的开销。代理上的所有方法都被认为是建议的,因为切入点不能与引入一起使用。但是,对于许多可以通过使用对象修改检查之类的介绍来实现的服务来说,这种性能开销对于实现服务所需代码的减少以及完全集中服务逻辑带来的稳定性和可维护性的提高来说是很小的代价。

面向 AOP 的框架服务

到目前为止,我们不得不编写大量代码来通知对象并为它们生成代理。尽管这本身并不是一个大问题,但它确实意味着所有的通知配置都被硬编码到您的应用中,从而消除了能够透明地通知方法实现的一些好处。幸运的是,Spring 提供了额外的框架服务,允许您在应用配置中创建一个建议代理,然后将这个代理注入到目标 bean 中,就像任何其他依赖项一样。

使用 AOP 配置的声明性方法比手工编程机制更好。当您使用声明性机制时,您不仅外部化了通知的配置,还减少了编码错误的机会。您还可以利用 DI 和 AOP 的结合来启用 AOP,以便可以在完全透明的环境中使用它。

以声明方式配置 AOP

当使用 Spring AOP 的声明性配置时,有三种选择。

  • 使用 ProxyFactoryBean:在 Spring AOP 中,ProxyFactoryBean提供了一种声明性的方式来配置 Spring 的ApplicationContext(以及底层的BeanFactory)当基于定义的 Spring beans 创建 AOP 代理时。
  • 使用 Spring aop 名称空间:在 Spring 2.0 中引入,aop名称空间提供了一种简化的方式(与ProxyFactoryBean相比)来定义 Spring 应用中的方面及其 DI 需求。然而,aop名称空间也在幕后使用ProxyFactoryBean
  • 使用@AspectJ 样式的注释:除了基于 XML 的aop名称空间,还可以在类中使用@AspectJ-样式的注释来配置 Spring AOP。虽然它使用的语法是基于 AspectJ 的,并且在使用这个选项时需要包含一些 AspectJ 库,但是 Spring 在引导ApplicationContext时仍然使用代理机制(也就是为目标创建代理对象)。

使用 ProxyFactoryBean

ProxyFactoryBean类是FactoryBean的一个实现,它允许您指定一个 bean 作为目标,并且它为这个 bean 提供了一组建议和顾问,这些建议和顾问最终被合并到一个 AOP 代理中。ProxyFactoryBean用于将拦截器逻辑应用于现有的目标 bean,当调用该 bean 上的方法时,拦截器在该方法调用之前和之后执行。因为可以通过ProxyFactoryBean使用 advisor 和 advice,所以不仅可以声明性地配置 advice,还可以配置切入点。

ProxyFactoryBeanProxyFactory共享一个公共接口(org.springframework.aop.framework.Advised接口)(两个类都间接扩展了org.springframework.aop.framework.AdvisedSupport类,后者实现了Advised接口),因此,它公开了许多相同的标志,如frozenoptimizeexposeProxy。这些标志的值直接传递给底层的ProxyFactory,这也允许您以声明方式配置工厂。

ProxyFactoryBean 运行中

使用ProxyFactoryBean很简单。您定义一个将成为目标 bean 的 bean,然后使用ProxyFactoryBean,您定义您的应用将访问的 bean,使用目标 bean 作为代理目标。在可能的情况下,将目标 bean 定义为代理 bean 声明中的匿名 bean。这可以防止您的应用意外访问未经修改的 bean。然而,在某些情况下,比如我们将要向您展示的示例,您可能想要为同一个 bean 创建多个代理,因此对于这种情况,您应该使用一个普通的顶级 bean。

对于下面的例子,想象一下这个场景:你有一个歌手和一个纪录片制作人一起制作一个巡演的纪录片。在这种情况下,Documentarist依赖于Singer的实现。这里我们将使用的Singer实现是之前介绍的GrammyGuitarist。这里再次显示了内容:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;

public class GrammyGuitarist implements Singer {

        @Override public void sing() {
                System.out.println("sing: Gravity is working against me\n" +
                                "And gravity wants to bring me down");
        }

        public void sing(Guitar guitar) {
                System.out.println("play: " + guitar.play());
        }

        public void rest(){
                System.out.println("zzz");
        }

        public void talk(){
                System.out.println("talk");
        }
}

这个Documentarist类基本上会告诉歌手在拍摄纪录片时该做什么,如下所示:

package com.apress.prospring5.ch5;

public class Documentarist {

        private GrammyGuitarist guitarist;

        public void execute() {
                guitarist.sing();
                guitarist.talk();
        }

        public void setDep(GrammyGuitarist guitarist) {
                this.guitarist = guitarist;
        }

}

对于这个例子,我们将为一个单独的GrammySinger实例创建两个代理,两者都使用这里显示的相同的基本建议:

package com.apress.prospring5.ch5;

import org.aspectj.lang.JoinPoint;

public class AuditAdvice {
    public void simpleBeforeAdvice(JoinPoint joinPoint) {
        System.out.println("Executing: " +
                joinPoint.getSignature().getDeclaringTypeName() + " "
                + joinPoint.getSignature().getName());
    }
}

第一代理将通过直接使用建议来建议目标;因此,将建议所有方法。对于第二个代理,我们将配置AspectJExpressionPointcutDefaultPointcutAdvisor,以便只建议使用GrammySinger类的sing()方法。为了测试这个建议,我们将创建两个类型为Documentarist的 bean 定义,每个定义将被注入一个不同的代理。然后,我们将调用每个 beans 上的execute()方法,并观察调用依赖项上的建议方法时会发生什么。图 5-10 显示了该示例的配置(app-context-xml.xml)。我们使用了一个图像来描述这个配置,因为它可能看起来有点混乱,我们希望确保很容易看到每个 bean 被注入的位置。在本例中,我们只是简单地设置了使用 Spring 的 DI 功能在代码中设置的属性。唯一有趣的是,我们使用匿名 bean 作为切入点,并且使用了ProxyFactoryBean类。当切入点不被共享时,我们更喜欢使用匿名 bean 作为切入点,因为这使得可以直接访问的 bean 集尽可能小,并且尽可能与应用相关。当您使用ProxyFactoryBean时,要认识到的重要一点是ProxyFactoryBean声明是向您的应用公开的声明,也是当您实现依赖时要使用的声明。不建议使用底层的目标 bean 声明,所以只有当您想要绕过 AOP 框架时,才应该使用这个 bean,尽管一般来说,您的应用不应该知道 AOP 框架,因此也不应该想要绕过它。因此,您应该尽可能使用匿名 beans 来避免应用的意外访问。

A315511_5_En_5_Fig10_HTML.jpg

图 5-10。

Declarative AOP configuration pict this configuration because it might look a little confusing and we wanted to make sure it is easy to see where each bean is injected. In the example, we are simply setting the properties that we set in code using Spring’s DI capabilities. The only points of interest are that we use an anonymous bean for the pointcut, and we use the

下面的代码片段显示了一个简单的类,它从ApplicationContext获得两个Documentarist实例,然后为每个实例运行execute()方法:

package com.apress.prospring5.ch5;

import org.springframework.context.support.GenericXmlApplicationContext;
public class ProxyFactoryBeanDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("spring/app-context-xml.xml");
        ctx.refresh();

        Documentarist documentaristOne =
            ctx.getBean("documentaristOne", Documentarist.class);
        Documentarist documentaristTwo =
            ctx.getBean("documentaristTwo", Documentarist.class)

;

        System.out.println("Documentarist One >>");
        documentaristOne.execute();

        System.out.println("\nDocumentarist Two >> ");
        documentaristTwo.execute();
    }
}

运行此示例会产生以下输出:

Documentarist One >>
Executing: public void com.apress.prospring5.ch5.GrammyGuitarist.sing()
sing: Gravity is working against me
And gravity wants to bring me down
Executing: public void com.apress.prospring5.ch5.GrammyGuitarist.talk()
talk

Documentarist Two >>
Executing: public void com.apress.prospring5.ch5.GrammyGuitarist.sing()
sing: Gravity is working against me
And gravity wants to bring me down
talk

正如所料,第一个代理中的sing()talk()方法都被建议,因为在其配置中没有使用切入点。然而,对于第二个代理,由于配置中使用的切入点,只建议使用sing()方法。

使用 ProxyFactoryBean 进行介绍

使用ProxyFactoryBean类不仅可以通知一个对象,还可以向对象引入混合。记住前面关于介绍的讨论,您必须使用IntroductionAdvisor来添加介绍;您不能直接添加简介。当你在介绍中使用ProxyFactoryBean时,同样的规则也适用。当你使用ProxyFactoryBean时,如果你为你的 mixin 创建了一个定制的Advisor,配置你的代理会变得容易得多。以下配置片段显示了本章前面的IsModifiedMixin简介(app-context-xml.xml)的示例配置:

<beans ...>

    <bean id="guitarist"
          class="com.apress.prospring5.ch2.common.Contact"
          p:name="John Mayer"/>
    <bean id="advisor"
               class="com.apress.prospring5.ch5.introduction.IsModifiedAdvisor"/>
    <util:list id="interceptorAdvisorNames">
        <value>advisor</value>
    </util:list>

    <bean id="bean"
          class="org.springframework.aop.framework.ProxyFactoryBean"
          p:target-ref="guitarist"
          p:interceptorNames-ref="interceptorAdvisorNames"
          p:proxyTargetClass="true">
    </bean>

</beans>

从配置中可以看到,我们使用IsModifiedAdvisor类作为ProxyFactoryBean的顾问,因为我们不需要创建同一个目标对象的另一个代理,所以我们对目标 bean 使用匿名声明。下面的代码片段显示了从ApplicationContext获得代理的前面的简介示例的修改:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Contact;
import com.apress.prospring5.ch5.introduction.IsModified;
import org.springframework.context.support.GenericXmlApplicationContext;

public class IntroductionConfigDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh()

;

        Contact bean = (Contact) ctx.getBean("bean");
        IsModified mod = (IsModified) bean;

        System.out.println("Is Contact?: " + (bean instanceof Contact));
        System.out.println("Is IsModified?: " + (bean instanceof IsModified));

        System.out.println("Has been modified?: " + mod.isModified());
        bean.setName("John Mayer");

        System.out.println("Has been modified?: " + mod.isModified());
        bean.setName("Eric Clapton");

        System.out.println("Has been modified?: " + mod.isModified());
    }
}

运行这个示例会产生与前面的介绍示例完全相同的输出,但是这次代理是从ApplicationContext获得的,并且应用代码中没有配置。

因为我们已经介绍了 Java 配置,所以前面描述的 XML 配置可以替换为如下所示的配置类:

package com.apress.prospring5.ch5.config;

import com.apress.prospring5.ch2.common.Contact;
import com.apress.prospring5.ch5.introduction.IsModifiedAdvisor;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig 

{

        @Bean
        public Contact guitarist() {
                Contact guitarist = new Contact();
                guitarist.setName("John Mayer");
                return guitarist;
        }

        @Bean
        public Advisor advisor() {
                return new IsModifiedAdvisor();
        }

        @Bean ProxyFactoryBean bean() {
                ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
                proxyFactoryBean.setTarget(guitarist());
                proxyFactoryBean.setProxyTargetClass(true);
                proxyFactoryBean.addAdvisor(advisor());
                return proxyFactoryBean;
        }
}

为了测试前面的类是否真的工作,在类IntroductionConfigDemomain()方法中,用下面的代码替换初始化上下文的代码行:

GenericApplicationContext ctx =
     new AnnotationConfigApplicationContext(AppConfig.class);

配置类的不同之处在于,不需要通过名称引用advisor bean 或将其添加到列表中作为参数提供给ProxyFactoryBean,因为可以直接调用addAdvisor(..),并且可以作为参数提供advisor bean。这显然简化了配置。

ProxyFactoryBean 摘要

当您使用ProxyFactoryBean时,您可以配置 AOP 代理,提供编程方法的所有灵活性,而不需要将您的应用耦合到 AOP 配置。除非您需要在运行时决定如何创建代理,否则最好使用代理配置的声明性方法,而不是编程性方法。让我们继续,这样您就可以看到声明性 Spring AOP 的另外两个选项,这两个选项都是基于 Spring 2.0 或更新版本以及 JDK 5 或更新版本的应用的首选选项。

使用 aop 名称空间

aop名称空间为声明性 Spring AOP 配置提供了一个大大简化的语法。为了向您展示它是如何工作的,让我们重用前面的ProxyFactoryBean例子,稍微修改一下版本,以便演示它的用法。上一个例子中的GrammyGuitarist类仍然被使用,但是Documentarist将被扩展来调用带有Guitar参数的sing()方法。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;

public class NewDocumentarist extends Documentarist {

        @Override
        public void execute() {
                guitarist.sing();
                guitarist.sing(new Guitar());
                guitarist.talk();
        }
}

建议类更改如下:

package com.apress.prospring5.ch5;

import org.aspectj.lang.JoinPoint;

public class SimpleAdvice {

        public void simpleBeforeAdvice(JoinPoint joinPoint) {
          System.out.println("Executing: " +
               joinPoint.getSignature().getDeclaringTypeName() + " "
               + joinPoint.getSignature().getName());
        }
}

您将看到 advice 类不再需要实现MethodBeforeAdvice接口。此外,before 通知接受连接点作为参数,但不接受方法、对象和参数。实际上,对于 advice 类,这个参数是可选的,所以您可以让这个方法没有参数。但是,如果在通知中您需要访问被通知的连接点的信息(在这种情况下,我们希望转储调用类型和方法名的信息),那么我们需要定义参数的接受。当为方法定义参数时,Spring 会自动将连接点传递到方法中进行处理。下面是来自app-context-xml-01.xml文件的带有aop名称空间的 Spring XML 配置:

<?xml version="1.0" encoding="UTF-8"?>

<beans 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
      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="singExecution"
            expression="execution(
  * com.apress.prospring5.ch5..sing*(com.apress.prospring5.ch2.common.Guitar)
)"/>
        <aop:aspect ref="advice">
            <aop:before pointcut-ref="singExecution"
                   method="simpleBeforeAdvice"/>
        </aop:aspect>
    </aop:config>

    <bean id="advice"
          class="com.apress.prospring5.ch5.SimpleAdvice"/>
    <bean id="johnMayer"
          class="com.apress.prospring5.ch5.GrammyGuitarist"/>
    <bean id="documentarist"
          class="com.apress.prospring5.ch5.NewDocumentarist"
          p:guitarist-ref="johnMayer"/>
</beans>

首先,我们需要在<beans>标签中声明aop名称空间。第二,所有的 Spring AOP 配置都放在标签<aop:config>下。在<aop:config>下,您可以定义切入点、方面、顾问等等,并像往常一样引用其他 Spring beans。

在前面的配置中,我们定义了一个 ID 为singExecution的切入点。表情

"execution(*
   com.apress.prospring5.ch5..sing*(com.apress.prospring5.ch2.common.Guitar)
)"

意味着我们要通知所有带有前缀sing的方法,并且类是在包com.apress.prospring5.ch5下定义的(包括所有的子包)。另外,sing()方法应该接收一个类型为Guitar的参数。随后,通过使用<aop:aspect>标签声明了方面,advice 类引用了 ID 为advice的 Spring bean,它是SimpleAdvice类。pointcut-ref属性引用 ID 为singExecution的已定义切入点,而 before advice(使用<aop:before>标签声明)是 advice bean 中的方法simpleBeforeAdvice ()。要测试前面的配置,可以使用下面的类:

package com.apress.prospring5.ch5;

import org.springframework.context.support.GenericXmlApplicationContext;

public class AopNamespaceDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml-01.xml");
        ctx.refresh();
        NewDocumentarist documentarist =
            ctx.getBean("documentarist", NewDocumentarist.class);
        documentarist.execute();

        ctx.close();
    }
}

在这个例子中,我们简单地像往常一样初始化ApplicationContext,检索 bean,并调用它的execute()方法。运行该程序将产生以下输出:

sing: Gravity is working against me
And gravity wants to bring me down
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing
play: G C G C Am D7
talk

如您所见,只建议调用带有Guitar参数的sing(..)方法;没有参数的sing()方法和talk()方法则没有。这完全符合预期,您可以看到与ProxyFactoryBean配置相比,配置大大简化了。

让我们进一步把前面的例子修改成稍微复杂一点的情况。假设现在我们只想建议那些带有 Spring beans 的方法,这些方法的 ID 以john开始,参数类型为Guitar,参数的brand属性设置为Gibson

为此,首先必须更改Guitar类以添加brand属性。我们将使它成为非强制性的,并用默认值填充它,只是为了保持前面的例子正常工作。

package com.apress.prospring5.ch2.common;

public class Guitar {
        private String brand =" Martin";

        public String play(){
                return "G C G C Am D7";
        }

        public String getBrand() {
                return brand;
        }

        public void setBrand(String brand) {
                this.brand = brand;
        }
}

然后我们需要用一个特殊牌子的吉他让NewDocumentarist调用sing()方法。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;

public class NewDocumentarist extends Documentarist {

        @Override
        public void execute() {
                guitarist.sing();
                Guitar guitar = new Guitar();
                guitar.setBrand("Gibson");

                guitarist.sing(guitar);
                guitarist.talk();
        }
}

现在我们需要一种新的更复杂的建议。参数guitar被添加到 before 建议的签名中。第二,在通知中,我们只在参数的brand属性等于Gibson时检查和执行逻辑。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import org.aspectj.lang.JoinPoint;

public class ComplexAdvice {
    public void simpleBeforeAdvice(JoinPoint joinPoint, Guitar value) {
        if(value.getBrand().equals("Gibson")) {
              System.out.println("Executing: " +
                  joinPoint.getSignature().getDeclaringTypeName() + " "
                  + joinPoint.getSignature().getName());
        }
    }
}

此外,XML 配置需要修改,因为我们需要使用新类型的通知并更新切入点表达式。(你可以在app-context-xml-02.xml中找到完整的配置,除了下面显示的几行,其他的都和app-context-xml-01.xml的内容一样,这里不再赘述。)

<beans ..>
...

    <bean id="advice"
          class="com.apress.prospring5.ch5.ComplexAdvice"/>

    <aop:config>
        <aop:pointcut id="singExecution"
            expression="execution(* sing*(com.apress.prospring5.ch2.common.Guitar))
                and args(value) and bean(john*)"/>

</beans>

切入点表达式中又增加了两条指令。首先,args(value)指示 Spring 也将名为value的参数传递到 before 建议中。其次,bean(john*)指令指示 Spring 只通知 ID 前缀为john的 beans。这是一个强大的功能;如果您有一个定义良好的 Spring beans 命名结构,您可以很容易地建议您想要的对象。例如,您可以使用bean(*DAO*)获得适用于所有 DAO beans 的建议,或者使用bean(*Service*)获得适用于所有服务层 bean 的建议,而不是使用完全限定的类名进行匹配。使用新的配置文件app-context-xml02.xml运行相同的测试程序会产生以下输出:

sing: Gravity is working against me
And gravity wants to bring me down
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing
play: G C G C Am D7
talk

您可以看到,只建议了带有Guitar参数并且brand等于Gibsonsing()方法。

让我们再看一个将aop名称空间用于 around 通知的例子。我们可以简单地给ComplexAdvice类添加一个新方法,而不是创建另一个类来实现MethodInterceptor接口。以下代码示例显示了修订后的ComplexAdvice类中名为simpleAroundAdvice()的新方法:

//ComplexAdvice.java
public Object simpleAroundAdvice(ProceedingJoinPoint pjp,
      Guitar value) throws Throwable {
    System.out.println("Before execution: " +
        pjp.getSignature().getDeclaringTypeName() + " "
            + pjp.getSignature().getName()
            + " argument: " + value.getBrand());

        Object retVal = pjp.proceed();

        System.out.println("After execution: " +
            pjp.getSignature().getDeclaringTypeName() + " "
                + pjp.getSignature().getName()
                + " argument: " + value.getBrand());

    return retVal;
}

新添加的simpleAroundAdvice()方法需要接受至少一个类型为ProceedingJoinPoint的参数,这样它才能继续调用目标对象。我们还添加了value参数来显示建议中的值。必须修改<aop:aspect>的 XML 配置来添加新的建议。(您可以在app-context-xml-03.xml中找到完整的配置,除了下面显示的几行之外,其他内容都与app-context-xml-02.xml中的内容相同,因此这里不再赘述。)

<beans ..>
...
    <aop:config>
        <aop:pointcut id="singExecution"
            expression="execution(
                     * sing*(com.apress.prospring5.ch2.common.Guitar))
                    and args(value) and bean(john*)"
        />

        <aop:aspect ref="advice">
            <aop:before pointcut-ref="singExecution"
                method="simpleBeforeAdvice"/>
            <aop:around pointcut-ref="singExecution"
                method="simpleAroundAdvice"/>
        </aop:aspect>
    </aop:config>
</beans>

我们只是添加了新的标签<aop:around>来声明 around 通知并引用相同的切入点。让我们再次修改NewDocumentarist.execute()方法,加入一个带有默认Guitarsing()调用,以获得我们想要分析的行为。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;

public class NewDocumentarist extends Documentarist {

        @Override
        public void execute() {
                guitarist.sing();
                Guitar guitar = new Guitar();
                guitar.setBrand("Gibson");
                guitarist.sing(guitar);
                guitarist.sing(new Guitar());

                guitarist.talk();
        }
}

再次运行测试程序,您将得到以下输出:

sing: Gravity is working against me
And gravity wants to bring me down

Executing: com.apress.prospring5.ch5.GrammyGuitarist sing
Before execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
play: G C G C Am D7
After execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson

Before execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Martin
play: G C G C Am D7
After execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Martin
talk

这里有两个有趣的点。首先,您会看到 around 建议被应用于带有一个Guitar参数的sing(..)方法的两次调用,因为它不检查参数。第二,对于将"Gibson" Guitar作为参数的sing()方法,before 和 around 建议都被执行,默认情况下 before 建议优先。

A315511_5_En_5_Figa_HTML.jpg当使用aop名称空间或@AspectJ样式时,有两种类型的 after 建议。只有当目标方法正常完成时,after-returning建议(使用<aop:after-returning>标签)才适用。另一个是 after advice(使用<aop:after>标签),无论方法是正常完成还是遇到错误并抛出异常都会发生。如果您需要不管目标方法的执行结果而执行的通知,您应该使用 after advice。

使用@AspectJ 样式的注释

在 JDK 5 或更新版本中使用 Spring AOP 时,您也可以使用@AspectJ-样式的注释来声明您的建议。然而,如前所述,Spring 仍然使用自己的代理机制来通知目标方法,而不是 AspectJ 的编织机制。

在这一节中,我们将介绍如何通过使用@AspectJ-样式注释来实现与aop名称空间中相同的方面。AspectJ 是一个通用的面向方面的 Java 扩展,是为了解决传统编程方法不能很好地捕捉的问题或关注点,换句话说,横切关注点。对于本节中的例子,我们也将对其他 Spring beans 使用注释,并且我们将使用 Java 配置类。

以下示例描述了使用注释声明 bean 的GrammyGuitarist类:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;
import org.springframework.stereotype.Component;

@Component("johnMayer")

public class GrammyGuitarist implements Singer {

        @Override public void sing() {
                System.out.println("sing: Gravity is working against me\n" +
                                "And gravity wants to bring me down");
        }

        public void sing(Guitar guitar) {
                System.out.println("play: " + guitar.play());
        }

        public void rest(){
                System.out.println("zzz");
        }

        public void talk(){
                System.out.println("talk");
        }
}

这个职业也需要适应。

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("documentarist")
public class NewDocumentarist {
        protected GrammyGuitarist guitarist;

        public void execute() {
                guitarist.sing();
                Guitar guitar = new Guitar();
                guitar.setBrand("Gibson");
                guitarist.sing(guitar);
                guitarist.talk();
        }

        @Autowired
        @Qualifier("johnMayer")
        public void setGuitarist(GrammyGuitarist guitarist) {
                this.guitarist = guitarist;
        }

}

我们用@Component注释来注释这两个类,并给它们分配相应的名称。在GrammyGuitarist类中,属性guitarist的 setter 方法被注释为@Autowired,用于 Spring 的自动注入。

现在让我们看看使用@AspectJ-样式注释的AnnotationAdvice类。我们将一次性实现通知前和通知周围的切入点。下面的代码片段显示了AnnotationAdvice类:

package com.apress.prospring5.ch5;

import com.apress.prospring5.ch2.common.Guitar;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component

@Aspect

public class AnnotatedAdvice {
    @Pointcut("execution(*
            com.apress.prospring5.ch5..sing*(com.apress.prospring5.ch2.common.Guitar))
        && args(value)")
    public void singExecution(Guitar value) {
    }

    @Pointcut("bean(john*)")
    public void isJohn() {
    }

    @Before("singExecution(value) && isJohn()")

    public void simpleBeforeAdvice(JoinPoint joinPoint, Guitar value) {
        if(value.getBrand().equals("Gibson")) {
        System.out.println("Executing: " +
                joinPoint.getSignature().getDeclaringTypeName() + " "

                + joinPoint.getSignature().getName() + " argument: " + value.getBrand());
        }
    }

    @Around("singExecution(value) && isJohn()")

    public Object simpleAroundAdvice(ProceedingJoinPoint pjp,
        Guitar value) throws Throwable {
        System.out.println("Before execution: " +
            pjp.getSignature().getDeclaringTypeName() + " "
            + pjp.getSignature().getName()
            + " argument: " + value.getBrand());

        Object retVal = pjp.proceed();

        System.out.println("After execution: " +
            pjp.getSignature().getDeclaringTypeName() + " "
            + pjp.getSignature().getName()
            + " argument: " + value.getBrand());

        return retVal;
    }
}

您会注意到代码结构与我们在aop名称空间中使用的非常相似,只是在这种情况下我们使用了注释。不过,还是有几点值得注意。

  • 我们使用了@Component@Aspect来注释AnnotatedAdvice类。@Aspect注释用于声明它是一个方面类。当我们在 XML 配置中使用<context:component-scan>标签时,为了允许 Spring 扫描组件,您还需要用@Component注释这个类。
  • 切入点被定义为返回void的方法。在类中,我们定义了两个切入点;两者都标注了@Pointcut。我们有意将aop名称空间示例中的切入点表达式一分为二。第一个(由方法singExecution(Guitar value))指示)用一个guitar参数定义了包com.apress.prospring4.ch5下所有类的sing*()方法执行的切入点,参数(value)也将被传递到通知中。另一个(由方法isJohn()指示)是定义另一个切入点,该切入点定义所有方法执行,并在 Spring beans 的名称前加上前缀john。还要注意,我们需要使用&&来定义切入点表达式中的“and”条件,而对于aop名称空间,我们需要使用and操作符。
  • before-advice 方法用@Before注释,而 around advice 用@Around注释。对于这两种通知类型,我们传递使用类中定义的两个切入点的值。值singExecution(value) && isJohn()意味着两个切入点的条件应该匹配以应用建议,这与ComposablePointcut中的交集操作相同。
  • 通知前逻辑和绕过通知逻辑与aop名称空间示例中的相同。

有了所有的注释,XML 配置就变得简单了。

<?xml version="1.0" encoding="UTF-8"?>

<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-aop.xsd
      http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd">

    <aop:aspectj-autoproxy/>

    <context:component-scan
      base-package="com.apress.prospring5.ch5"/>
</beans>

只声明了两个标记。<aop:aspect-autoproxy>标签通知 Spring 扫描@AspectJ-样式的注释,而<context:component-scan>标签仍然是 Spring 扫描通知所在的包中的 Spring beans 所必需的。我们还需要用@Component注释 advice 类,以表明它是一个 Spring 组件。

下面的代码片段描述了测试此配置的类:

package com.apress.prospring5.ch5;

import org.springframework.context.support.GenericXmlApplicationContext;

public class AspectJAnnotationDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        NewDocumentarist documentarist =
           ctx.getBean("documentarist", NewDocumentarist.class);
        documentarist.execute();
    }
}

如果您按原样运行该示例,您会感到有点惊讶,因为您将在控制台中看到以下内容:

Exception in thread "main"
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'documentarist': Unsatisfied dependency
expressed through method 'setGuitarist' parameter 0; nested exception is
 org.springframework.beans.factory.BeanNotOfRequiredTypeException:
 Bean named 'johnMayer' is expected to be of type
   'com.apress.prospring5.ch5.GrammyGuitarist' but was actually of
   type 'com.sun.proxy.$Proxy18'
...

这是怎么回事?嗯,GrammyGuitarist实现了Singer接口,默认情况下,基于接口的 JDK 动态代理被创建。但是NewDocumentarist严格要求依赖关系必须是类型Grammy-Guitarist或者是它的扩展。因此,会引发上一个异常。我们如何解决它?有两种方法:一种是修改NewDocumentarist来接受Singer依赖,但是这并不适合我们的例子,因为我们想要访问GrammyGuitarist类中的方法,这些方法不是在Singer接口中定义的方法的实现。第二种方法是请求 Spring 生成 CGLIB,基于类的代理。在 XML 中,这可以通过修改<aop:aspectj-autoproxy/>标签的配置并将proxy-target-class属性值设置为true来实现。

Java 配置类甚至比这更简单:

@Configuration
@ComponentScan(basePackages = {"com.apress.prospring5.ch5"})

@EnableAspectJAutoProxy(proxyTargetClass = true)

public class AppConfig {
}

注意@EnableAspectJAutoProxy注释。它相当于<aop:aspectj-autoproxy/>,也有一个类似于proxy-target-class属性的属性proxyTargetClass。该注释支持处理用 AspectJ 的@Aspect注释标记的组件,并且被设计用于用@Configuration注释的类。

下面是测试程序。它被设计成一个 JUnit 测试用例,以便 XML 和 Java 配置示例可以放在同一个类中。像 IntelliJ IDEA 这样的智能编辑器提供了独立执行每个测试方法的可能性。

package com.apress.prospring5.ch5;

import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class AspectJAnnotationTest {

        @Test
        public void xmlTest() {
                GenericXmlApplicationContext ctx =
                   new GenericXmlApplicationContext();
                ctx.load("classpath:spring/app-context-xml.xml");
                ctx.refresh();

                NewDocumentarist documentarist =
                    ctx.getBean("documentarist", NewDocumentarist.class);
                documentarist.execute();

                ctx.close();
        }

        @Test
        public void configTest() {
                GenericApplicationContext ctx =
                   new AnnotationConfigApplicationContext(AppConfig.class);

                NewDocumentarist documentarist =
                   ctx.getBean("documentarist", NewDocumentarist.class);
                documentarist.execute();
                ctx.close();
        }
}

运行这些测试方法中的任何一种,如果通过,都会产生以下输出:

sing: Gravity is working against me
And gravity wants to bring me down
Before execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
play: G C G C Am D7
After execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
talk

Spring Boot 提供了一个特殊的 AOP 启动库,消除了一些配置的麻烦。该库通常在pro-spring-15/build.properties文件中配置,并作为依赖项添加到子项目配置文件aspectj-boot/build.gradle中。

//pro-spring-15/build.properties
ext {
    bootVersion = '2.0.0.BUILD-SNAPSHOT'

    ...
    boot = [
         springBootPlugin:
              "org.springframework.boot:spring-boot-gradle-plugin:$bootVersion",
         ...
         starterAop:
             "org.springframework.boot:spring-boot-starter-aop:$bootVersion"
    ]
}
//aspectj-boot/build.gradle
buildscript {
          ...
    dependencies {
        classpath boot.springBootPlugin
    }
}

apply plugin: 'org.springframework.boot'

dependencies {
    compile boot.starterAop
}

在图 5-11 中,你可以看到作为依赖项添加到 Spring Boot 项目中的一组库。通过添加这个库

A315511_5_En_5_Fig11_HTML.jpg

图 5-11。

Spring Boot AOP starter tranzitive dependencies as depicted in IntelliJ IDEA

作为对应用的依赖,不再需要@EnableAspectJAutoProxy(proxyTargetClass = true)注释,因为 AOP Spring 支持已经默认启用。该属性也不必在任何地方设置,因为 Spring Boot 会自动检测您需要什么类型的代理。考虑到前面的例子,您可以删除AppConfig类,并用一个典型的 Spring Boot 应用类替换它。

package com.apress.prospring5.ch5;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

        private static Logger logger = LoggerFactory.getLogger(Application.class);

        public static void main(String args) throws Exception {
                ConfigurableApplicationContext ctx =
                    SpringApplication.run(Application.class, args);
                assert (ctx != null);

                NewDocumentarist documentarist =
                    ctx.getBean("documentarist", NewDocumentarist.class);
                documentarist.execute();

                System.in.read();
                ctx.close();
        }
}

声明性 Spring AOP 配置的注意事项

到目前为止,我们已经讨论了声明 Spring AOP 配置的三种方式,包括ProxyFactoryBeanaop名称空间和@AspectJ-样式注释。我们相信你会同意aop名称空间比ProxyFactoryBean简单得多。所以,总的问题是,您使用的是aop名称空间还是@AspectJ-样式注释?

如果您的 Spring 应用是基于 XML 配置的,那么使用aop名称空间方法是一个自然的选择,因为它保持了 AOP 和 DI 配置风格的一致性。另一方面,如果您的应用主要基于注释,请使用@AspectJ注释。同样,让应用的需求驱动配置方法,并尽最大努力保持一致性。

此外,aop名称空间和@AspectJ注释方法之间还有一些其他的区别。

  • 切入点表达式语法有一些细微的区别(例如,在前面的讨论中,我们需要在aop名称空间中使用and,但是在@AspectJ注释中使用&&)。
  • aop名称空间方法只支持“单例”方面实例化模型。
  • aop名称空间中,您不能“组合”多个切入点表达式。在使用@AspectJ的例子中,我们可以在 before 和 around 通知中组合两个切入点定义(即singExecution(value) && isJohn())。当使用aop名称空间并且需要创建一个新的组合匹配条件的切入点表达式时,需要使用ComposablePointcut类。

AspectJ 集成

AOP 为基于 OOP 的应用出现的许多常见问题提供了一个强大的解决方案。当使用 Spring AOP 时,您可以利用 AOP 功能的精选子集,在大多数情况下,这些功能可以帮助您解决应用中遇到的问题。然而,在某些情况下,您可能想要使用一些超出 Spring AOP 范围的 AOP 特性。

从连接点的角度来看,Spring AOP 只支持执行公共非静态方法时的切入点匹配。但是,在某些情况下,在对象构造或字段访问等过程中,您可能需要将建议应用于受保护的/私有的方法。

在这些情况下,您需要查看具有更全面特性集的 AOP 实现。在这种情况下,我们的首选是使用 AspectJ,因为现在可以使用 Spring 配置 AspectJ 方面,AspectJ 形成了 Spring AOP 的完美补充。

关于 AspectJ

AspectJ 是一个全功能的 AOP 实现,它使用编织过程(编译时或加载时编织)将方面引入到代码中。在 AspectJ 中,方面和切入点是使用类似 Java 的语法构建的,这减少了 Java 开发人员的学习曲线。我们不打算花太多时间研究 AspectJ 及其工作原理,因为这超出了本书的范围。相反,我们给出一些简单的 AspectJ 示例,并向您展示如何使用 Spring 配置它们。要了解更多关于 AspectJ 的信息,你一定要阅读 Ramnivas Laddad 的《AspectJ 在行动中:带有 Spring 应用的企业 AOP》(Manning,2009)。

A315511_5_En_5_Figa_HTML.jpg我们不打算讨论如何将 AspectJ 方面编织到您的应用中。参考 AspectJ 文档获取详细信息,或者看看第 5 的aspectj-aspects项目中提供的 Gradle build。

使用单例方面

默认情况下,AspectJ 方面是单例的,这意味着每个类装入器只有一个实例。Spring 面对任何 AspectJ 方面的问题是它不能创建方面实例,因为这已经由 AspectJ 自己处理了。然而,每个方面都公开了一个名为org.aspectj.lang.Aspects.aspectOf()的方法,可以用来访问方面实例。使用aspectOf()方法和 Spring 配置的一个特殊特性,您可以让 Spring 为您配置方面。有了这种支持,您就可以充分利用 AspectJ 强大的 AOP 特性集,而不会失去 Spring 出色的 DI 和配置能力。这也意味着您的应用不需要两种单独的配置方法;您可以对所有 Spring 管理的 beans 和 AspectJ 方面使用相同的 Spring ApplicationContext方法。

为了支持 Spring 应用中的方面,需要在配置中添加一个 Gradle 插件。你可以在这里找到源代码和如何在 Gradle 应用中使用它的说明: https://github.com/eveoh/gradle-aspectj 。在这里你可以看到chapter05/aspectj-aspects/build.gradle的内容:

buildscript {
   repositories {
      mavenLocal()
      mavenCentral()
      maven { url "http://repo.spring.io/release" }
      maven { url "http://repo.spring.io/milestone" }
      maven { url "http://repo.spring.io/snapshot" }
      maven { url "https://repo.spring.io/libs-snapshot" }
      maven { url "https://maven.eveoh.nl/content/repositories/releases" }

   }
   dependencies {
      classpath "nl.eveoh:gradle-aspectj:1.6"
   }
}

apply plugin: 'aspectj'

jar {
   manifest {
      attributes(
         'Main-Class': 'com.apress.prospring5.ch5.AspectJDemo',
         "Class-Path": configurations.compile.collect { it.getName() }.join(' '))
   }
}

在下面的代码片段中,您可以看到一个基本类MessageWriter,我们将使用 AspectJ 来建议它:

package com.apress.prospring5.ch5;

public class MessageWriter {
    public void writeMessage() {
        System.out.println("foobar!");
    }

    public void foo() {
        System.out.println("foo");
    }
}

对于这个例子,我们将使用 AspectJ 来通知writeMessage()方法,并在方法调用前后写出一条消息。这些消息可以使用 Spring 进行配置。下面的代码示例展示了MessageWrapper方面(文件名是MessageWrapper.aj,它是一个 AspectJ 文件,而不是一个标准的 Java 类):

package com.apress.prospring5.ch5;

public aspect MessageWrapper {
    private String prefix;
    private String suffix;

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getPrefix() {
        return this.prefix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    public String getSuffix() {
        return this.suffix;
    }

    pointcut doWriting() :
        execution(*
    com.apress.prospring5.ch5.MessageWriter.writeMessage());

    before() : doWriting() {
        System.out.println(prefix);
    }

    after() : doWriting() {
        System.out.println(suffix);
    }
}

本质上,我们创建了一个名为MessageWrapper的方面,就像普通的 Java 类一样,我们给了方面两个属性suffixprefix,我们将在通知writeMessage()方法时使用它们。接下来,我们为单个连接点定义一个命名的切入点doWriting(),在本例中,是writeMessage()方法的执行。AspectJ 有大量的连接点,但是这些不在本例的讨论范围之内。最后,我们定义了两条建议:一条在doWriting()切入点之前执行,另一条在它之后执行。下面的配置片段展示了如何在 Spring ( app-config-xml.xml)中配置这个方面:

<?xml version="1.0" encoding="UTF-8"?>

<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="aspect" class="com.apress.prospring5.ch5.MessageWrapper"
        factory-method="aspectOf" p:prefix="The Prefix" p:suffix="The Suffix"/>
</beans>

如您所见,方面 bean 的大部分配置类似于标准 bean 配置。唯一的区别是使用了<bean>标签的factory-method属性。factory-method属性旨在允许遵循传统工厂模式的类无缝集成到 Spring 中。例如,如果您有一个带有私有构造函数和静态工厂方法getInstance()的类Foo,使用factory-method属性允许 Spring 管理这个类的 bean。每个 AspectJ 方面公开的aspectOf()方法允许您访问方面的实例,从而允许 Spring 设置方面的属性。在这里,您可以看到一个简单的驱动程序应用:

package com.apress.prospring5.ch5;

import org.springframework.context.support.GenericXmlApplicationContext;

public class AspectJDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        MessageWriter writer = new MessageWriter();
        writer.writeMessage();
        writer.foo();
    }
}

注意,首先我们加载ApplicationContext来允许 Spring 配置方面。接下来我们创建一个MessageWriter的实例,然后调用writeMessage()foo()方法。该示例的输出如下:

The Prefix
foobar!
The Suffix
foo

如您所见,MessageWrapper方面中的建议被应用到了writeMessage()方法中,并且在ApplicationContext配置中指定的前缀和后缀值被建议在写出之前和之后的消息时使用。

摘要

在这一章中,我们讨论了大量的 AOP 核心概念,并研究了这些概念如何转化为 Spring AOP 实现。我们讨论了 Spring AOP 中实现的(和没有实现的)特性,并且指出 AspectJ 是 Spring 没有实现的那些特性的 AOP 解决方案。我们花了一些时间解释 Spring 中可用的建议类型的细节,并且您看到了四种类型的实例。我们还看了如何通过使用切入点来限制建议适用的方法。特别是,我们研究了 Spring 提供的六个基本切入点实现。我们还详细介绍了 AOP 代理是如何构造的,不同的选项,以及它们的不同之处。我们比较了三种代理类型的性能,并强调了在选择 JDK 代理和 CGLIB 代理之间的一些主要差异和限制。我们讨论了切入点的高级选项,以及如何使用简介来扩展由对象实现的接口集。我们还介绍了 Spring Framework 服务以声明方式配置 AOP,从而避免了将 AOP 代理构造逻辑硬编码到代码中的需要。我们花了一些时间来研究 Spring 和 AspectJ 是如何集成的,以允许您使用 AspectJ 的额外功能,而不会失去 Spring 的任何灵活性。那当然是大量的 AOP!

在下一章,我们将转移到一个完全不同的话题——我们如何使用 Spring 的 JDBC 支持来从根本上简化基于 JDBC 的数据访问代码的创建。

Footnotes 1

http://eclipse.org/aspectj

  2

UML 在开发中非常重要,因为它是一种简化应用逻辑并使其可视化的方法,因此在编写代码之前就很容易发现问题。当向团队介绍新成员时,它也可以用作文档,使他们尽快提高工作效率。你可以在 www.uml.org/ 找到更多信息。