【翻译 Spring 5.0.4.RELEASE】5. Aspect Oriented Programming with Spring

5. Aspect Oriented Programming with Spring

5.1. Introduction

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

AOP框架是Spring的关键组件之一。 虽然Spring IoC容器不依赖AOP,也就是说如果你不想使用AOP,AOP可以补充Spring IoC提供的非常强大的中间件解决方案。

Spring 2.0+ AOP
Spring 2.0引入了一种更简单,更强大的使用基于模式的方法或@AspectJ注释风格编写自定义方面的方法。 这两种风格都提供完全类型化的建议和AspectJ切入点语言的使用,同时仍然使用Spring AOP进行编织。

本章将讨论基于Spring 2.0+模式和@AspectJ的AOP支持。 通常在Spring 1.2应用程序中公开的较低级别的AOP支持将在下一章中讨论。

AOP在Spring框架中用于……

…提供声明式企业服务,特别是作为EJB声明式服务的替代品。 最重要的这种服务是声明式事务管理。

…允许用户实现自定义方面,补充他们与AOP一起使用OOP。

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

5.1.1. AOP concepts

让我们从定义一些中心的AOP概念和术语开始。 这些术语不是Spring特有的……不幸的是,AOP术语并不是特别直观, 然而,如果Spring使用自己的术语,那将更加令人困惑。

  • Aspect:跨越多个班级的问题的模块化。事务管理是企业Java应用程序中横切关注的一个很好的例子。在Spring AOP中,方面是使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ样式)注释的常规类来实现的。

  • Join point:程序执行期间的一个点,例如执行方法或处理异常。在Spring AOP中,连接点总是表示方法的执行。

  • Advice:某个特定连接点的某个方面采取的行动。不同类型的建议包括“周围”,“之前”和“之后”建议。 (建议类型将在下面讨论。)许多AOP框架(包括Spring)将建议建模为拦截器,在连接点周围维护一系列拦截器。

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

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

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

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

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

Types of advice:

  • Before advice:在连接点之前执行的建议,但无法阻止执行流程继续到连接点(除非它引发异常)。

  • After returning advice:在连接点正常完成后要执行的通知:例如,如果方法返回而不抛出异常。

  • After throwing advice:如果方法通过抛出异常退出,则要执行的建议。

  • After (finally) advice:无论连接点退出的方式(正常或异常退回),要执行的建议。

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

围绕建议是最普遍的建议。由于Spring AOP与AspectJ一样提供了全面的通知​​类型,因此我们建议您使用能够实现所需行为的功能最低的通知类型。例如,如果只需要使用方法的返回值来更新缓存,则最好是实现返回后的建议而不是周围的建议,尽管周围的建议可以完成同样的事情。使用最具体的建议类型提供了一个更简单的编程模型,但出错的可能性较小。例如,您不需要在用于around通知的JoinPoint上调用proceed()方法,因此无法调用它。

在Spring 2.0中,所有通知参数都是静态类型的,因此您可以使用适当类型的通知参数(例如来自方法执行的返回值的类型)而不是对象数组。

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

5.1.2. Spring AOP capabilities and goals

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框架的AOP功能通常与Spring IoC容器一起使用。方面使用正常的bean定义语法进行配置(尽管这允许强大的“自动代理”功能):这与其他AOP实现有着至关重要的区别。有些事情你不能用Spring AOP轻松或高效地完成,比如建议非常细粒度的对象(比如域对象):在这种情况下,AspectJ是最好的选择。但是,我们的经验是,Spring AOP为适用于AOP的企业Java应用程序中的大多数问题提供了极好的解决方案。

Spring AOP永远不会与AspectJ竞争提供全面的AOP解决方案。我们认为像Spring AOP这样的基于代理的框架和像AspectJ这样的全面框架都是有价值的,而且它们是互补的,而不是竞争。 Spring将Spring AOP和IoC与AspectJ无缝集成,以便在基于Spring的一致应用程序体系结构中满足AOP的所有用途。此集成不影响Spring AOP API或AOP Alliance API:Spring AOP仍保持向后兼容。有关Spring AOP API的讨论,请参阅以下章节。

Spring框架的核心原则之一是非侵入性;这是一个想法,您不应该被迫将框架特定的类和接口引入到您的业务/域模型中。但是,在某些地方,Spring框架可以让你选择在你的代码库中引入特定于Spring Framework的依赖关系:给你这样的选项的理由是因为在某些场景下,阅读或编写一些特定的功能以这种方式。 Spring框架(几乎)总是为您提供选择:您可以自由地做出明智的决定,选择最适合您的特定用例或场景的选项。

与本章相关的一个选择就是AOP框架(以及AOP风格)的选择。您可以选择AspectJ和/或Spring AOP,并且您还可以选择@AspectJ批注样式方法或Spring XML配置样式方法。本章选择首先引入@AspectJ风格方法的事实不应视为Spring团队赞成Spring XML配置风格的@AspectJ注释风格方法。

请参阅选择使用哪种AOP声明样式来更全面地讨论每种样式的原因和原因

5.1.3. AOP Proxies

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

Spring AOP也可以使用CGLIB代理。 这对代理类而不是接口是必需的。 如果业务对象不实现接口,则缺省使用CGLIB。 由于编程接口而不是类是一种很好的做法, 业务类通常会实现一个或多个业务接口。 在需要通知未在接口中声明的方法的情况下,或者需要将代理对象作为具体类型传递给方法的情况下(希望很少),可以强制使用CGLIB。

掌握Spring AOP是基于代理的事实是很重要的。 请参阅了解AOP代理以彻底检查此实现细节的实际含义。

5.2. @AspectJ support

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

使用AspectJ编译器和编织器可以使用完整的AspectJ语言,并在Spring应用程序中使用AspectJ进行了讨论。

5.2.1. Enabling @AspectJ Support

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

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

Enabling @AspectJ Support with Java configuration

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

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

Enabling @AspectJ Support with XML configuration

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

<aop:aspectj-autoproxy/>

这假定您正在使用模式支持,如基于XML模式的配置中所述。 有关如何在aop名称空间中导入标签的信息,请参阅AOP架构。

5.2.2. Declaring an aspect

在启用@AspectJ支持的情况下,在您的应用程序上下文中定义的任何具有@AspectJ方面的类(具有@Aspect注释)的bean将被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注释将其标记为一个方面,因此将其从自动代理中排除。

5.2.3. Declaring a pointcut

回想一下,切入点决定了感兴趣的连接点,从而使我们能够控制何时执行建议。 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编程指南”(以及用于扩展的AspectJ 5开发人员笔记本)或AspectJ的其中一本书,例如Colyer等人的“Eclipse AspectJ”。人。 或Ramnivas Laddad的“AspectJ in Action”。

Supported Pointcut Designators

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

其他切入点类型
完整的AspectJ切入点语言支持Spring中不支持的其他切入点指示符。 这些是:调用,获取,设置,预初始化,静态初始化,初始化,处理程序,adviceexecution,withincode,cflow,cflowbelow,if,@ this和@withincode。 在由Spring AOP解释的切入点表达式中使用这些切入点指示符将导致抛出IllegalArgumentException。

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

  • execution - 用于匹配方法执行连接点,这是您在使用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本身具有基于类型的语义,并且在执行连接点上,this和target都指向同一个对象 - 执行该方法的对象。 Spring AOP是一个基于代理的系统,它区分代理对象本身(绑定到此)和代理(绑定到目标)后面的目标对象。

由于Spring的AOP框架的基于代理的本质,目标对象内的调用根本没有被拦截。对于JDK代理,只有代理上的公共接口方法调用才能被拦截。使用CGLIB时,代理上的公共和受保护的方法调用将被拦截,如果需要的话,甚至包可见方法也会被拦截。但是,通过代理的常见交互应始终通过公共签名来设计。

请注意,切入点定义通常与任何截取的方法相匹配。如果一个切入点严格意味着仅仅是公开的,即使在通过代理进行潜在的非公共交互的CGLIB代理场景中,也需要相应地定义切入点。

如果您的拦截需求包括方法调用,甚至包含目标类中的构造函数,请考虑使用Spring驱动的本机AspectJ编织,而不是Spring的基于代理的AOP框架。这构成了具有不同特征的AOP使用的不同模式,所以在作出决定之前一定要先熟悉编织。

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

bean(idOrNameOfBean)

idOrNameOfBean标记可以是任何Spring bean的名称:提供了使用*字符的有限通配符支持,因此如果您为Spring bean建立了一些命名约定,则可以很容易地编写一个bean PCD表达式来将其选出。 与其他切入点指示符的情况一样,bean PCD可以&&&’ed,||’ed,和! (否定)。

请注意,bean PCD仅在Spring AOP中受支持 - 而不是在本机AspectJ编织中。 它是AspectJ定义的标准PCD的Spring特定扩展,因此不适用于@Aspect模型中声明的方面。

bean PCD在实例级别上运行(基于Spring bean名称概念构建),而不是仅在类型级别(这是基于编织的AOP所限制的)。 基于实例的切入点指示符是Spring基于代理的AOP框架的一个特殊功能,它与Spring bean工厂紧密集成,通过名称识别特定的bean是很自然和直接的。

Combining pointcut expressions

切入点表达式可以使用’&&’,’||’ 和’!’。 也可以通过名称来引用切入点表达式。 以下示例显示三个切入点表达式:anyPublicOperation(如果方法执行连接点表示任何公共方法的执行,则匹配); inTrading(与交易模块中的方法执行相匹配时)和tradingOperation(如果方法执行代表交易模块中的任何公共方法,则匹配)。

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

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

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

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

Sharing common pointcut definitions

在使用企业应用程序时,您通常希望从几个方面参考应用程序的模块和特定操作集。 我们建议定义一个“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支持中讨论<aop:config><aop:advisor>元素。 事务管理中讨论了事务元素。

Examples

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

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

除了返回类型模式(上面代码片段中的ret-type-pattern),名称模式和参数模式以外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么,才能匹配连接点。大多数情况下,您将使用作为返回类型模式,它与任何返回类型匹配。只有当方法返回给定类型时,完全限定类型名称才会匹配。名称模式与方法名称匹配。您可以将通配符用作全部或部分名称模式。如果指定一个声明类型模式,则包含尾部。将其加入名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量的参数(零个或多个)。模式()匹配任何类型的一个参数的方法,(,String)匹配一个采用两个参数的方法,第一个可以是任何类型,第二个必须是字符串。有关更多信息,请参阅“AspectJ编程指南”的“语言语义”部分。

常见切入点表达式的一些示例如下所示。

  • 执行任何公共方法:
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’更常用于绑定形式: - 请参阅以下关于如何在建议主体中使方法参数可用的建议。

请注意,此示例中给出的切入点与执行不同(* *(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)
  • 对于名称与通配符表达式匹配的Spring bean,任何连接点(仅在Spring AOP中执行的方法)* Service:
bean(*Service)

Writing good pointcuts

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

但是,AspectJ只能使用它所说的内容,为了获得最佳的匹配性能,您应该考虑他们正在尝试实现的目标,并尽可能缩小匹配的搜索空间。现有的指定者自然属于三个群体之一:激励,范围界定和背景:

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

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

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

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

5.2.4. Declaring advice

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

Before advice

在使用@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() {
        // ...
    }

}

After returning advice

在匹配的方法执行正常返回时返回通知后运行。 它使用@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) {
        // ...
    }

}

返回属性中使用的名称必须与通知方法中参数的名称相对应。 当方法执行返回时,返回值将作为相应的参数值传递给通知方法。 返回子句还将匹配限制为仅返回指定类型的值的方法执行(本例中为Object,它将与任何返回值相匹配)。

请注意,在使用退货后的建议时,不可能返回完全不同的参考。

After throwing advice

抛出通知后,当匹配的方法执行通过抛出异常退出时运行。 它使用@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方法。 throwing子句也会将匹配限制为仅引发抛出指定类型(本例中为DataAccessException)的异常的方法执行。

After (finally) advice

在(最后)建议运行后,匹配的方法执行退出。 它使用@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 advice

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

周围的建议是使用@Around注释声明的。 通知方法的第一个参数必须是ProceedingJoinPoint类型。 在通知的主体中,ProceedingJoinPoint调用proceed()会导致底层方法执行。 proceed方法也可以被称为传递Object [] - 数组中的值将在继续时用作方法执行的参数。

使用Object []调用时的继续行为与AspectJ编译器编译的around处理的行为稍有不同。对于使用传统AspectJ语言编写的周围建议,传递给进行处理的参数数量必须与传递给around通知的参数数量(而不是基础连接点采用的参数数量)匹配,并且传递的值将继续给定的参数位置取代了值绑定到的实体连接点的原始值(如果现在没有意义,请不要担心!)。 Spring采用的方法更简单,并且与其基于代理的只执行语义更匹配。如果您正在编译针对Spring编写的@AspectJ方面,并使用AspectJ编译器和编织器进行参数处理,则只需要了解这种差异。有一种方法可以编写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()。 请注意,继续可能会被调用一次,多次或根本不在周围建议的正文中,所有这些都是非常合法的。

Advice parameters

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

Access to the current JoinPoint

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

Passing parameters to advice

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

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

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

另一种编写这种方法的方式是声明一个切入点,该切入点在与连接点相匹配时“提供”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();
    // ...
}

Advice parameters and generics

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

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

您可以将方法类型的截取限制为某些参数类型,方法是只需将参数类型输入到要截取该方法的参数类型即可:

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

这个作品非常明显,正如我们上面已经讨论的那样。 但值得指出的是,这对于通用集合不起作用。 所以你不能像这样定义一个切入点:

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

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

Determining argument names

通知调用中的参数绑定依赖于切入点表达式中使用的名称与(通知和切入点)方法签名中声明的参数名称匹配。 参数名称不能通过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
}

如果第一个参数属于JoinPoint,ProceedingJoinPoint或JoinPoint.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
}

给予JoinPoint,ProceedingJoinPoint和JoinPoint.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。

Proceeding with arguments

我们之前说过,我们将描述如何使用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});
}

在很多情况下,你会做这个绑定无论如何(如在上面的例子)。

Advice ordering

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

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

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

5.2.5. Introductions

引入(在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建议中,服务bean可以直接用作UsageTracked接口的实现。 如果以编程方式访问bean,您可以编写以下代码:

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

5.2.6. Aspect instantiation models

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

默认情况下,应用程序上下文中将存在每个方面的单个实例。 AspectJ将这称为单例实例化模型。 可以使用不同的生命周期来定义方面: - Spring支持AspectJ的perthis和pertarget实例化模型(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”的唯一对象)创建一个aspect实例。 方面实例首次在服务对象上调用方法时创建。 当服务对象超出范围时,该方面超出范围。 在创建aspect实例之前,其中的任何通知都不会执行。 一旦创建了aspect实例,其中声明的通知将在匹配的连接点上执行,但只有当服务对象是与此方面相关联的那个时才会执行。 有关每个子句的更多信息,请参阅AspectJ编程指南。

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

5.2.7. Example

现在你已经看到了所有组成部分是如何工作的,让我们把它们放在一起做一些有用的事情!

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

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

@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接口,因此我们可以将方面的优先级设置为高于交易建议(我们希望每次重试时都有新的交易)。 maxRetries和订单属性都将由Spring配置。 主要的行动发生在doConcurrentOperation围绕建议。 请注意,目前我们正在将重试逻辑应用于所有businessService()。 我们尝试继续,如果我们因PessimisticLockingFailureException而失败,我们只需再试一次,除非我们已经用尽了所有的重试尝试。

相应的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>

为了改进该方面以便它只重试幂等运算,我们可以定义一个幂等注释:

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

并使用注释来注释服务操作的实现。 只改变aspect等幂操作只需要改进切入点表达式,以便只有@Idempotent操作匹配:

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

5.3. Schema-based AOP support

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

要使用本节中描述的aop命名空间标记,您需要按照基于XML模式的配置中所述导入spring-aop模式。 有关如何在aop名称空间中导入标签的信息,请参阅AOP架构。

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

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

5.3.1. Declaring an aspect

使用模式支持,一个方面就是在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一样配置和依赖注入。

5.3.2. Declaring a pointcut

一个已命名的切入点可以在<aop:config>元素中声明,从而使切入点定义可以在多个方面和顾问之间共享。

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

<aop:config>

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

</aop:config>

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

<aop:config>

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

</aop:config>

假设您具有共享常用切入点定义中所述的SystemArchitecture方面。

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

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

当结合pointcut子表达式时,&&在XML文档中很难使用,所以关键字和,或者和不能用于&&,||和! 分别。 例如,以前的切入点可能会写得更好:

<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样式提供的更有限。

5.3.3. Declaring advice

@AspectJ风格支持相同的五种建议类型,它们具有完全相同的语义。

Before advice

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

<aop:aspect id="beforeExample" ref="aBean">
    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>
    ...
</aop:aspect>

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

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

正如我们在讨论@AspectJ风格时指出的那样,使用命名切入点可以显着提高代码的可读性。

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

After returning advice

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

<aop:aspect id="afterReturningExample" ref="aBean">
    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>
    ...
</aop:aspect>

就像@AspectJ风格一样,可以在通知主体中获得返回值。 使用返回属性来指定返回值应传递给的参数的名称:

<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) {...

After throwing advice

抛出通知后,通过抛出异常退出匹配的方法执行后执行。 它在<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 (finally) advice

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

<aop:aspect id="afterFinallyExample" ref="aBean">
    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>
    ...
</aop:aspect>

Around advice

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

周围的建议是使用aop:around元素声明的。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知的主体中,在ProceedingJoinPoint上调用proceed()会导致基础方法执行。 proceed方法也可以调用Object []中的传递 - 数组中的值将在继续时用作方法执行的参数。有关调用的注意事项,请参阅四周建议继续Object []。

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

Advice parameters

基于模式的声明风格支持完全类型化的建议,就像@AspectJ支持所描述的一样 - 通过名称与建议方法参数匹配的切入点参数。 详细信息请参阅咨询参数。 如果您希望为通知方法显式指定参数名称(不依赖于之前描述的检测策略),则使用通知元素的arg-names属性完成此操作,该属性与“argNames”属性的处理方式相同 在确定参数名称中描述的通知注释中。 例如:

<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(..)方法接受许多强类型参数,其中第一个恰好是用于继续进行方法调用的连接点:此参数的存在表示配置文件( ..)被用作周围的建议:

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)

Advice ordering

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

5.3.4. Introductions

引入(在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();
}

要实现的接口由实现接口属性确定。 类型匹配属性的值是AspectJ类型模式: - 匹配类型的任何bean将实现UsageTracked接口。 请注意,在上述示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。 如果以编程方式访问bean,您可以编写以下代码:

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

5.3.5. Aspect instantiation models

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

5.3.6. Advisors

“顾问”的概念是从Spring定义的AOP支持中提出的,在AspectJ中没有直接的等价物。 顾问就像一个小型的独立的方面,有一个单一的建议。 建议本身由一个bean表示,并且必须实现Spring中Advice类型中描述的一个建议接口。 顾问可以利用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属性来内联定义切入点表达式。

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

5.3.7. Example

让我们来看看如何使用模式支持重新编写来自Example的并发锁定失败重试示例。

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

因为我们想重试操作,所以我们需要使用周围的建议,以便我们可以多次调用进行操作。 以下是基本方面实现的外观(它只是使用模式支持的常规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接口,因此我们可以将方面的优先级设置为高于交易建议(我们希望每次重试时都有新的交易)。 maxRetries和订单属性都将由Spring配置。 主要操作发生在doConcurrentOperation周围的建议方法中。 我们尝试继续,如果我们因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>

请注意,目前我们假设所有的商业服务都是幂等的。 如果情况并非如此,我们可以通过引入幂等注释来改进方面,使其只重试真正的幂等操作:

@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)"/>

5.4. Choosing which AOP declaration style to use

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

5.4.1. Spring AOP or full AspectJ?

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

使用AspectJ时,您可以选择AspectJ语言语法(也称为“代码样式”)或@AspectJ注释样式。很显然,如果你不使用Java 5+,那么你已经选择了…使用代码风格。如果方面在您的设计中扮演重要角色,并且您可以使用针对Eclipse的AspectJ开发工具(AJDT)插件,那么AspectJ语言语法是首选选项:它更干净更简单,因为语言是专门为写方面。如果您没有使用Eclipse,或者只有少数几个方面在应用程序中不起主要作用,那么您可能需要考虑使用@AspectJ风格,并在IDE中使用常规的Java编译,并添加一个方面编织阶段到您的构建脚本。

5.4.2. @AspectJ or XML for Spring AOP?

如果您选择使用Spring AOP,那么您可以选择@AspectJ或XML样式。有各种折衷考虑。

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

XML样式有两个缺点。首先,它并没有完全包含它在单个地方所要求的实施。 DRY原则规定,对系统内的任何知识应该有一个单一的,明确的,权威的表示。在使用XML风格时,关于如何实现需求的知识将在支持Bean类的声明和配置文件中的XML之间进行分割。当使用@AspectJ风格时,有一个模块 - 方面 - 封装了这些信息。其次,XML风格比@AspectJ风格稍微有点局限:只支持“singleton”方面的实例化模型,并且不可能组合使用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风格。

5.5. Mixing aspect types

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

5.6. Proxying mechanisms

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

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

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

  • 最终的方法不能被建议,因为它们不能被覆盖。

  • 从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自动代理支持时强制执行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代理的使用 其中。

5.6.1. Understanding AOP proxies

Spring AOP是基于代理的。 在您编写自己的方面或使用Spring框架提供的任何基于Spring AOP的方面之前,掌握最后声明实际意义的语义至关重要。

首先考虑一下你有一个普通的,非代理的,没什么特别的关于它,直接的对象引用的场景,如下面的代码片段所示。

public class SimplePojo implements Pojo {

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

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

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

这里写图片描述

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

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

当客户端代码的引用是代理时,情况会稍有改变。 考虑下面的图表和代码片段。
这里写图片描述

public class Main {

    public static void main(String[] args) {

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

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

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

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

好的,那么该怎么办?最好的方法(在这里松散地使用术语best)是重构你的代码,使得自调用不会发生。当然,这确实需要你做一些工作,但它是最好的,侵入性最小的方法。下一个方法是非常可怕的,我几乎不愿意指出它,因为它太可怕了。你可以(呛!)通过这样做,将你的类中的逻辑完全绑定到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上下文面向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框架。

5.7. Programmatic creation of @AspectJ Proxies

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

类org.springframework.aop.aspectj.annotation.AspectJProxyFactory可用于为一个或多个@AspectJ方面建议的目标对象创建代理。 这个类的基本用法非常简单,如下所示。 查看javadoc获取完整信息。

// 创建一个可以为给定目标对象生成代理的工厂
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 添加一个方面,该类必须是@AspectJ方面
//  您可以根据需要多次调用此方面的不同方面
factory.addAspect(SecurityManager.class);

//您还可以添加现有的方面实例,提供的对象的类型必须是@AspectJ方面
factory.addAspect(usageTracker);

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

5.8. Using AspectJ with Spring applications

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

Spring提供了一个小AspectJ方面库,它可以在你的发行版中独立使用,如spring-aspects.jar; 您需要将其添加到您的类路径中以便使用它中的方面。 使用AspectJ向Spring依赖注入域对象,以及AspectJ的其他Spring方面讨论该库的内容以及如何使用它。 使用Spring IoC配置AspectJ方面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ方面。 最后,在Spring框架中使用AspectJ加载时织入提供了使用AspectJ的Spring应用程序加载时织入的介绍。

5.8.1. Using AspectJ to dependency inject domain objects with Spring

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

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

package com.xyz.myapp.domain;

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

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

当以这种方式用作标记接口时,Spring将使用与完全限定类型名称(com.xyz)相同名称的bean定义(通常是原型范围的)配置注释类型的新实例(在这种情况下为Account).myapp.domain.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批注的自动装配属性:分别指定@Configurable(autowire = Autowire.BY_TYPE)或@Configurable(autowire = Autowire.BY_NAME),以便按类型或名称分别进行自动装配。 Spring 2.5最好在字段或方法级别使用@Autowired或@Inject为您的@Configurable bean指定显式注解驱动的依赖注入(有关更多详细信息,请参阅基于注释的容器配置)。

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

当然,使用注释当然不会做任何事情。 Spring-aspects.jar中的AnnotationBeanConfigurerAspect作用于注释的存在。实质上,这个方面说:“从用@Configurable注解的类型的新对象的初始化返回后,根据注解的属性使用Spring配置新创建的对象”。在这种情况下,初始化是指新实例化的对象(例如,用新运算符实例化的对象)以及正在经历反序列化的Serializable对象(例如,通过readResolve())。

上段中的关键词之一是“本质上”。 在大多数情况下,’从新对象初始化返回后’的确切语义将会很好……在这种情况下,’初始化后’意味着依赖项将在对象构建后注入 - 这意味着 依赖关系将不可用于该类的构造函数体中。 如果你想在构造函数体执行之前注入依赖关系,并且可以在构造函数的主体中使用,那么你需要在@Configurable声明中定义它,如下所示:

@Configurable(preConstruction=true)

您可以在AspectJ编程指南的附录中找到关于AspectJ中各种切入点类型的语言语义的更多信息。

为此,注释类型必须与AspectJ编织器交织在一起 - 您可以使用构建时的Ant或Maven任务来执行此操作(请参阅“AspectJ开发环境指南”)或加载时织入(请参阅加载时间 在Spring框架中使用AspectJ编织)。 AnnotationBeanConfigurerAspect本身需要由Spring进行配置(为了获得对用来配置新对象的bean工厂的引用)。 如果您使用基于Java的配置,只需将@EnableSpringConfigured添加到任何@Configuration类。

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您更喜欢基于XML的配置,Spring上下文命名空间定义了一个方便的上下文: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处理。 特别是,确保你没有在bean类上使用@Configurable,这些类被注册为容器的普通Spring bean:你将得到双重初始化,否则,一旦通过容器,一次通过方面。

Unit testing @Configurable objects

@Configurable支持的目标之一是支持域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。 如果@Configurable类型不是由AspectJ编织的,那么注释在单元测试中没有任何影响,您可以简单地在被测对象中设置模拟或存根属性引用,并照常进行。 如果@Configurable类型是由AspectJ编织的,那么您仍然可以像平常一样在容器外进行单元测试,但是每次构建一个@Configurable对象时,您都会看到一条警告消息,指出它尚未由Spring配置。

Working with multiple application contexts

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是一个AspectJ单例方面。单例方面的范围与静态成员的范围相同,也就是说每个类加载器有一个方面实例定义类型。这意味着如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@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应用程序将共享相同的方面实例,这可能不是您想要的。

5.8.2. Other Spring aspects for AspectJ

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

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

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

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

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

对于希望使用Spring配置和事务管理支持但不希望(或不能)使用注释的AspectJ程序员,spring-aspects.jar还包含可以扩展以提供自己的切入点定义的抽象方面。 有关更多信息,请参阅AbstractBeanConfigurerAspect和AbstractTransactionAspect方面的来源。 作为示例,以下摘录显示了如何使用与完全限定的类名匹配的原型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);
}

5.8.3. Configuring AspectJ aspects using Spring IoC

在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自动代理支持,应该使用配置中定义的@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运行时。

5.8.4. Load-time weaving with AspectJ in the Spring Framework

加载时织入(LTW)是指在将AspectJ方面加载到Java虚拟机(JVM)中时将AspectJ方面编织到应用程序的类文件中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW:本节不是LTW的介绍。有关LTW细节的详细信息以及仅使用AspectJ配置LTW(Spring根本不参与),请参阅AspectJ开发环境指南的LTW部分。

Spring框架为AspectJ LTW带来的增值功能是对织造过程进行更细粒度的控制。 ‘Vanilla’AspectJ LTW使用Java(5+)代理来实现,该代理在启动JVM时通过指定VM参数来启用。因此它是一个JVM范围的设置,在某些情况下可能会很好,但通常有点太粗糙。启用S​​pring的LTW使您能够在每个ClassLoader基础上开启LTW,这显然更加细化,并且在“单JVM多应用程序”环境中更有意义(例如在典型应用服务器环境)。

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

现在销售情况已经结束,让我们先来看看使用Spring的AspectJ LTW的一个简单示例,然后详细介绍以下示例中介绍的元素。有关完整示例,请参阅Petclinic示例应用程序。

A first example

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

此处介绍的示例使用XML样式配置,还可以使用Java配置来配置和使用@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编织者我们想将ProfilingAspect编织到我们的类中。 这个文件约定,即称为META-INF / aop.xml的Java类路径上的文件(或多个文件)的存在是标准的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(稍后会解释,现在只需信任它)。 此加载时织机是负责将一个或多个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();
    }
}

还有最后一件事要做。 本节的介绍确实表明,可以在每个ClassLoader的基础上有选择地使用Spring切换LTW,这是真的。 但是,就本例而言,我们将使用Java代理(随Spring提供)来开启LTW。 这是我们将用来运行上面的Main类的命令行:

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

-javaagent是一个用于指定和启用代理程序以测试运行在JVM上的程序的标志。 Spring框架附带了一个代理InstrumentationSavingAgent,该代理打包在spring-instrument.jar中,该代码作为上述示例中-javaagent参数的值提供。

主程序执行的输出将如下所示。 (我已经在calculateEntitlement()实现中引入了Thread.sleep(..)语句,以便事件探查器实际上捕获0毫秒以外的内容–1234毫秒不是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或生产中的应用程序的构建。

Aspects

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

‘META-INF/aop.xml’

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

该文件的结构和内容在主要的AspectJ参考文档中有详细介绍,感兴趣的读者可以参考该资源。 (我明白这一部分很简短,但aop.xml文件是100%的AspectJ–没有适用于Spring的特定信息或语义,因此没有额外的价值可供我们作为结果) ,而不是重复一下AspectJ开发人员写的非常令人满意的部分,我只是在那里指导你。)

Required libraries (JARS)

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

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

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

如果您使用Spring提供的代理来启用检测,则还需要:

  • spring-instrument.jar

Spring configuration

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

如果您不熟悉运行时类文件转换的想法,建议您在继续之前阅读java.lang.instrument包的javadoc API文档。 这不是一件很麻烦的事情,因为在那里有 - 相当烦人的 - 宝贵的小文档……关键的接口和类将至少在您阅读本节时摆放在您面前作为参考。

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

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

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {

}

或者,如果您偏好基于XML的配置,请使用<context:load-time-weaver />元素。 请注意,该元素是在上下文命名空间中定义的。

<?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,例如LoadTimeWeaver和AspectJWeavingEnabler。 默认的LoadTimeWeaver是DefaultContextLoadTimeWeaver类,它尝试修饰一个自动检测到的LoadTimeWeaver:将自动检测到的LoadTimeWeaver的确切类型取决于您的运行时环境(总结在下表中)。

Table 13. DefaultContextLoadTimeWeaver LoadTimeWeavers

Runtime Environment LoadTimeWeaver implementation
Running in Oracle’s WebLogic WebLogicLoadTimeWeaver
Running in Oracle’s GlassFish GlassFishLoadTimeWeaver
Running in Apache Tomcat TomcatLoadTimeWeaver
Running in Red Hat’s JBoss AS or WildFly JBossLoadTimeWeaver
Running in IBM’s WebSphere WebSphereLoadTimeWeaver
JVM started with Spring InstrumentationSavingAgent (java -javaagent:path/to/spring-instrument.jar) InstrumentationLoadTimeWeaver
Fallback, expecting the underlying ClassLoader to follow common conven ReflectiveLoadTimeWeaver

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

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

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

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

如果您使用的是基于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">

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

</beans>

通过配置定义和注册的LoadTimeWeaver可以稍后使用众所周知的名称loadTimeWeaver从Spring容器中检索。 请记住,LoadTimeWeaver只是作为Spring的LTW基础结构添加一个或多个ClassFileTransformers的机制而存在。 执行LTW的实际ClassFileTransformer是ClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包)类。 请参阅ClassPreProcessorAgentAdapter类的类级别javadoc以了解更多详细信息,因为实际编织方式的具体细节超出了本节的范围。

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

Table 14. AspectJ weaving attribute values

Annotation Value XML Value Explanation
ENABLED on AspectJ编织处于打开状态,并且方面将根据需要在加载时进行编织。
DISABLED off LTW关闭……在加载时不会编织任何方面。
AUTODETECT autodetect 如果Spring LTW基础结构可以找到至少一个META-INF / aop.xml文件,则AspectJ编织处于打开状态,否则它将关闭。 这是默认值。

Environment-specific configuration

最后一节包含在应用程序服务器和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复制到CATALINAHOME/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应用程序

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

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

或者,考虑使用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利用这样的类加载器来启用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"/>

Generic Java applications

当在不支持或不受现有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代理将对整个虚拟机进行测试,这可能证明是昂贵的。

出于性能方面的原因,只有当您的目标环境(如Jetty)没有(或不支持)专用LTW时,才推荐使用此配置。

5.9. Further Resources

有关AspectJ的更多信息可以在AspectJ网站上找到。

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

书中的AspectJ in Action第二版Ramnivas Laddad(Manning,2009)受到强烈推荐; 本书的重点在于AspectJ,但是探索了很多普通的AOP主题(在一定深度上)。

阅读更多
文章标签: Spring
上一篇【翻译 Spring 5.0.4.RELEASE】4. Spring Expression Language (SpEL)
下一篇【翻译 Spring 5.0.4.RELEASE】6. Spring AOP APIs
想对作者说点什么? 我来说一句

Spring面向切面编程AOP

2012年11月29日 4.13MB 下载

Spring 框架 web 5.0.4.RELEASE jar

2018年02月25日 6.34MB 下载

没有更多推荐了,返回首页

关闭
关闭