11. 使用Spring进行面向方面编程

11.1 简介

面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。 OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。方面实现了诸如跨越多种类型和对象的事务管理之类的关注点的模块化。 (这种担忧通常被称为AOP文献中的横切关注点。)

Spring的一个关键组件是AOP框架。虽然Spring IoC容器不依赖于AOP,但这意味着如果您不需要使用AOP,AOP将补充Spring IoC以提供非常强大的中间件解决方案。


Spring 2.0+ AOP

Spring 2.0引入了一种使用 schema-based approach@AspectJ annotation style 编写自定义方面的更简单,更强大的方法。这两种样式都提供完全类型的建议和AspectJ切入点语言的使用,同时仍然使用Spring AOP进行编织。

本章将讨论Spring 2.0模式和基于@ AspectJ的AOP支持。在 the following chapter 中讨论了在Spring 1.2应用程序中常见的低级AOP支持。


AOP在Spring Framework中用于......

  • ...提供声明性企业服务,尤其是作为EJB声明性服务的替代品。最重要的此类服务是 declarative transaction management

  • ...允许用户实现自定义方面,补充他们使用AOP的OOP。

如果您只对通用声明性服务或其他预先打包的声明性中间件服务(如池)感兴趣,则无需直接使用Spring AOP,并且可以跳过本章的大部分内容。

11.1.1 AOP概念

让我们首先定义一些中心AOP概念和术语。这些术语不是特定于Spring的......不幸的是,AOP术语不是特别直观;但是,如果Spring使用自己的术语,那将更加令人困惑。

  • 方面:跨越多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,方面是使用常规类( schema-based approach )或使用 @Aspect 注释( @AspectJ style )注释的常规类实现的。

  • 加入点:程序执行期间的一个点,例如执行方法或处理异常。在Spring AOP中,连接点始终表示方法执行。

  • 建议:某个方面在特定连接点采取的操作。不同类型的建议包括 "around" , "before" 和 "after" 建议。 (建议类型将在下面讨论。)许多AOP框架(包括Spring)将建议建模为拦截器,在连接点周围维护一系列拦截器。

  • 切入点:匹配连接点的谓词。建议与切入点表达式相关联,并在切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。

  • 简介:代表类型声明其他方法或字段。 Spring AOP允许您向任何建议的对象引入新接口(和相应的实现)。例如,您可以使用简介使bean实现 IsModified 接口,以简化缓存。 (介绍被称为AspectJ社区中的类型间声明。)

  • 目标对象:由一个或多个方面建议的对象。也称为建议对象。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。

  • AOP代理:由AOP框架创建的对象,用于实现方面 Contract (建议方法执行等)。在Spring Framework中,AOP代理将是JDK动态代理或CGLIB代理。

  • 编织:将方面与其他应用程序类型或对象链接以创建建议对象。这可以在编译时(例如,使用AspectJ编译器),加载时间或在运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行编织。

建议类型:

  • 在建议之前:在连接点之前执行但不能阻止执行流程进入连接点的建议(除非它抛出异常)。

  • 返回建议后:在连接点正常完成后执行的建议:例如,如果方法返回而不抛出异常。

  • 抛出建议后:如果方法通过抛出异常退出,则执行建议。

  • (最后)建议之后:无论加入点退出的方式(正常或异常退货),都要执行建议。

  • 围绕建议:围绕连接点的建议,例如方法调用。这是最有力的建议。 around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的返回值或抛出异常来快速建议的方法执行。

围绕建议是最普遍的建议。由于Spring AOP(如AspectJ)提供了全方位的建议类型,因此我们建议您使用可以实现所需行为的最不强大的建议类型。例如,如果您只需要使用方法的返回值更新缓存,那么最好实现返回后的建议而不是周围的建议,尽管周围的建议可以完成同样的事情。使用最具体的建议类型可以提供更简单的编程模型,减少错误的可能性。例如,您不需要在用于around建议的 JoinPoint 上调用 proceed() 方法,因此无法调用它。

在Spring 2.0中,所有通知参数都是静态类型的,因此您可以使用相应类型的建议参数(例如,方法执行的返回值的类型)而不是 Object 数组。

通过切入点匹配的连接点的概念是AOP的关键,它将其与仅提供拦截的旧技术区分开来。切入点使得建议可以独立于面向对象的层次结构进行定向。例如,提供声明式事务管理的around建议可以应用于跨越多个对象的一组方法(例如服务层中的所有业务操作)。

11.1.2 Spring AOP的功能和目标

Spring AOP是用纯Java实现的。不需要特殊的编译过程。 Spring AOP不需要控制类加载器层次结构,因此适合在Servlet容器或应用程序服务器中使用。

Spring AOP目前仅支持方法执行连接点(建议在Spring bean上执行方法)。虽然可以在不破坏核心Spring AOP API的情况下添加对字段拦截的支持,但未实现字段拦截。如果您需要建议字段访问和更新连接点,请考虑使用AspectJ等语言。

Spring AOP的AOP方法与大多数其他AOP框架的方法不同。目的不是提供最完整的AOP实现(尽管Spring AOP非常强大);它是在AOP实现和Spring IoC之间提供紧密集成,以帮助解决企业应用程序中的常见问题。

因此,例如,Spring Framework的AOP功能通常与Spring IoC容器一起使用。使用普通bean定义语法配置方面(尽管这允许强大的“自动代理”功能):这是与其他AOP实现的重要区别。使用Spring AOP时,您可以轻松或高效地执行某些操作,例如建议非常细粒度的对象(例如域对象):在这种情况下,AspectJ是最佳选择。但是,我们的经验是Spring AOP为适合AOP的企业Java应用程序中的大多数问题提供了出色的解决方案。

Spring AOP永远不会努力与AspectJ竞争,以提供全面的AOP解决方案。我们相信基于代理的框架都像Spring AOP和像AspectJ这样的全面框架是有 Value 的,它们是互补的,而不是竞争。 Spring将Spring AOP和IoC与AspectJ无缝集成,以便在基于Spring的一致应用程序架构中满足AOP的所有使用需求。此集成不会影响Spring AOP API或AOP Alliance API:Spring AOP保持向后兼容。有关Spring AOP API的讨论,请参见 the following chapter

Spring框架的核心原则之一是非侵入性;这是一个想法,你不应该被迫在你的业务/领域模型中引入特定于框架的类和接口。但是,在某些地方,Spring Framework确实为您提供了将Spring Framework特定的依赖项引入代码库的选项:为您提供此类选项的基本原理是因为在某些情况下,读取或编写某些特定部分可能更容易。以这种方式的功能。 Spring Framework(几乎)总是为您提供选择:您可以自由决定哪种选项最适合您的特定用例或场景。与本章相关的一个选择是选择哪种AOP框架(以及哪种AOP样式)。您可以选择AspectJ和/或Spring AOP,也可以选择@AspectJ注释样式方法或Spring XML配置样式方法。本章选择首先介绍@ AspectJ风格的方法,这一事实不应被视为Spring团队倾向于采用Spring XML配置风格的@AspectJ注释风格方法。请参见第11.4节“选择使用哪种AOP声明样式”,以更全面地讨论每种样式的原因和原因。

11.1.3 AOP Proxies

Spring AOP默认使用AOP代理的标准JDK动态代理。这使得任何接口(或接口集)都可以被代理。

Spring AOP也可以使用CGLIB代理。这是代理类而不是接口所必需的。如果业务对象未实现接口,则默认使用CGLIB。因为优良的做法是编程接口而不是类;业务类通常会实现一个或多个业务接口。在那些(希望很少见)需要建议未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法的情况下,可能会出现 force the use of CGLIB

掌握Spring AOP是基于代理的这一事实非常重要。请参阅 Section 11.6.1, “Understanding AOP proxies” 以全面了解此实现细节的实际含义。

11.2 @AspectJ支持

@AspectJ指的是将方面声明为使用注释注释的常规Java类的样式。作为AspectJ 5版本的一部分, AspectJ project 引入了@AspectJ样式。 Spring使用AspectJ提供的库解释与AspectJ 5相同的注释,用于切入点解析和匹配。 AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或weaver。

使用AspectJ编译器和weaver可以使用完整的AspectJ语言,并在第11.8节“将AspectJ与Spring应用程序一起使用”中进行了讨论。

11.2.1 启用@AspectJ支持

要在Spring配置中使用@AspectJ方面,您需要启用Spring支持,以便基于@AspectJ方面配置Spring AOP,并根据这些方面是否建议来自动执行bean。通过autoproxying我们的意思是,如果Spring确定bean被一个或多个方面建议,它将自动生成该bean的代理以拦截方法调用并确保根据需要执行建议。

可以使用XML或Java样式配置启用@AspectJ支持。在任何一种情况下,您还需要确保AspectJ的 aspectjweaver.jar 库位于应用程序的类路径中(版本1.6.8或更高版本)。该库可在AspectJ发行版的 'lib' 目录中或通过Maven Central存储库获得。

使用Java配置启用@AspectJ支持

要使用Java @Configuration 启用@AspectJ支持,请添加 @EnableAspectJAutoProxy 注释:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

使用XML配置启用@AspectJ支持

要使用基于XML的配置启用@AspectJ支持,请使用 aop:aspectj-autoproxy 元素:

<aop:aspectj-autoproxy/>

这假设您正在使用 Chapter 41, XML Schema-based configuration 中描述的架构支持。有关如何在 aop 命名空间中导入标记,请参阅 Section 41.2.7, “the aop schema”

11.2.2 声明一个方面

在启用@AspectJ支持的情况下,应用程序上下文中定义的任何bean都具有@AspectJ方面的类(具有 @Aspect 注释)将由Spring自动检测并用于配置Spring AOP。以下示例显示了非常有用的方面所需的最小定义:

应用程序中的常规bean定义上下文,指向具有 @Aspect 注释的bean类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of aspect here as normal -->
</bean>

NotVeryUsefulAspect 类定义,用 org.aspectj.lang.annotation.Aspect 注释注释;

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

方面(使用 @Aspect 注释的类)可能具有与任何其他类一样的方法和字段。它们还可能包含切入点,建议和引入(类型间)声明。

您可以在Spring XML配置中将方面类注册为常规bean,或者通过类路径扫描自动检测它们 - 就像任何其他Spring管理的bean一样。但是,请注意@Aspect注释不足以在类路径中自动检测:为此,您需要添加单独的@Component注释(或者根据Spring的组件扫描程序的规则添加符合条件的自定义构造型注释)。

在Spring AOP中,不可能将方面本身作为其他方面建议的目标。类上的@Aspect注释将其标记为方面,因此将其从自动代理中排除。

11.2.3 声明切入点

回想一下,切入点确定了感兴趣的连接点,从而使我们能够控制建议何时执行。 Spring AOP仅支持Spring bean的方法执行连接点,因此您可以将切入点视为匹配Spring bean上方法的执行。切入点声明有两个部分:一个包含名称和任何参数的签名,以及一个精确确定我们感兴趣的方法执行的切入点表达式。在AOP的@AspectJ注释样式中,切入点签名由常规方法提供定义,并使用 @Pointcut 注释指示切入点表达式(用作切入点签名的方法必须具有 void 返回类型)。

一个示例将有助于区分切入点签名和切入点表达式。以下示例定义名为 'anyOldTransfer' 的切入点,该切入点将匹配名为 'transfer' 的任何方法的执行:

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

形成 @Pointcut 注释值的切入点表达式是常规的AspectJ 5切入点表达式。有关AspectJ切入点语言的完整讨论,请参阅 AspectJ Programming Guide (和扩展名, AspectJ 5 Developers Notebook )或AspectJ上的一本书,如Colyer等的 "Eclipse AspectJ" 。人。或者是Ramnivas Laddad的 "AspectJ in Action" 。

支持的切入点指示符

Spring AOP支持以下AspectJ切入点指示符(PCD)用于切入点表达式:


Other pointcut types

完整的AspectJ切入点语言支持Spring中不支持的其他切入点指示符。它们是: call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this@withincode 。在Spring AOP解释的切入点表达式中使用这些切入点指示符将导致抛出 IllegalArgumentException

Spring AOP支持的切入点指示符集可以在将来的版本中进行扩展,以支持更多的AspectJ切入点指示符。


  • 执行 - 对于匹配方法执行连接点,这是使用Spring AOP时将使用的主要切入点指示符

  • within - 限制匹配某些类型中的连接点(只是在使用Spring AOP时执行在匹配类型中声明的方法)

  • this - 限制匹配到连接点(使用Spring AOP时执行方法),其中bean引用(Spring AOP代理)是给定类型的实例

  • target - 限制匹配连接点(使用Spring AOP时执行方法),其中目标对象(被代理的应用程序对象)是给定类型的实例

  • args - 限制匹配连接点(使用Spring AOP时执行方法),其中参数是给定类型的实例

  • @target - 限制匹配连接点(使用Spring AOP时执行方法),其中执行对象的类具有给定类型的注释

  • @args - 限制匹配到连接点(使用Spring AOP时执行方法),其中传递的实际参数的运行时类型具有给定类型的注释

  • @within - 限制匹配以连接具有给定注释的类型中的点(使用Spring AOP时在具有给定注释的类型中声明的方法的执行)

  • @annotation - 限制连接点的匹配,其中连接点的主题(在Spring AOP中执行的方法)具有给定的注释

由于Spring AOP仅限制与方法执行连接点的匹配,因此上面对切入点指示符的讨论给出了比在AspectJ编程指南中找到的更窄的定义。此外,AspectJ本身具有基于类型的语义,并且在执行连接点处 thistarget 都引用相同的对象 - 执行该方法的对象。 Spring AOP是一个基于代理的系统,它区分代理对象本身(绑定到 this )和代理后面的目标对象(绑定到 target )。

到期对于Spring的AOP框架的基于代理的特性,根据定义,目标对象内的调用不会被截获。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,代理上的公共和受保护方法调用将被拦截,甚至包括必要的包可见方法。但是,通过代理进行的常见交互应始终通过公共签名进行设计。请注意,切入点定义通常与任何截获的方法匹配。如果切入点严格意义上是公开的,即使在通过代理进行潜在非公共交互的CGLIB代理方案中,也需要相应地定义切入点。如果你的拦截需要包括方法调用甚至是目标类中的构造函数,那么考虑使用Spring驱动的原生AspectJ编织而不是Spring的基于代理的AOP框架。这构成了具有不同特征的不同AOP使用模式,因此在做出决定之前一定要先熟悉编织。

Spring AOP还支持另一个名为 bean 的PCD。此PCD允许您限制连接点与特定命名的Spring bean或一组命名的Spring bean(使用通配符时)的匹配。 bean PCD具有以下形式:

bean(idOrNameOfBean)

idOrNameOfBean 标记可以是任何Spring bean的名称:提供了使用 * 字符的有限通配符支持,因此如果为Spring bean Build 一些命名约定,则可以非常轻松地编写 bean PCD表达式来选择它们。与其他切入点指示符的情况一样, bean PCD可以是&&',||'和! (否定)也是。

请注意,bean PCD仅在Spring AOP中受支持 - 而不是在原生AspectJ编织中。它是AspectJ定义的标准PCD的Spring特定扩展,因此不适用于@Aspect模型中声明的方面。 bean PCD在实例级别(基于Spring bean名称概念)而不是仅在类型级别(这是基于编织的AOP限制)运行。基于实例的切入点指示符是Spring基于代理的AOP框架的一种特殊功能,它与Spring bean工厂紧密集成,通过名称可以自然而直接地识别特定的bean。

组合切入点表达式

可以使用'&&','||'组合切入点表达式和'!'。也可以通过名称引用切入点表达式。以下示例显示了三个切入点表达式: anyPublicOperation (如果方法执行连接点表示任何公共方法的执行,则匹配); inTrading (如果方法执行在 Transaction 模块中,则匹配)和 tradingOperation (如果方法执行表示 Transaction 模块中的任何公共方法,则匹配)。

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

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

共享公共切入点定义

使用企业应用程序时,您经常需要从几个方面引用应用程序的模块和特定的操作集。我们建议定义一个“SystemArchitecture”方面,为此目的捕获常见的切入点表达式。典型的这种方面看起来如下:

package com.xyz.someapp;

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

@Aspect
public class SystemArchitecture {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

在这样的方面定义的切入点可以被引用到您需要切入点表达式的任何地方。例如,要使服务层具有事务性,您可以编写:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config><aop:advisor> 元素在 Section 11.3, “Schema-based AOP support” 中讨论。事务元素在 Chapter 17, Transaction Management 中讨论。

示例

Spring AOP用户可能最常使用 execution 切入点指示符。执行表达式的格式为:

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

除返回类型模式(上面的代码段中的ret-type-pattern),名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配连接点。最常见的是,您将使用 * 作为返回类型模式,它匹配任何返回类型。仅当方法返回给定类型时,完全限定类型名称才匹配。名称模式与方法名称匹配。您可以使用 * 通配符作为名称模式的全部或部分。如果指定声明类型模式,则包括尾随 . 以将其连接到名称模式组件。参数模式稍微复杂一些: () 匹配不带参数的方法,而 (..) 匹配任意数量的参数(零或更多)。模式 (*) 匹配采用任何类型的一个参数的方法, (*,String) 匹配a方法取两个参数,第一个可以是任何类型,第二个必须是String。有关更多信息,请参阅AspectJ编程指南的 Language Semantics 部分。

下面给出了常见切入点表达式的一些示例。

  • 执行任何公共方法:
execution(public * *(..))
  • 执行名称以 "set" 开头的任何方法:
execution(* set*(..))
  • 执行 AccountService 接口定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))
  • 执行服务包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
  • 执行服务包或子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))
  • 服务包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service.*)
  • 服务包或子包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service..*)
  • 代理实现 AccountService 接口的任何连接点(仅在Spring AOP中执行方法):
this(com.xyz.service.AccountService)

'this'更常用于绑定形式: - 请参阅以下有关如何在建议体中提供代理对象的建议。

  • 目标对象实现 AccountService 接口的任何连接点(仅在Spring AOP中执行方法):
target(com.xyz.service.AccountService)

'target'更常用于绑定形式: - 请参阅以下有关如何在建议体中提供目标对象的建议。

  • 任何连接点(仅在Spring AOP中执行的方法),它接受一个参数,并且在运行时传递的参数是 Serializable
args(java.io.Serializable)

'args'更常用于绑定形式: - 请参阅以下有关如何在建议体中提供方法参数的建议。

请注意,此示例中给出的切入点与 execution(* *(java.io.Serializable)) 不同:如果在运行时传递的参数是Serializable,则args版本匹配,如果方法签名声明了类型为 Serializable 的单个参数,则执行版本匹配。

  • 目标对象具有 @Transactional 注释的任何连接点(仅在Spring AOP中执行方法):
@target(org.springframework.transaction.annotation.Transactional)

'@ target'也可用于绑定形式: - 请参阅以下有关如何在建议体中提供注释对象的建议。

  • 任何连接点(仅在Spring AOP中执行方法),其中目标对象的声明类型具有 @Transactional 注释:
@within(org.springframework.transaction.annotation.Transactional)

'@ within'也可用于绑定形式: - 请参阅以下有关如何在建议体中提供注释对象的建议。

  • 任何连接点(仅在Spring AOP中执行方法),其中执行方法具有 @Transactional 注释:
@annotation(org.springframework.transaction.annotation.Transactional)

'@ annotation'也可用于绑定形式: - 请参阅以下有关如何在建议体中提供注释对象的建议。

  • 任何连接点(仅在Spring AOP中执行的方法),它接受一个参数,并且传递的参数的运行时类型具有 @Classified 注释:
@args(com.xyz.security.Classified)

'@ args'也可用于绑定形式: - 请参阅以下有关如何在建议体中提供注释对象的建议。

  • 名为 tradeService 的Spring bean上的任何连接点(仅在Spring AOP中执行方法):
bean(tradeService)
  • 具有与通配符表达式 *Service 匹配的名称的Spring bean上的任何连接点(仅在Spring AOP中执行方法):
bean(*Service)

写好切入点

在编译期间,AspectJ处理切入点以尝试和优化匹配性能。检查代码并确定每个连接点是否(静态地或动态地)匹配给定切入点是一个代价高昂的过程。 (动态匹配意味着无法通过静态分析完全确定匹配,并且将在代码中放置测试以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ会将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点在DNF(析取范式)中重写,并且切入点的组件被排序,以便首先检查那些评估成本更低的组件。这意味着您不必担心了解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。

但是,AspectJ只能处理它所说的内容,并且为了获得最佳匹配性能,您应该考虑它们想要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三组:kinded,scoping和context:

  • Kinded指示符是选择特定类型的连接点的指示符。例如:执行,获取,设置,调用,处理程序

  • 范围界定指示符是那些选择一组感兴趣的连接点(可能是许多连接点)的指示符种)。例如:内部,内部代码

  • 上下文指示符是基于上下文匹配(并可选地绑定)的指示符。例如:this,target,@ annotation

一个写得很好的切入点应该尝试包括至少前两种类型(kinded和scoping),而如果希望基于连接点上下文匹配,则可以包括上下文指示符,或者绑定该上下文以在建议中使用。仅提供一个kinded指示符或仅提供上下文指示符将起作用,但由于所有额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围界定指示符非常快速匹配,它们的使用意味着AspectJ可以非常快速地消除不应该进一步处理的连接点组 - 这就是为什么一个好的切入点应该总是包括一个如果可能的原因。

11.2.4 宣布建议

建议与切入点表达式相关联,并在切入点匹配的方法执行之前,之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是在适当位置声明的切入点表达式。

在建议之前

在使用 @Before 注释在方面中声明建议之前:

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

@Aspect
public class BeforeExample {

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

}

如果使用就地切入点表达式,我们可以将上面的示例重写为:

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

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}

返回建议后

返回建议后,匹配的方法执行正常返回。它是使用 @AfterReturning 注释声明的:

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

@Aspect
public class AfterReturningExample {

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

}

注意:当然可以在同一方面内有多个建议声明和其他成员。我们只是在这些例子中展示了一个建议声明,专注于当时正在讨论的问题。

有时您需要在建议体中访问返回的实际值。您可以使用 @AfterReturning 的形式来绑定返回值:

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

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning 属性中使用的名称必须与advice方法中的参数名称相对应。当方法执行返回时,返回值将作为相应的参数值传递给advice方法。 returning 子句还将匹配仅限于那些返回指定类型值的方法执行(在这种情况下为 Object ,它将匹配任何返回值)。

请注意,在使用返回后的建议时,无法返回完全不同的参考。

投掷建议后

抛出建议运行时,匹配的方法执行通过抛出异常退出。它是使用 @AfterThrowing 注释声明的:

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

@Aspect
public class AfterThrowingExample {

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

}

通常,您希望仅在抛出给定类型的异常时才运行建议,并且您还经常需要访问建议体中的抛出异常。使用 throwing 属性来限制匹配(如果需要,否则使用 Throwable 作为异常类型)并将抛出的异常绑定到advice参数。

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

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

throwing 属性中使用的名称必须与advice方法中的参数名称相对应。当通过抛出异常退出方法时,异常将作为相应的参数值传递给advice方法。 throwing 子句还将匹配仅限于那些抛出指定类型异常的方法执行(在本例中为 DataAccessException )。

(最后)建议之后

在(最终)建议运行之后,匹配的方法执行退出。它是使用 @After 注释声明的。在建议必须准备好处理正常和异常返回条件。它通常用于释放资源等。

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

@Aspect
public class AfterFinallyExample {

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

}

周围的建议

最后一种建议是建议。周围的建议围绕匹配的方法执行运行。它有机会在方法执行之前和之后完成工作,并确定方法实际上何时,如何,甚至是否实际执行。如果您需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则经常使用around建议。始终使用符合您要求的最不强大的建议形式(即如果在建议之前,请不要使用简单的建议)。

使用 @Around 注释声明around建议。 advice方法的第一个参数必须是 ProceedingJoinPoint 类型。在建议的主体内,在 ProceedingJoinPoint 上调用 proceed() 会导致执行基础方法。也可以调用 proceed 方法传入 Object[] - 数组中的值将在进行时用作方法执行的参数。

使用Object []调用时,proceed的行为与由AspectJ编译器编译的around建议的行为略有不同。对于使用传统AspectJ语言编写的周围建议,传递给proceed的参数数量必须与传递给around建议的参数数量相匹配(而不是参数的数量)底层连接点),并且传递给在给定参数位置继续的值取代了值绑定到的实体的连接点处的原始值(如果现在没有意义,请不要担心!)。 Spring采用的方法更简单,更好地匹配其基于代理的,仅执行语义。如果要编译为Spring编写的@AspectJ方面并使用带有AspectJ编译器和weaver的参数继续,则只需要知道这种差异。有一种方法可以编写在Spring AOP和AspectJ上100%兼容的方面,这将在下面的建议参数部分中讨论。

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

@Aspect
public class AroundExample {

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

}

around通知返回的值将是方法调用者看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值(如果有的话),如果没有,则调用proceed()。请注意,可以在周围建议的主体内调用一次,多次或根本不调用,所有这些都是非常合法的。

建议参数

Spring提供完全类型的建议 - 这意味着您在建议签名中声明了所需的参数(正如我们在上面看到的返回和抛出示例所示),而不是始终使用 Object[] 数组。我们将看到如何在一瞬间为建议主体提供参数和其他上下文值。首先让我们来看看如何编写通用建议,以便了解建议目前建议的方法。

访问当前的JoinPoint

任何通知方法都可以声明为它的第一个参数,类型为 org.aspectj.lang.JoinPoint 的参数(请注意,需要提供建议来声明 ProceedingJoinPoint 类型的第一个参数,它是 JoinPoint 的子类. JoinPoint 接口提供了许多有用的方法,例如 getArgs() (返回方法参数), getThis() (返回代理对象), getTarget() (返回目标对象), getSignature() (返回正在建议的方法的描述)和 toString() (打印建议方法的有用描述)请查阅javadocs了解详细信息。

将参数传递给建议

我们已经看到了如何绑定返回的值或异常值(在返回之后和抛出建议之后使用)。要使参数值可用于建议体,您可以使用 args 的绑定形式。如果在args表达式中使用参数名称代替类型名称,则在调用通知时,相应参数的值将作为参数值传递。一个例子应该使这更清楚。假设您要建议执行以Account对象作为第一个参数的dao操作,并且您需要访问建议体中的帐户。你可以写下面的内容:

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

切入点表达式的 args(account,..) 部分有两个目的:首先,它将匹配仅限于那些方法至少接受一个参数的方法执行,而传递给该参数的参数是 Account 的实例;其次,它通过 account 参数使实际的 Account 对象可用于建议。

另一种写入方式是在匹配连接点时声明 "provides" Account 对象值的切入点,然后从建议中引用指定的切入点。这看起来如下:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

感兴趣的读者再次参考AspectJ编程指南以获取更多详细信息。

代理对象( this ),目标对象( target )和注释( @within, @target, @annotation, @args )都可以以类似的方式绑定。以下示例显示了如何匹配使用 @Auditable 注释注释的方法的执行,并提取审计代码。

首先是 @Auditable 注释的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

然后是与 @Auditable 方法的执行相匹配的建议:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}
建议参数和泛型

Spring AOP可以处理类声明和方法参数中使用的泛型。假设您有这样的泛型类型:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

您可以通过在要拦截方法的参数类型中键入advice参数,将方法类型的拦截限制为某些参数类型:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

正如我们上面已经讨论的那样,这很有效。但是,值得指出的是,这不适用于通用集合。所以你不能像这样定义一个切入点:

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

为了完成这项工作,我们必须检查集合中的每个元素,这是不合理的,因为我们也无法决定如何处理 null 值。要实现与此类似的操作,您必须将参数键入 Collection<?> 并手动检查元素的类型。

确定参数名称

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

  • 如果用户明确指定了参数名称,则使用指定的参数名称:通知和切入点注释都有一个可选的 "argNames" 属性,可用于指定带注释的方法的参数名称 - 这些参数名称在运行时可用。例如:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一个参数是 JoinPointProceedingJoinPointJoinPoint.StaticPart 类型,则可以从 "argNames" 属性的值中省略参数的名称。例如,如果修改前面的建议以接收连接点对象,则 "argNames" 属性不需要包含它:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

对于不收集任何其他连接点上下文的建议,对 JoinPointProceedingJoinPointJoinPoint.StaticPart 类型的第一个参数的特殊处理特别方便。在这种情况下,您可以简单地省略 "argNames" 属性。例如,以下建议无需声明 "argNames" 属性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
  • 使用 'argNames' 属性有点笨拙,因此如果未指定 'argNames' 属性,则Spring AOP将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息(至少为 '-g:vars' )编译了类,就会显示此信息。使用此标志进行编译的后果是:(1)您的代码将更容易理解(逆向工程),(2)类文件大小将略微更大(通常无关紧要),(3)要删除的优化编译器不会应用未使用的局部变量。换句话说,使用此标志构建时不会遇到任何困难。

如果没有调试信息,AspectJ编译器(ajc)编译了@AspectJ方面,则无需添加argNames属性,因为编译器将保留所需的信息。

  • 如果代码编译时没有必要的调试信息,则Spring AOP将尝试推断绑定变量与参数的配对(例如,如果只有一个变量绑定在切入点表达式中,并且advice方法只接受一个参数,配对很明显!)。如果给定可用信息,变量的绑定是不明确的,那么将抛出 AmbiguousBindingException

  • 如果上述所有策略都失败,则会抛出 IllegalArgumentException

继续论证

我们之前评论过,我们将描述如何使用在Spring AOP和AspectJ中一致工作的参数编写一个继续调用。解决方案只是确保建议签名按顺序绑定每个方法参数。例如:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

在许多情况下,无论如何你都会做这个绑定(如上例所示)。

建议订购

当多条建议都想在同一个连接点运行时会发生什么? Spring AOP遵循与AspectJ相同的优先级规则来确定建议执行的顺序。最高优先级的建议首先“在路上”(因此,给出两条之前的建议,优先级最高的建议首先运行)。从连接点“出路”,最高优先级建议最后运行(因此,给出两条后建议,具有最高优先级的建议将运行第二)。

当在不同方面定义的两条建议都需要在同一个连接点运行时,除非您另行指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这是通过在方法类中实现 org.springframework.core.Ordered 接口或使用 Order 注释对其进行注释来以常规Spring方式完成的。给定两个方面,从 Ordered.getValue() (或注释值)返回较低值的方面具有较高的优先级。

当在同一方面定义的两条建议都需要在同一个连接点上运行时,排序是未定义的(因为没有办法通过反射为javac编译的类检索声明顺序)。考虑将这些建议方法折叠到每个方面类中每个连接点的一个建议方法中,或者将这些建议重构为单独的方面类 - 可以在方面级别进行排序。

11.2.5 介绍

简介(在AspectJ中称为类型间声明)使方面能够声明建议对象实现给定接口,并代表这些对象提供该接口的实现。

使用 @DeclareParents 注释进行了介绍。此批注用于声明匹配类型具有新父级(因此名称)。例如,给定接口 UsageTracked ,以及该接口 DefaultUsageTracked 的实现,以下方面声明服务接口的所有实现者也是实现 UsageTracked 接口。 (例如,为了通过JMX公开统计信息。)

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要实现的接口由注释字段的类型确定。 @DeclareParents 注释的 value 属性是一种AspectJ类型模式: - 任何匹配类型的bean都将实现UsageTracked接口。请注意,在上面示例的before advice中,服务bean可以直接用作 UsageTracked 接口的实现。如果以编程方式访问bean,您将编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

11.2.6 Aspect实例化模型

(这是一个高级主题,所以如果你刚刚开始使用AOP,你可以安全地跳过它直到以后。)

默认情况下,应用程序上下文中的每个方面都有一个实例。 AspectJ将其称为单例实例化模型。可以使用备用生命周期定义方面: - Spring支持AspectJ的 perthispertarget 实例化模型(当前不支持 percflow, percflowbelow,pertypewithin )。

通过在 @Aspect 注释中指定 perthis 子句来声明 "perthis" 方面。让我们看一个例子,然后我们将解释它是如何工作的。

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

    private int someState;

    @Before(com.xyz.myapp.SystemArchitecture.businessService())
    public void recordServiceUsage() {
        // ...
    }

}

'perthis' 子句的作用是为执行业务服务的每个唯一服务对象创建一个方面实例(每个唯一对象在由切入点表达式匹配的连接点处绑定到'this')。方法实例是在第一次在服务对象上调用方法时创建的。当服务对象超出范围时,该方面超出范围。在创建方面实例之前,其中没有任何建议执行。一旦创建了方面实例,在其中声明的建议将在匹配的连接点处执行,但仅在服务对象是与此方面相关联的服务对象时执行。有关per子句的更多信息,请参阅AspectJ编程指南。

'pertarget' 实例化模型的工作方式与perthis完全相同,但在匹配的连接点为每个唯一目标对象创建一个方面实例。

11.2.7 示例

既然你已经看到了所有组成部分的工作方式,那就让我们把它们放在一起做一些有用的事情吧!

由于并发问题(例如,死锁失败者),业务服务的执行有时可能会失败。如果重试该操作,下次很可能成功。对于适合在这种情况下重试的业务服务(不需要返回给用户进行冲突解决的幂等操作),我们希望透明地重试该操作以避免客户端看到 PessimisticLockingFailureException 。这是明确跨越服务层中的多个服务的要求,因此是通过方面实现的理想选择。

因为我们想要重试操作,所以我们需要使用around建议,以便我们可以多次调用proceed。以下是基本方面实现的外观:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

请注意,该方面实现了 Ordered 接口,因此我们可以将方面的优先级设置为高于事务通知(我们每次重试时都需要一个新的事务)。 maxRetriesorder 属性都将由Spring配置。主要行动发生在 doConcurrentOperation 周围的建议。请注意,目前我们正在将重试逻辑应用于所有 businessService()s 。我们试图继续进行,如果我们失败了,我们只需再试一次,除非我们已经用尽所有的重试尝试。

相应的Spring配置是:

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

为了优化方面以便它只重试幂等操作,我们可以定义一个 Idempotent 注释:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

并使用注释来注释服务操作的实现。对方面的更改仅重试幂等操作只涉及改进切入点表达式,以便只有 @Idempotent 个操作匹配:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

11.3 基于模式的AOP支持

如果您更喜欢基于XML的格式,那么Spring还支持使用新的 "aop" 命名空间标记定义方面。使用@AspectJ样式时支持完全相同的切入点表达式和建议类型,因此在本节中我们将重点介绍新语法,并引用读者阅读上一节( Section 11.2, “@AspectJ support” )中的讨论,以了解编写切入点表达式和建议参数的绑定。

要使用本节中描述的aop命名空间标记,您需要按照 Chapter 41, XML Schema-based configuration 中的描述导入 spring-aop 模式。有关如何在 aop 命名空间中导入标记,请参见 Section 41.2.7, “the aop schema”

在Spring配置中,所有aspect和advisor元素必须放在 <aop:config> 元素中(在应用程序上下文配置中可以有多个 <aop:config> 元素)。 <aop:config> 元素可以包含切入点,顾问程序和方面元素(请注意这些元素必须按此顺序声明)。

<aop:config>的样式配置大量使用Spring的自动代理机制。如果您已经通过使用BeanNameAutoProxyCreator等使用显式自动代理,这可能会导致问题(例如建议不被编织)。建议的使用模式是仅使用<aop:config>样式,或仅使用AutoProxyCreator样式。

11.3.1 声明一个方面

使用模式支持,方面只是在Spring应用程序上下文中定义为bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在XML中捕获。

使用<aop:aspect>元素声明方面,并使用 ref 属性引用辅助bean:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>

支持方面的bean(在这种情况下是 "aBean" )当然可以像任何其他Spring bean一样配置和依赖注入。

11.3.2 声明切入点

可以在<aop:config>元素内声明命名切入点,从而使切入点定义能够跨多个方面和顾问程序共享。

表示服务层中任何业务服务执行的切入点可以定义如下:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

请注意,切入点表达式本身使用与 Section 11.2, “@AspectJ support” 中描述的相同的AspectJ切入点表达式语言。如果使用基于模式的声明样式,则可以引用切入点表达式中类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法是:

<aop:config>

    <aop:pointcut id="businessService"
        expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

假设你有一个 SystemArchitecture 方面,如 the section called “Sharing common pointcut definitions” 所述。

在方面内部声明切入点与声明顶级切入点非常相似:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...

    </aop:aspect>

</aop:config>

在@AspectJ方面的方式大致相同,使用基于模式的定义样式声明的切入点可能会收集连接点上下文。例如,以下切入点将'this'对象收集为连接点上下文并将其传递给建议:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

必须通过包含匹配名称的参数来声明建议以接收收集的连接点上下文:

public void monitor(Object service) {
    ...
}

组合切入点子表达式时, && 在XML文档中很难处理,因此可以分别使用关键字 andornot 代替 &&||! 。例如,之前的切入点可能更好地写为:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

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

请注意,以这种方式定义的切入点由其XML ID引用,不能用作命名切入点来形成复合切入点。因此,基于模式的定义样式中的命名切入点支持比@AspectJ样式提供的更有限。

11.3.3 宣布建议

对于@AspectJ样式,支持相同的五种建议类型,它们具有完全相同的语义。

在建议之前

在匹配的方法执行之前运行建议之前。它使用<aop:before>元素在 <aop:aspect> 内声明。

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

这里 dataAccessOperation 是在顶部( <aop:config> )级别定义的切入点的id。要改为内联切入点,请将 pointcut-ref 属性替换为 pointcut 属性:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

正如我们在讨论@AspectJ样式时所提到的,使用命名切入点可以显着提高代码的可读性。

method属性标识提供建议正文的方法( doAccessCheck )。必须为包含通知的aspect元素引用的bean定义此方法。在执行数据访问操作(由切入点表达式匹配的方法执行连接点)之前,将调用方面bean上的 "doAccessCheck" 方法。

返回建议后

在匹配的方法执行正常完成后返回通知运行。它在 <aop:aspect> 中以与建议之前相同的方式声明。例如:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

就像在@AspectJ样式中一样,可以在建议体内获得返回值。使用returns属性指定应将返回值传递到的参数的名称:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck方法必须声明一个名为 retVal 的参数。此参数的类型以与@AfterReturning所述相同的方式约束匹配。例如,方法签名可以声明为:

public void doAccessCheck(Object retVal) {...

投掷建议后

抛出建议执行时,匹配的方法执行通过抛出异常退出。它使用投掷后元素在 <aop:aspect> 内声明:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

就像在@AspectJ样式中一样,可以在建议体内获取抛出的异常。使用throwing属性指定应将异常传递到的参数的名称:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必须声明一个名为 dataAccessEx 的参数。此参数的类型以与@AfterThrowing相同的方式约束匹配。例如,方法签名可以声明为:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

之后(最后)忠告

在(最终)建议运行之后,匹配的方法执行退出。它使用 after 元素声明:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

周围的建议

最后一种建议是建议。周围的建议围绕匹配的方法执行运行。它有机会在方法执行之前和之后完成工作,并确定方法实际上何时,如何,甚至是否实际执行。如果您需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则经常使用around建议。始终使用符合您要求的最不强大的建议形式;如果简单,建议之前不要使用周围的建议。

使用 aop:around 元素声明around建议。 advice方法的第一个参数必须是 ProceedingJoinPoint 类型。在建议的主体内,在 ProceedingJoinPoint 上调用 proceed() 会导致执行基础方法。 proceed 方法也可能正在调用传递 Object[] - 数组中的值将在进行时用作方法执行的参数。有关调用继续 Object[] 的说明,请参阅 the section called “Around advice”

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling 建议的实现与@AspectJ示例中的完全相同(当然减去注释):

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

建议参数

基于模式的声明样式支持完全类型化的建议,方法与@AspectJ支持描述的方式相同 - 通过名称匹配切入点参数和建议方法参数。有关详细信息,请参阅 the section called “Advice parameters” 。如果您希望显式指定通知方法的参数名称(不依赖于前面描述的检测策略),那么这是使用advice元素的 arg-names 属性完成的,该属性的处理方式与建议注释中的 "argNames" 属性相同。如 the section called “Determining argument names” 中所述。例如:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names 属性接受以逗号分隔的参数名称列表。

下面是一个基于XSD的方法的一个稍微复杂的例子,它说明了与一些强类型参数一起使用的一些建议。

package x.y.service;

public interface FooService {

    Foo getFoo(String fooName, int age);
}

public class DefaultFooService implements FooService {

    public Foo getFoo(String name, int age) {
        return new Foo(name, age);
    }
}

接下来是方面。请注意 profile(..) 方法接受许多强类型参数这一事实,第一个参数恰好是用于继续方法调用的连接点:此参数的存在表明 profile(..) 将用作 around 建议:

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}

最后,这是为特定连接点执行上述建议所需的XML配置:

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

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

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

            <aop:pointcut id="theExecutionOfSomeFooServiceMethod"
                expression="execution(* x.y.service.FooService.getFoo(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"
                method="profile"/>

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

</beans>

如果我们有以下驱动程序脚本,我们将在标准输出上获得类似的输出:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.FooService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        FooService foo = (FooService) ctx.getBean("fooService");
        foo.getFoo("Pengo", 12);
    }
}
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

建议订购

当多个建议需要在同一个连接点(执行方法)执行时,排序规则如 the section called “Advice ordering” 中所述。方面之间的优先级是通过将 Order 注释添加到支持方面的bean或通过使bean实现 Ordered 接口来确定的。

11.3.4 介绍

简介(在AspectJ中称为类型间声明)使方面能够声明建议对象实现给定接口,并代表这些对象提供该接口的实现。

使用 aop:aspect 中的 aop:declare-parents 元素进行了介绍。此元素用于声明匹配类型具有新父级(因此名称)。例如,给定接口 UsageTracked ,以及该接口 DefaultUsageTracked 的实现,以下方面声明服务接口的所有实现者也实现 UsageTracked 接口。 (例如,为了通过JMX公开统计信息。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持 usageTracking bean的类将包含以下方法:

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要实现的接口由 implement-interface 属性确定。 types-matching 属性的值是AspectJ类型模式: - 任何匹配类型的bean都将实现 UsageTracked 接口。请注意,在上面示例的before advice中,服务bean可以直接用作 UsageTracked 接口的实现。如果以编程方式访问bean,您将编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

11.3.5 Aspect实例化模型

模式定义方面唯一支持的实例化模型是单例模型。未来的版本可能支持其他实例化模型。

11.3.6 顾问

"advisors" 的概念是从Spring中定义的AOP支持中提出的,并且在AspectJ中没有直接的等价物。顾问就像一个小小的自足方面,只有一条建议。建议本身由bean表示,并且必须实现 Section 12.3.2, “Advice types in Spring” 中描述的建议接口之一。顾问可以利用AspectJ切入点表达式。

Spring使用 <aop:advisor> 元素支持顾问程序概念。你最常见它与事务性建议结合使用,在Spring中也有自己的命名空间支持。以下是它的外观:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了上面示例中使用的 pointcut-ref 属性之外,您还可以使用 pointcut 属性来内联定义切入点表达式。

要定义顾问程序的优先级以便建议可以参与排序,请使用 order 属性来定义顾问程序的 Ordered 值。

11.3.7 示例

让我们看看当使用模式支持重写时, Section 11.2.7, “Example” 中的并发锁定失败重试示例如何。

由于并发问题(例如,死锁失败者),业务服务的执行有时可能会失败。如果重试该操作,下一次它很可能会成功。对于适合在这种情况下重试的业务服务(不需要返回给用户进行冲突解决的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException 。这是明确跨越服务层中的多个服务的要求,因此是通过方面实现的理想选择。

因为我们想要重试操作,所以我们需要使用around建议,以便我们可以多次调用proceed。以下是基本方面实现的外观(它只是使用模式支持的常规Java类):

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

请注意,该方面实现了 Ordered 接口,因此我们可以将方面的优先级设置为高于事务通知(我们每次重试时都需要一个新的事务)。 maxRetriesorder 属性都将由Spring配置。主要操作发生在 doConcurrentOperation around advice方法中。我们试图继续,如果我们失败了 PessimisticLockingFailureException 我们只是再试一次,除非我们已经用尽所有的重试尝试。

此类与@AspectJ示例中使用的类相同,但删除了注释。

相应的Spring配置是:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

请注意,目前我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入 Idempotent 注释来优化方面,使其仅重试真正的幂等操作:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

并使用注释来注释服务操作的实现。对方面进行更改以仅重试幂等操作只需要改进切入点表达式,以便只有 @Idempotent 个操作匹配:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

11.4 选择要使用的AOP声明样式

一旦确定某个方面是实现给定需求的最佳方法,您如何决定使用Spring AOP或AspectJ,以及Aspect语言(代码)样式,@ AspectJ注释样式还是Spring XML样式之间?这些决策受到许多因素的影响,包括应用程序要求,开发工具和团队对AOP的熟悉程度。

11.4.1 Spring AOP还是完整的AspectJ?

使用最简单的方法。 Spring AOP比使用完整的AspectJ更简单,因为不需要将AspectJ编译器/ weaver引入开发和构建过程。如果您只需要建议在Spring bean上执行操作,那么Spring AOP是正确的选择。如果您需要建议不由Spring容器管理的对象(通常是域对象),那么您将需要使用AspectJ。如果您希望建议除简单方法执行之外的连接点(例如,字段获取或设置连接点等),您还需要使用AspectJ。

使用AspectJ时,您可以选择AspectJ语言语法(也称为 "code style" )或@AspectJ注释样式。显然,如果您没有使用Java 5,那么已经为您做出了选择...使用代码样式。如果方面在您的设计中发挥重要作用,并且您能够使用Eclipse的 AspectJ Development Tools (AJDT) 插件,则AspectJ语言语法是首选选项:它更清晰,更简单,因为该语言是专门为编写方面而设计的。如果您没有使用Eclipse,或只有几个方面在您的应用程序中不起主要作用,那么您可能需要考虑使用@AspectJ样式并在IDE中坚持使用常规Java编译,并添加一个方面编织阶段到您的构建脚本。

11.4.2 @AspectJ或Spring AOP的XML?

如果您选择使用Spring AOP,那么您可以选择@AspectJ或XML样式。需要考虑各种权衡。

XML样式对于现有的Spring用户来说是最熟悉的,并且由真正的POJO支持。当使用AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个好的测试是你是否认为切入点表达式是你可能想要独立改变的配置的一部分)。使用XML风格可以说,从您的配置中可以更清楚地了解系统中存在哪些方面。

XML风格有两个缺点。首先,它没有完全封装它在一个地方解决的要求的实现。 DRY原则指出,系统中的任何知识都应该有单一,明确,权威的表示。使用XML样式时,有关如何实现需求的知识将分支到支持bean类的声明和配置文件中的XML。使用@AspectJ样式时,有一个模块 - 方面 - 用于封装此信息。其次,XML样式在它可以表达的内容方面比@AspectJ样式稍微受限:仅支持 "singleton" aspect实例化模型,并且不可能组合在XML中声明的命名切入点。例如,在@AspectJ样式中,您可以编写如下内容:

@Pointcut(execution(* get*()))
public void propertyAccess() {}

@Pointcut(execution(org.xyz.Account+ *(..))
public void operationReturningAnAccount() {}

@Pointcut(propertyAccess() && operationReturningAnAccount())
public void accountPropertyAccess() {}

在XML样式中,我可以声明前两个切入点:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺点是您无法通过组合这些定义来定义 accountPropertyAccess 切入点。

@AspectJ样式支持额外的实例化模型和更丰富的切入点组合。它具有将方面保持为模块化单元的优点。它还具有以下优点:Spring AOP和AspectJ可以理解(并因此消耗)@AspectJ方面 - 因此,如果您以后决定需要AspectJ的功能来实现其他需求,那么迁移到AspectJ非常容易基于方法。总的来说,只要你的方面不仅仅是企业服务的简单“配置”,Spring团队更喜欢@AspectJ风格。

11.5 混合方面类型

完全可以使用自动代理支持,模式定义的 <aop:aspect> 方面, <aop:advisor> 声明的顾问程序以及甚至在相同配置中使用Spring 1.2样式定义的代理和拦截器来混合@AspectJ样式方面。所有这些都是使用相同的底层支持机制实现的,并且可以毫无困难地共存。

11.6 代理机制

Spring AOP使用JDK动态代理或CGLIB为给定目标对象创建代理。 (只要有选择,JDK动态代理就是首选)。

如果要代理的目标对象实现至少一个接口,则将使用JDK动态代理。目标类型实现的所有接口都将被代理。如果目标对象未实现任何接口,则将创建CGLIB代理。

如果要强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),您可以这样做。但是,有一些问题需要考虑:

  • final 方法无法建议,因为它们无法被覆盖。

  • 从Spring 3.2开始,不再需要将CGLIB添加到项目类路径中,因为CGLIB类在org.springframework下重新打包并直接包含在spring-core JAR中。这意味着基于CGLIB的代理支持“正常工作”的方式与JDK动态代理总是一样。

  • 从Spring 4.0开始,代理对象的构造函数将不再被调用两次,因为CGLIB代理实例将通过Objenesis创建。只有当您的JVM不允许构造函数绕过时,您才会看到Spring的AOP支持中的双重调用和相应的调试日志条目。

要强制使用CGLIB代理,请将 <aop:config> 元素的 proxy-target-class 属性的值设置为true:

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

要在使用@AspectJ autoproxy支持时强制CGLIB代理,请将 <aop:aspectj-autoproxy> 元素的 'proxy-target-class' 属性设置为 true

<aop:aspectj-autoproxy proxy-target-class="true"/>

多个<aop:config />部分在运行时折叠为单个统一自动代理创建器,它应用了指定的任何<aop:config />部分(通常来自不同的XML bean定义文件)的最强代理设置。这也适用于<tx:annotation-driven />和<aop:aspectj-autoproxy />元素。要明确:在<tx:annotation-driven />上使用proxy-target-class =“true”,<aop:aspectj-autoproxy />或<aop:config />元素将强制使用CGLIB代理他们

11.6.1 了解AOP代理

Spring AOP是基于代理的。在编写自己的方面或使用Spring Framework提供的任何基于Spring AOP的方面之前,掌握最后一个语句实际意味着什么的语义是非常重要的。

首先考虑一个场景,其中有一个普通的,未代理的,没有特别关注它的直接对象引用,如下面的代码片段所示。

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下所示。

aop proxy plain pojo call

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

当客户端代码具有的引用是代理时,事情会稍微改变。请考虑以下图表和代码段。

aop proxy call

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

这里要理解的关键是 Main 类的 main(..) 中的客户端代码具有对代理的引用。这意味着对该对象引用的方法调用将是代理上的调用,因此代理将能够委托给与该特定方法调用相关的所有拦截器(通知)。但是,一旦调用最终到达目标对象,在这种情况下, SimplePojo 引用,它可能对其自身进行的任何方法调用,例如 this.bar()this.foo() ,将针对此引用而不是代理进行调用。这具有重要意义。这意味着自我调用不会导致与方法调用相关的建议有机会执行。

好的,那么该怎么办呢?最好的方法(这里松散地使用最好的术语)是重构代码,以便不会发生自我调用。当然,这确实需要你做一些工作,但这是最好的,最少侵入性的方法。接下来的方法是绝对可怕的,我几乎要谨慎地指出它,因为它是如此可怕。你可以(窒息!)通过这样做完全将你的类中的逻辑绑定到Spring AOP:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

这完全将您的代码耦合到Spring AOP,它使类本身意识到它正在AOP上下文中使用,它在AOP面前飞行。在创建代理时,还需要一些额外的配置:

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.adddInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

最后,必须注意的是AspectJ没有这种自调用问题,因为它不是基于代理的AOP框架。

11.7 编程创建@AspectJ Proxies

除了使用 <aop:config><aop:aspectj-autoproxy> 在配置中声明方面之外,还可以通过编程方式创建建议目标对象的代理。有关Spring的AOP API的完整详细信息,请参阅下一章。在这里,我们希望关注使用@AspectJ方面自动创建代理的能力。

org.springframework.aop.aspectj.annotation.AspectJProxyFactory 可用于为一个或多个@AspectJ方面建议的目标对象创建代理。此类的基本用法非常简单,如下所示。有关完整信息,请参阅javadocs。

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

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

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

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

11.8 在Spring应用程序中使用AspectJ

到目前为止,我们在本章中介绍的所有内容都是纯粹的Spring AOP。在本节中,我们将讨论如何使用AspectJ编译器/编织器代替Spring AOP,或者除了Spring AOP之外,如果您的需求超出Spring AOP提供的功能。

Spring附带了一个小的AspectJ方面库,它在您的发行版中可以单独使用 spring-aspects.jar ;您需要将其添加到类路径中才能使用其中的方面。 Section 11.8.1, “Using AspectJ to dependency inject domain objects with Spring”Section 11.8.2, “Other Spring aspects for AspectJ” 讨论了这个库的内容以及如何使用它。 Section 11.8.3, “Configuring AspectJ aspects using Spring IoC” 讨论了如何依赖注入使用AspectJ编译器编织的AspectJ方面。最后, Section 11.8.4, “Load-time weaving with AspectJ in the Spring Framework” 介绍了使用AspectJ为Spring应用程序加载时编织。

11.8.1 使用AspectJ依赖注入域对象与Spring

Spring容器实例化和配置在应用程序上下文中定义的bean。在给定包含要应用的配置的bean定义的名称的情况下,还可以要求bean工厂配置预先存在的对象。 spring-aspects.jar 包含一个注释驱动的方面,利用此功能允许依赖注入任何对象。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于此类别,因为它们通常使用 new 运算符以编程方式创建,或者由于数据库查询而由ORM工具创建。

@Configurable 注释标记一个类符合Spring驱动配置的条件。在最简单的情况下,它可以用作标记注释:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

当以这种方式用作标记接口时,Spring将使用与完全限定类型名称( com.xyz.myapp.domain.Account )同名的bean定义(通常为prototype-scoped)来配置带注释类型的新实例(在本例中为 Account )。由于bean的默认名称是其类型的完全限定名称,因此声明原型定义的简便方法是省略 id 属性:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型bean定义的名称,可以直接在注释中执行此操作:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring现在将查找名为 "account" 的bean定义,并将其用作配置新 Account 实例的定义。

您还可以使用自动装配来避免必须指定专用的bean定义。要让Spring应用自动装配,请使用 @Configurable 注释的 autowire 属性:分别按类型或名称指定 @Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME 进行自动装配。作为替代方案,从Spring 2.5开始,最好指定显式的注释驱动通过在字段或方法级别使用 @Autowired@Inject 来为 @Configurable bean依赖注入(有关详细信息,请参阅 Section 7.9, “Annotation-based container configuration” )。

最后,您可以使用 dependencyCheck 属性(例如: @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) )为新创建和配置的对象中的对象引用启用Spring依赖项检查。如果此属性设置为true,则Spring将在配置后验证是否已设置所有属性(不是基元或集合)。

单独使用注释当然不会做任何事情。 spring-aspects.jar 中的 AnnotationBeanConfigurerAspect 作用于注释的存在。本质上,方面说“在从使用_682848注释的类型的新对象的初始化返回之后,根据注释的属性使用Spring配置新创建的对象”。在此上下文中,初始化是指新实例化的对象(例如,用 new 运算符实例化的对象)以及正在进行反序列化的对象(例如,通过 readResolve() )。

上段中的一个关键短语是“实质上”。对于大多数情况,“在从新对象初始化返回之后”的确切语义将很好......在此上下文中,“初始化之后”意味着将在构造对象之后注入依赖项 - 这意味着依赖项将无法在类的构造函数体中使用。如果你想在构造函数体执行之前注入依赖项,从而可以在构造函数体中使用,那么你需要在@Configurable声明中定义它,如下所示:@Configurable(preConstruction = true)你可以在AspectJ编程指南的本附录中找到有关AspectJ中各种切入点类型的语言语义的更多信息。

为此,必须使用AspectJ编织器编写带注释的类型 - 您可以使用构建时Ant或Maven任务来执行此操作(请参阅例如 AspectJ Development Environment Guide )或加载时编织(请参阅 Section 11.8.4, “Load-time weaving with AspectJ in the Spring Framework” )。 AnnotationBeanConfigurerAspect 本身需要通过Spring进行配置(以获取对用于配置新对象的bean工厂的引用)。如果您使用的是基于Java的配置,只需将 @EnableSpringConfigured 添加到任何 @Configuration 类。

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您更喜欢基于XML的配置,Spring context namespace 定义了一个方便的 context:spring-configured 元素:

<context:spring-configured/>

在配置方面之前创建的 @Configurable 对象的实例将导致向调试日志发出消息,并且不会发生对象的配置。一个示例可能是Spring配置中的bean,它在Spring初始化时创建域对象。在这种情况下,您可以使用 "depends-on" bean属性手动指定bean依赖于配置方面。

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

不要通过bean配置器方面激活@Configurable处理,除非你真的想在运行时依赖它的语义。特别是,确保不对使用容器注册为常规Spring bean的bean类使用@Configurable:否则将进行双初始化,一次通过容器,一次通过方面。

单元测试@Configurable对象

@Configurable 支持的目标之一是启用域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果AspectJ没有编写 @Configurable 类型,那么注释在单元测试期间没有影响,您只需在被测对象中设置模拟或存根属性引用并继续正常进行。如果_s82866_类型已由AspectJ编织,那么您仍然可以正常地在容器外部进行单元测试,但每次构造 @Configurable 对象时都会看到一条警告消息,表明它尚未由Spring配置。

使用多个应用程序上下文

用于实现 @Configurable 支持的 AnnotationBeanConfigurerAspect 是AspectJ单例方面。单例方面的范围与 static 成员的范围相同,也就是说每个类加载器有一个方面实例来定义类型。这意味着如果在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义 @EnableSpringConfigured bean以及在类路径上放置 spring-aspects.jar 的位置。

考虑一个典型的Spring Web应用程序配置,其中包含共享父应用程序上下文,用于定义公共业务服务和支持它们所需的一切,以及每个servlet包含一个子应用程序上下文,其中包含特定于该servlet所有这些上下文将在同一个类加载器层次结构中共存,因此 AnnotationBeanConfigurerAspect 只能包含对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义 @EnableSpringConfigured bean:这定义了您可能需要的服务注入域对象。结果是您无法使用@Configurable机制(可能不是您想要做的事情!)来配置域对象,并引用在子(特定于servlet)的上下文中定义的bean。

在同一容器中部署多个Web应用程序时,请确保每个Web应用程序使用自己的类加载器加载 spring-aspects.jar 中的类型(例如,将 spring-aspects.jar 放在 'WEB-INF/lib' 中)。如果 spring-aspects.jar 仅添加到容器范围的类路径中(因此由共享父类加载器加载),则所有Web应用程序将共享相同的方面实例,这可能不是您想要的。

11.8.2 AspectJ的其他Spring方面

@Configurable 方面外, spring-aspects.jar 还包含一个AspectJ方面,可用于为使用 @Transactional 注释注释的类型和方法驱动Spring的事务管理。这主要适用于希望在Spring容器之外使用Spring Framework的事务支持的用户。

解释 @Transactional 注释的方面是 AnnotationTransactionAspect 。使用此方面时,必须注释实现类(和/或该类中的方法),而不是该类实现的接口(如果有)。 AspectJ遵循Java的规则,即接口上的注释不会被继承。

类上的 @Transactional 注释指定了在类中执行任何公共操作的默认事务语义。

类中方法的 @Transactional 注释会覆盖类注释(如果存在)给出的默认事务语义。可以注释任何可见性的方法,包括私有方法。直接注释非公共方法是获得执行此类方法的事务划分的唯一方法。

自Spring Framework 4.2以来,spring-aspects提供了类似的方面,为标准的javax.transaction.Transactional注释提供了完全相同的功能。检查JtaAnnotationTransactionAspect以获取更多详细信息。

对于想要使用Spring配置和事务管理支持但不想(或不能)使用注释的AspectJ程序员, spring-aspects.jar 也包含 abstract 方面,您可以扩展以提供自己的切入点定义。有关更多信息,请参阅 AbstractBeanConfigurerAspectAbstractTransactionAspect 方面的来源。作为示例,以下摘录显示了如何使用与完全限定类名匹配的原型bean定义编写方面来配置域模型中定义的所有对象实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);

}

11.8.3 使用Spring IoC配置AspectJ方面

在Spring应用程序中使用AspectJ方面时,很自然希望能够使用Spring配置这些方面。 AspectJ运行时本身负责方面创建,并且通过Spring配置AspectJ创建方面的方法取决于方面使用的AspectJ实例化模型( per-xxx 子句)。

AspectJ的大多数方面都是单例方面。这些方面的配置非常简单:只需创建一个正常引用方面类型的bean定义,并包含bean属性 'factory-method="aspectOf"' 。这可以确保Spring通过向AspectJ请求它来获取方面实例,而不是尝试创建实例本身。例如:

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf">

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

非单例方面更难配置:但是可以通过创建原型bean定义并使用 spring-aspects.jar 中的 @Configurable 支持来配置方面实例(一旦它们具有由AspectJ运行时创建的bean)。

如果您想要使用AspectJ编写一些@AspectJ方面(例如,使用域模型类型的加载时编织)和其他要与Spring AOP一起使用的@AspectJ方面,并且这些方面都是使用Spring配置的,那么你需要告诉Spring AOP @AspectJ autoproxying支持配置中定义的@AspectJ方面的确切子集应该用于自动代理。您可以通过在 <aop:aspectj-autoproxy/> 声明中使用一个或多个 <include/> 元素来完成此操作。每个 <include/> 元素指定一个名称模式,只有名称与至少一个模式匹配的bean才会用于Spring AOP autoproxy配置:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

不要被<aop:aspectj-autoproxy />元素的名称误导:使用它将导致创建Spring AOP代理。这里只使用@AspectJ样式的方面声明,但不涉及AspectJ运行时。

11.8.4 在Spring Framework中使用AspectJ进行加载时编织

加载时编织(LTW)是指在将AspectJ方面加载到Java虚拟机(JVM)中时将其编织到应用程序的类文件中的过程。本节的重点是在Spring Framework的特定上下文中配置和使用LTW:本节不是对LTW的介绍。有关详细信息LTW的细节和仅使用AspectJ配置LTW(完全不涉及Spring),请参阅 LTW section of the AspectJ Development Environment Guide

Spring Framework为AspectJ LTW带来的附加 Value 在于对编织过程实现更细粒度的控制。 “Vanilla”AspectJ LTW使用Java(5)代理实现,该代理通过在启动JVM时指定VM参数来启用。因此,它是一个JVM范围的设置,在某些情况下可能很好,但通常有点过于粗糙。支持Spring的LTW使您能够在每个ClassLoader的基础上打开LTW,这显然更精细,并且在“单JVM多应用程序”环境中更有意义(例如在典型的环境中)应用服务器环境)。

此外, in certain environments ,此支持启用加载时编织,而无需对应用程序服务器的启动脚本进行任何修改,这些修改将需要添加 -javaagent:path/to/aspectjweaver.jar 或(如本节后面所述) -javaagent:path/to/org.springframework.instrument-{version}.jar (以前称为 spring-agent.jar )。开发人员只需修改构成应用程序上下文的一个或多个文件即可启用加载时编织,而不是依赖通常负责部署配置的管理员(如启动脚本)。

既然销售情况已经结束,那么让我们首先介绍使用Spring的AspectJ LTW的快速示例,然后详细介绍以下示例中介绍的元素。有关完整示例,请参阅 Petclinic sample application

第一个例子

让我们假设您是一名应用程序开发人员,负责诊断系统中某些性能问题的原因。我们要做的不是打破分析工具,而是打开一个简单的分析方面,这将使我们能够非常快速地获得一些性能指标,这样我们就可以立即将更精细的分析工具应用于该特定区域。然后。

此处提供的示例使用XML样式配置,也可以使用Java Configuration配置和使用@AspectJ。具体来说,@ EnableLoadTimeWeaving注释可以用作<context:load-time-weaver />的替代方法(详见下文)。

这是剖析方面。没有什么太花哨的,只是一个快速而肮脏的基于时间的探查器,使用@ AspectJ风格的方面声明。

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

我们还需要创建一个 META-INF/aop.xml 文件,以通知AspectJ weaver我们想要将_682911编织到我们的类中。此文件约定,即Java类路径中名为 META-INF/aop.xml 的文件(或多个文件)的存在是标准AspectJ。

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>

</aspectj>

现在到Spring特定的配置部分。我们需要配置一个 LoadTimeWeaver (后面会解释,现在就把它当作信任)。此加载时weaver是负责将一个或多个 META-INF/aop.xml 文件中的方面配置编织到应用程序的类中的基本组件。好处是它不需要很多配置,如下所示(您可以指定更多选项,但稍后会详细介绍)。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        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">

    <!-- a service object; we will be profiling its methods -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>

    <!-- this switches on the load-time weaving -->
    <context:load-time-weaver/>
</beans>

现在所有必需的工件都已到位 - 方面, META-INF/aop.xml 文件和Spring配置 - 让我们使用 main(..) 方法创建一个简单的驱动程序类来演示LTW的运行情况。

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService
            = (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

还有最后一件事要做。本节的介绍确实表明可以在Spring的基础上选择性地开启LTW,这是事实。但是,仅为此示例,我们将使用Java代理(随Spring提供)来打开LTW。这是我们用来运行上述 Main 类的命令行:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent 是用于指定和启用 agents to instrument programs running on the JVM 的标志。 Spring Framework附带了一个代理 InstrumentationSavingAgent ,它被打包在_682926中,它作为上例中 -javaagent 参数的值提供。

执行 Main 程序的输出将如下所示。 (我已经在 calculateEntitlement() 实现中引入了 Thread.sleep(..) 语句,以便探查器实际捕获0毫秒以外的东西 - 01234 毫秒不是AOP引入的开销:))

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由于LTW是使用成熟的AspectJ实现的,因此我们不仅限于为Spring bean提供建议; Main 程序的以下细微变化将产生相同的结果。

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {

        new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
            new StubEntitlementCalculationService();

        // the profiling aspect will be 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

注意在上面的程序中我们只是简单地引导Spring容器,然后在Spring的上下文之外创建一个 StubEntitlementCalculationService 的新实例......分析建议仍然可以编入。

不可否认,这个例子很简单......但是在上面的例子中已经介绍了Spring中LTW支持的基础知识,本节的其余部分将详细解释每个配置和用法背后的“原因”。

本示例中使用的ProfilingAspect可能是基本的,但它非常有用。这是开发人员在开发期间(当然)可以使用的开发时间方面的一个很好的例子,然后很容易从部署到UAT或 生产环境 中的应用程序的构建中排除。

方面

您在LTW中使用的方面必须是AspectJ方面。它们可以用AspectJ语言本身编写,也可以用@ AspectJ风格编写方面。这意味着您的方面都是有效的AspectJ和Spring AOP方面。此外,编译的方面类需要在类路径上可用。

'META-INF / aop.xml'

AspectJ LTW基础结构使用一个或多个 META-INF/aop.xml 文件进行配置,这些文件位于Java类路径上(直接或更常见于jar文件中)。

该文件的结构和内容在主要的AspectJ参考文档中有详细介绍,感兴趣的读者是 referred to that resource 。 (我很欣赏这一部分是简短的,但 aop.xml 文件是100%AspectJ - 没有适用于它的特定于Spring的信息或语义,因此没有额外的值可以作为结果贡献),所以而不是重新审视AspectJ开发人员所写的相当令人满意的部分,我只是指导你那里。)

必需的库(JARS)

您至少需要以下库才能使用Spring Framework对AspectJ LTW的支持:

  • spring-aop.jar (版本2.5或更高版本,加上所有必需的依赖项)

  • aspectjweaver.jar (版本1.6.8或更高版本)

如果您使用 Spring-provided agent to enable instrumentation ,您还需要:

  • spring-instrument.jar

Spring配置

Spring的LTW支持的关键组件是 LoadTimeWeaver 接口(在 org.springframework.instrument.classloading 包中),以及随Spring发行版一起提供的众多实现。 LoadTimeWeaver 负责在运行时将一个或多个 java.lang.instrument.ClassFileTransformers 添加到 ClassLoader ,这为各种有趣的应用程序打开了大门,其中一个恰好是方面的LTW。

如果您不熟悉运行时类文件转换的想法,建议您在继续之前阅读java.lang.instrument包的javadoc API文档。这不是一项繁重的工作,因为那里有相当令人讨厌的珍贵文档......关键接口和类至少会在您阅读本章时作为参考。

为特定的 ApplicationContext 配置 LoadTimeWeaver 可以像添加一行一样简单。 (请注意,您几乎肯定需要使用 ApplicationContext 作为Spring容器 - 通常 BeanFactory 是不够的,因为LTW支持使用了 BeanFactoryPostProcessors 。)

要启用Spring Framework的LTW支持,您需要配置 LoadTimeWeaver ,这通常使用 @EnableLoadTimeWeaving 注释完成。

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {

}

或者,如果您更喜欢基于XML的配置,请使用 <context:load-time-weaver/> 元素。请注意,该元素在 context 名称空间中定义。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        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">

    <context:load-time-weaver/>

</beans>

上面的配置将自动为您定义和注册一些LTW特定的基础架构bean,例如 LoadTimeWeaverAspectJWeavingEnabler 。默认 LoadTimeWeaverDefaultContextLoadTimeWeaver 类,它尝试修饰自动检测到的 LoadTimeWeaver :将被“自动检测”的 LoadTimeWeaver 的确切类型取决于您的运行时环境(在下表中进行了总结)。

Table 11.1. DefaultContextLoadTimeWeaver LoadTimeWeavers

运行时环境LoadTimeWeaver 实施
在Oracle中运行 WebLogicWebLogicLoadTimeWeaver
在Oracle中运行 GlassFishGlassFishLoadTimeWeaver
Apache TomcatTomcatLoadTimeWeaver中运行
在Red Hat中运行 JBoss ASWildFlyJBossLoadTimeWeaver
在IBM的 WebSphereWebSphereLoadTimeWeaver中运行
JVM以Spring InstrumentationSavingAgent 开头(java -javaagent:path / to / spring-instrument.jar)InstrumentationLoadTimeWeaver
回退,期望底层的ClassLoader遵循公共约定(例如适用于 TomcatInstrumentableClassLoaderResinReflectiveLoadTimeWeaver

请注意,这些只是在使用 DefaultContextLoadTimeWeaver 时自动检测的 LoadTimeWeavers :当然可以准确指定您希望使用的 LoadTimeWeaver 实现。

要使用Java配置指定特定的 LoadTimeWeaver ,请实现 LoadTimeWeavingConfigurer 接口并覆盖 getLoadTimeWeaver() 方法:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

如果使用基于XML的配置,则可以将完全限定的类名指定为 <context:load-time-weaver/> 元素上的 weaver-class 属性的值:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        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">

    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>

稍后可以使用众所周知的名称 loadTimeWeaver 从Spring容器中检索由配置定义和注册的 LoadTimeWeaver 。请记住, LoadTimeWeaver 仅作为Spring的LTW基础架构添加一个或多个 ClassFileTransformers 的机制而存在。执行LTW的实际 ClassFileTransformerClassPreProcessorAgentAdapter (来自 org.aspectj.weaver.loadtime 包)类。查看课程级别 ClassPreProcessorAgentAdapter 类的javadocs有更多细节,因为编织实际如何实现的细节超出了本节的范围。

剩下要讨论的配置有一个最终属性: aspectjWeaving 属性(如果使用XML,则为 aspectj-weaving )。这是一个控制LTW是否启用的简单属性;它是如此简单。它接受以下总结的三个可能值之一,如果该属性不存在,则默认值为 autodetect

Table 11.2. AspectJ weaving attribute values

注释值XML值说明
ENABLEDonAspectJ编织已打开,并且各个方面将在加载时编辑。
DISABLEDoffLTW关闭......在加载时不会编织任何方面。
AUTODETECTautodetect如果Spring LTW基础设施可以找到至少一个 META-INF/aop.xml 文件,则AspectJ编织开启,否则关闭。这是默认值。

特定于环境的配置

最后一节包含在应用程序服务器和Web容器等环境中使用Spring的LTW支持时所需的任何其他设置和配置。

Tomcat

从历史上看, Apache Tomcat 的默认类加载器不支持类转换,这就是Spring提供满足此需求的增强实现的原因。命名为 TomcatInstrumentableClassLoader ,加载程序适用于Tomcat 6.0及更高版本。

不要再在Tomcat 8.0及更高版本上定义TomcatInstrumentableClassLoader。相反,让Spring通过TomcatLoadTimeWeaver策略自动使用Tomcat的新本机InstrumentableClassLoader工具。

如果您仍然需要使用 TomcatInstrumentableClassLoader ,则可以为每个Web应用程序单独注册,如下所示:

  • org.springframework.instrument.tomcat.jar 复制到$ CATALINA_HOME / lib中,其中$ CATALINA_HOME表示Tomcat安装的根目录)

  • 通过编辑Web应用程序上下文文件,指示Tomcat使用自定义类加载器(而不是默认值):

<Context path="/myWebApp" docBase="/my/webApp/location">
    <Loader
        loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>

Apache Tomcat(6.0)支持多个上下文位置:

  • 服务器配置文件 - $ CATALINA_HOME / conf / server.xml

  • 默认上下文配置 - $ CATALINA_HOME / conf / context.xml - 影响所有已部署的Web应用程序

  • 每个Web应用程序配置,可以在服务器端部署在$ CATALINA_HOME / conf / [enginename] / [hostname] / [webapp] -context.xml,也可以嵌入在META-INF /的web-app存档中的context.xml

为了提高效率,建议使用嵌入式per-web-app配置样式,因为它只会影响使用自定义类加载器的应用程序,并且不需要对服务器配置进行任何更改。有关可用上下文位置的更多详细信息,请参阅Tomcat 6.0.x documentation

或者,考虑使用Spring提供的通用VM代理,在Tomcat的启动脚本中指定(参见上文)。这将使所有已部署的Web应用程序都可以使用检测,无论它们恰好运行在哪个ClassLoader上。

WebLogic,WebSphere,Resin,GlassFish,JBoss

最新版本的WebLogic Server(版本10及更高版本),IBM WebSphere Application Server(版本7及更高版本),Resin(3.1及更高版本)和JBoss(6.x或更高版本)提供了一个能够进行本地检测的ClassLoader。 Spring的原生LTW利用这种ClassLoader来实现AspectJ编织。您可以通过简单地激活加载时编织来启用LTW,如前所述。具体来说,您无需修改启动脚本即可添加 -javaagent:path/to/spring-instrument.jar

请注意,支持GlassFish检测的ClassLoader仅在其EAR环境中可用。对于GlassFish Web应用程序,请按照上面概述的Tomcat设置说明进行操作。

请注意,在JBoss 6.x上,需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是向您的工件添加一个名为 WEB-INF/jboss-scanning.xml 的文件,其中包含以下内容:

<scanning xmlns="urn:jboss:scanning:1.0"/>
通用Java应用程序

如果在不支持或不支持现有 LoadTimeWeaver 实现的环境中需要类检测,则JDK代理可以是唯一的解决方案。对于这种情况,Spring提供了 InstrumentationLoadTimeWeaver ,它需要一个特定于Spring的(但非常通用的)VM代理 org.springframework.instrument-{version}.jar (以前称为 spring-agent.jar )。

要使用它,必须通过提供以下JVM选项来启动具有Spring代理的虚拟机:

-javaagent:/path/to/org.springframework.instrument-{version}.jar

请注意,这需要修改VM启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的操作策略)。此外,JDK代理将检测整个VM,这可能证明是昂贵的。

出于性能原因,建议仅在目标环境(例如 Jetty )没有(或不具有)时使用此配置支持)专用的LTW。

11.9 更多资源

有关AspectJ的更多信息,请参见 AspectJ website

由Adrian Colyer等人撰写的Eclipse AspectJ一书。人。 (Addison-Wesley,2005)为AspectJ语言提供了全面的介绍和参考。

强烈推荐Ramnivas Laddad(Manning,2009)出版的“AspectJ in Action”第二版。本书的重点是AspectJ,但很多一般的AOP主题都在探索中(在某种程度上)。

Updated at: 5 months ago
10.5. 示例中使用的类Table of content12. Spring AOP API
Comment
You are not logged in.

There are no comments.