Spring源码之AOP官方文档翻译

11.1 Introduction

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

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

Spring 2.0+ AOP

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

本章将讨论基于Spring 2.0+ schema-和基于@ aspectj的AOP支持。正如Spring 1.2应用程序中通常公开的那样,低级AOP支持将在下一章中讨论。

AOP在Spring框架中被用来……

  • 提供声明性企业服务,特别是作为EJB声明性服务的替代。此类服务中最重要的是声明式事务管理
  • 允许用户实现自定义方面,用AOP补充他们对OOP的使用。

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

11.1.1 AOP concepts

让我们从定义一些核心AOP概念和术语开始。这些术语不是特定于spring的……不幸的是,AOP术语不是特别直观;然而,如果Spring使用它自己的术语,那就更令人困惑了。

  • 方面(Aspect): 跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,方面是使用常规类(the schema-based approach)或用@Aspect注释(@AspectJ样式)注释的常规类实现的。
  • 连接点(Joinpoint): 程序执行过程中的一个点,如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法的执行。
  • 通知(Advice): aspect在特定连接点上采取的动作。不同类型的advice包括“around”、“before”和“after”建议。(下面将讨论Advice类型。)许多AOP框架(包括Spring)将Advice建模为拦截器,在连接点周围维护拦截器链。
  • 切入点(Pointcut): 匹配连接点的述语。Advice与切入点表达式相关联,并在切入点匹配的任何连接点上运行(例如,具有特定名称的方法的执行)。切入点表达式匹配的连接点概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
  • 引入(Introduction): 代表类型声明其他方法或字段。Spring AOP允许您向任何建议的对象引入新的接口(和相应的实现)。例如,您可以使用Introduction使bean实现IsModified接口,以简化缓存。(在AspectJ社区中,Introduction被称为类型间声明。)
  • 目标对象(Target Object): 对象由一个或多个方面通知。也称为被建议对象。由于Spring AOP是使用运行时代理实现的,所以这个对象将始终是代理对象。
  • AOP代理(AOP Proxy): AOP框架为实现aspect契约(通知方法执行等等)而创建的对象。在Spring框架中,AOP代理将是JDK动态代理或CGLIB代理。
  • 织入(Weaving): 将aspects 与其他应用程序类型或对象链接起来以创建建议对象。这可以在编译时(例如使用AspectJ编译器)、加载时或运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行编织。

AOP的术语并不好理解,推荐特别棒的一篇文章,有助于理解AOP术语 :专治不会看源码的毛病--spring源码解析AOP篇

advice的类型:

  • Before advice: 在连接点之前执行的Advice ,但是它没有能力阻止执行流继续到连接点(除非它抛出异常)。
  • After returning advice: 在连接点正常完成后执行的Advice:例如,如果方法返回而未引发异常。
  • After throwing advice: 如果方法通过抛出异常退出,则执行的Advice。
  • After (finally) advice: 无论连接点以何种方式退出(正常或异常返回),都要执行Advice。
  • Around advice: 围绕连接点(如方法调用)的Advice。这是最有力的Advice。Around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或引发异常来缩短Advice的方法执行。

 Around advice是最普遍的一种advice。因为Spring AOP像AspectJ一样提供了完整的advice类型,所以我们建议您使用能够实现所需行为的最不强大的advice类型。例如,如果您只需要用方法的返回值更新缓存,那么您最好实现after返回通知而不是around通知,尽管around通知可以完成相同的任务。使用最特定的advice类型可以提供更简单的编程模型,并且出错的可能性更小。例如,您不需要在用于around advice的连接点上调用proceed()方法,因此不会调用失败。

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

连接点的概念,与切入点相匹配,是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框架的AOP功能通常与Spring IoC容器一起使用。方面使用普通的bean定义语法进行配置(尽管这允许强大的“自适应”功能):这是与其他AOP实现的关键区别。在Spring AOP中,有一些事情您不能轻松或有效地完成,例如advise非常细粒度的对象(通常是域对象):在这种情况下,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的讨论,请参阅下一章。

11.1.3 AOP Proxies

Spring AOP默认为AOP代理使用标准JDK动态代理。这允许代理任何接口(或一组接口)。

Spring AOP还可以使用CGLIB代理。这对于代理类而不是接口是必要的。如果业务对象没有实现接口,则默认使用CGLIB。因为对接口而不是类编程是一种很好的实践;业务类通常将实现一个或多个业务接口。强制使用CGLIB是有可能的,在那些(希望是罕见的)情况下,您需要通知没有在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法。

理解Spring AOP是基于代理的这一事实很重要。请参阅第11.6.1节“理解AOP代理”,以深入研究这个实现细节的实际含义。

11.2 @AspectJ support

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

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

11.2.1 Enabling @AspectJ Support

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

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

Enabling @AspectJ Support with Java configuration

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

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

Enabling @AspectJ Support with XML configuration

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

<aop:aspectj-autoproxy/>

这假设您正在使用模式支持,如第41章“基于XML模式的配置”所述。关于如何在aop名称空间中导入标记,请参阅章节41.2.7,“aop模式”。

11.2.2 Declaring an aspect

启用了@AspectJ支持后,在应用程序上下文中定义的任何bean都将被Spring自动检测到,并用于配置Spring AOP。下面的示例显示了一个not-very-useful 的方面所需的最小定义:

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

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

以及用org.aspectj.lang.annotation.Aspect 注释的NotVeryUsefulAspect 类定义。

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 Declaring a pointcut

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

一个示例将有助于明确切入点签名和切入点表达式之间的区别。下面的例子定义了一个名为“anyOldTransfer”的切入点,它将匹配名为“transfer”的任何方法的执行:

@Pointcut("execution(* transfer(..))")// 切入点表达式
private void anyOldTransfer() {}// 切入点的签名

构成@Pointcut注释值的切入点表达式是一个常规的AspectJ 5切入点表达式。有关AspectJ的切入点语言的完整讨论,请参阅AspectJ编程指南(以及扩展,AspectJ 5开发人员记事本)或Colyer等人的《Eclipse AspectJ》或Ramnivas Laddad的《AspectJ在行动》等关于AspectJ的书籍。

支持切入点标识符

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

Other pointcut types

完整的AspectJ切入点语言支持Spring中不支持的其他切入点指示器。这些都是:

call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, and @withincode.

在Spring AOP解释的切入点表达式中使用这些切入点指示符将导致抛出IllegalArgumentException。

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

 

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

由于Spring AOP框架基于代理的特性,目标对象中的调用根据定义不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,代理上的公共和受保护方法调用将被拦截,如果需要,甚至包可见的方法也会被拦截。然而,通过代理的公共交互应该始终通过公共签名来设计。

注意,切入点定义通常与任何拦截的方法匹配。如果严格地说切入点是公共的,那么即使在CGLIB代理场景中,通过代理进行潜在的非公共交互,也需要相应地定义它。

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

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

 

  • 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中执行的方法)具有给定注释的连接点的匹配限制
bean(idOrNameOfBean)

idOrNameOfBean 令牌可以是任何Spring bean的名称:提供了使用*字符的有限通配符支持,因此,如果为Spring bean建立一些命名约定,就可以很容易地编写一个bean PCD表达式来挑选它们。与其他切入点指示符一样,bean PCD可以是&&'ed、||'ed和!也(否定)。

请注意,bean PCD只支持Spring AOP,而不支持本机AspectJ编织。它是AspectJ定义的标准PCDs的一个特定于spring的扩展,因此不能用于@Aspect模型中声明的方面。

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

结合切入点表达式

切入点表达式可以使用'&&'、'||'和'!'进行组合。还可以通过名称引用切入点表达式。下面的例子展示了三个切入点表达式: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可见性规则(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何地方的公共切入点等等)。可见性不影响切入点匹配。

共享公共切入点定义

在使用企业应用程序时,您通常希望从几个方面引用应用程序的模块和特定的操作集。为此,我们建议定义一个“SystemArchitecture”方面来捕获公共切入点表达式。这方面的典型情况如下:

package com.xyz.someapp;

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

@Aspect
public class SystemArchitecture {

    /**
     * 如果方法定义在com.xyz.someapp.web中的类型中,则连接点位于web层。web包或它下面的任何子包。
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * 如果方法定义在com.xyz.someapp.service中的类型中,则连接点位于服务层。服务包或其下的任何子包。
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * 如果方法是在com.xyz.someapp.dao中的类型中定义的,连接点在数据访问层。dao包或它下面的任何子包。
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * 业务服务是服务接口上定义的任何方法的执行。这个定义假设接口放在“service”包中,实现类型在子包中。
     *
     * 如果您按功能区域对服务接口进行分组(例如,在包com.xyz.someapp.abc.service和com.xyz.someapp.abc.service中)。然后可以使用切入点表达式“execution(* com.xyz.someapp. service.*.*(..))”。
     *
     * 或者,可以使用“bean”PCD编写表达式,比如“bean(*Service)”。(这假定您已经以一致的方式命名了Spring服务bean。)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * 数据访问操作是dao接口上定义的任何方法的执行。这个定义假设接口放在“dao”包中,实现类型在子包中。
     */
    @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>元素在第11.3节“基于模式的aop支持”中进行了讨论。事务元素将在第17章事务管理中讨论。

例子

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

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

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

任何公共方法的执行:

execution(public * *(..))

任何名称以“set”开头的方法的执行:

execution(* set*(..))

执行由AccountService接口定义的任何方法:

execution(* com.xyz.service.AccountService.*(..))

service 包中定义的任何方法的执行:

execution(* com.xyz.service.*.*(..))

执行在service 包或子包中定义的任何方法:

execution(* com.xyz.service..*.*(..))

service 包中的任何连接点(仅在Spring AOP中执行方法):

within(com.xyz.service.*)

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中执行方法),并且在运行时传递的参数是可序列化的:

args(java.io.Serializable)

“args”更常用在绑定形式中:-请参阅关于通知的下一节,了解如何在通知主体中提供方法参数。

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

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

@target(org.springframework.transaction.annotation.Transactional)

任何目标对象声明类型具有@Transactional注释的连接点(仅在Spring AOP中执行方法):

@within(org.springframework.transaction.annotation.Transactional)

任何执行方法具有@Transactional注释的连接点(仅在Spring AOP中执行方法):

@annotation(org.springframework.transaction.annotation.Transactional)

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

@args(com.xyz.security.Classified)

在名为tradeService的Spring bean上的任何连接点(仅在Spring AOP中执行方法):

bean(tradeService)

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

bean(*Service)

Writing good pointcuts

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

然而,AspectJ只能处理它被告知的内容,为了获得最佳匹配性能,您应该考虑它们试图实现什么,并在定义中尽可能缩小匹配的搜索空间。现有的标识符自然分为三组:kinded, scope and context:

 

 编写良好的切入点应该尝试并至少包括前两种类型(kinded和scoping),同时,如果希望基于连接点上下文进行匹配,或将该上下文绑定到通知中使用,则可以包括上下文指示器。仅提供分类指示符或上下文指示符都可以工作,但是由于所有额外的处理和分析,可能会影响编织性能(使用的时间和内存)。作用域指示符匹配起来非常快,它们的使用意味着AspectJ可以非常快速地排除不应该进一步处理的连接点组——这就是为什么一个好的切入点应该总是包含一个连接点(如果可能的话)。

 

  • 分类指示符是选择特定类型连接点的指示符。例如:execution, get, set, call, handler
  • 作用域指示器是那些选择一组感兴趣的连接点(可能是许多类型的连接点)的指示器。例如:在代码内部
  • 上下文指示符是那些基于上下文匹配(或绑定)的指示符。例如:this, target, @annotation

11.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

当匹配的方法执行正常返回时运行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) {
        // ...
    }

}

返回属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值将作为相应的参数值传递给通知方法。return子句还限制只匹配返回指定类型值(在本例中是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() {
        // ...
    }

}

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

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

}

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

After (finally) advice

但是,在(最终)通知运行之后,将退出匹配的方法执行。它是使用@After注释声明的。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

最后一种advice是 around advice。Around通知围绕匹配的方法执行“运行”。它有机会在方法执行之前和之后执行工作,并确定方法何时、如何、甚至是否真正执行。如果需要以线程安全的方式(例如启动和停止计时器)共享方法执行前后的状态,则经常使用Around通知。始终使用最不强大的形式的建议,以满足您的需求(例如,不要使用周围的建议,如果简单,在建议可以做)。

Around通知是使用@Around注释声明的。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知体中,对ProceedingJoinPoint 调用proceed()会导致底层方法执行。也可以调用proceed方法传入Object[]—数组中的值将在方法执行时用作参数。

使用Object[]调用proceed时的行为与使用AspectJ编译器编译的around通知时的行为稍有不同。建议使用传统的AspectJ语言编写,左右进行传递的参数的数量必须匹配的参数传递到周围的建议(不是参数由底层连接点的数量),并继续在一个给定的参数传递的价值立场取代原来的价值实体价值的连接点是绑定到(别担心如果现在没有意义!)。Spring采用的方法更简单,更适合基于代理的、只执行语义。只有在编译为Spring编写的@AspectJ方面并在AspectJ编译器和weaver中使用proceed with参数时,才需要注意这种差异。有一种方法可以编写在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()。请注意,proceed可能在around通知的主体中被调用一次、多次,或者根本没有调用,所有这些都是非常合法的。

Advice parameters

Spring提供了完全类型的通知——这意味着您可以在通知签名中声明所需的参数(正如我们在上面的返回和抛出示例中看到的那样),而不是一直使用Object[]数组。我们稍后将看到如何使参数和其他上下文值对通知主体可用。首先,让我们看看如何编写通用的通知,以便找出该通知当前通知的方法。

Access to the current JoinPoint:访问当前连接点

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

Passing parameters to advice:将参数传递给通知

我们已经看到了如何绑定返回值或异常值(在返回和抛出通知之后使用)。要使参数值对通知主体可用,可以使用args的绑定形式。如果在args表达式中使用参数名代替类型名,那么在调用通知时,相应参数的值将作为参数值传递。举个例子就能说明这一点。假设您希望通知以Account对象作为第一个参数的dao操作的执行,并且需要访问advice主体中的帐户。你可以这样写:

@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();
    // ...
}

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
}

如果第一个参数是连接点、过程连接点或连接点的。在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
}

对连接点的第一个参数、过程连接点和连接点的特殊处理。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将尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,而通知方法只接受一个参数,那么这种配对是明显的!)如果给定可用信息,变量的绑定是不明确的,那么将抛出一个AmbiguousBindingException 。

如果上述所有策略都失败,那么将抛出IllegalArgumentException。

Proceeding with arguments

我们在前面提到过,我们将描述如何使用在Spring AOP和AspectJ中一致工作的参数编写一个proceed调用。解决方案只是确保通知签名按顺序绑定每个方法参数。例如:

@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相同的优先规则来确定通知执行的顺序。优先级最高的通知在“进入”时首先运行(因此,给定两条before通知,优先级最高的一条将首先运行)。从连接点出发的“On the way out”,优先级最高的通知最后运行(因此,给定两个after通知,优先级最高的通知将在第二个运行)。

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

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

11.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注释的值属性是AspectJ类型模式:-匹配类型的任何bean都将实现UsageTracked接口。注意,在上述示例的前面通知中,服务bean可以直接用作UsageTracked 接口的实现。如果以编程方式访问bean,您将编写以下代码:

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

11.2.6 Aspect instantiation models:方面实例化模型

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

默认情况下,应用程序上下文中每个方面都有一个实例。AspectJ将此称为单例实例化模型。定义具有替代生命周期的方面是可能的:- Spring支持AspectJ的perthis和pertarget实例化模型(目前不支持percflow、percflowbelow和pertypewithin)。

perthis方面通过在@Aspect注释中指定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 Example

现在您已经看到了所有组成部分的工作原理,让我们将它们组合在一起做一些有用的事情!

业务服务的执行有时会由于并发问题而失败(例如死锁失效)。如果操作被重试,下次很可能成功。对于适合在这种情况下重试的业务服务(不需要返回用户进行冲突解决的幂等操作),我们希望透明地重试操作,以避免客户端看到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;
    }

}

注意,方面实现了有序接口,因此我们可以将方面的优先级设置为高于事务通知的优先级(我们希望每次重试时都有一个新的事务)。maxretry和order属性都将由Spring配置。主要操作发生在围绕通知的doConcurrentOperation中。注意,目前我们将重试逻辑应用于所有businessService()s。我们试图继续,如果我们以一个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
}

并使用该注释对服务操作的实现进行注释。将方面更改为只重试幂等操作,这只涉及到细化切入点表达式,以便只有@Idempotent等操作匹配:

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

11.3 Schema-based AOP support

略。。。。。

11.4 Choosing which AOP declaration style to use

略。。。。。

11.5 Mixing aspect types

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

11.6 Proxying mechanisms

Spring AOP使用JDK动态代理或CGLIB为给定目标对象创建代理。(如果可以选择,JDK动态代理是首选)。

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

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

  • 不建议使用final方法,因为它们不能被覆盖。
  • 从Spring 3.2开始,不再需要将CGLIB添加到项目类路径中,因为CGLIB类是在org下重新打包的。它直接包含在spring-core JAR中。这意味着基于cglib的代理支持就像JDK动态代理一直拥有的那样“正常工作”。
  • 从Spring 4.0开始,代理对象的构造函数将不再被调用两次,因为CGLIB代理实例将通过Objenesis创建。只有当JVM不允许构造函数绕过时,您才可能看到来自Spring AOP支持的双重调用和相应的调试日志条目。

要强制使用CGLIB代理,请将<aop:config>元素的代理目标类属性的值设置为true:

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

要在使用@AspectJ autoproxy支持时强制CGLIB代理,请将<aop:aspectj-autoproxy>元素的“代理目标类”属性设置为true:

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

多个<aop:config/>部分在运行时分解为一个统一的自动代理创建器,它应用了指定的任何<aop:config/>部分(通常来自不同的XML bean定义文件)的最强的代理设置。这也适用于<tx:注解驱动的/>和<aop:aspectj-autoproxy/>元素。

需要说明的是:在<tx:注解驱动/>上使用proxy-target-class="true", <aop:aspectj-autoproxy/>或<aop:config/>元素将强制对这三个元素使用CGLIB代理。

11.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...
    }
}

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

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(),都将针对这个引用而不是代理调用。这具有重要意义。这意味着自调用不会导致与方法调用关联的通知获得执行机会。

好的,那么我们该怎么做呢?最好的方法(这里松散地使用术语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是相悖的。在创建代理时,它还需要一些额外的配置:

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 Programmatic creation of @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 Using AspectJ with Spring applications

Spring附带了一个小的AspectJ方面库,它在您的发行版中作为Spring-aspects.jar独立可用;为了使用类路径中的方面,需要将其添加到类路径中。第11.8.1节“使用AspectJ向依赖项注入带有Spring的域对象”和第11.8.2节“AspectJ的其他Spring方面”讨论了这个库的内容以及如何使用它。第11.8.3节“使用Spring IoC配置AspectJ方面”讨论了如何依赖注入使用AspectJ编译器编织的AspectJ方面。最后,第11.8.4节“在Spring框架中使用AspectJ进行加载时编织”介绍了使用AspectJ的Spring应用程序的加载时编织。

11.8.1 Using AspectJ to dependency inject domain objects with Spring:使用AspectJ向依赖项注入Spring域对象

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

@Configurable annotation将一个类标记为有资格进行spring驱动的配置。在最简单的情况下,它可以用作标记注释:

package com.xyz.myapp.domain;

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

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

当以这种方式作为标记接口使用时,Spring将使用bean定义(通常是原型作用域)配置带注释类型(在本例中为Account)的新实例,bean定义的名称与完全限定类型名称(com.xyz.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定义,并将其用作配置新帐户实例的定义。

您还可以使用自动连接来避免必须指定专用bean定义。要让Spring应用autowire,请使用@Configurable annotation的autowire属性:指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME分别用于按类型或按名称自动连接。作为一种替代方案,从Spring 2.5开始,最好在字段或方法级别使用@Autowired或@Inject(参见7.9节“基于注释的容器配置”了解更多细节)为您的@Configurable bean指定显式的依赖注入。

最后,通过使用dependencyCheck属性(例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)),您可以为新创建和配置的对象中的对象引用启用Spring依赖检查。如果该属性被设置为true,那么Spring将在配置之后验证是否设置了所有属性(不是原语或集合)。

11.8.2 Other Spring aspects for AspectJ

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

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

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

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

由于Spring Framework 4.2, Spring -aspects提供了一个类似的方面,为标准javax.transaction.Transactional提供了完全相同的特性。事务注释。查看JtaAnnotationTransactionAspect了解更多细节。

对于那些希望使用Spring配置和事务管理支持,但不希望(或不能)使用注释的AspectJ程序员,可以使用spring-aspects.jar还包含抽象方面,您可以扩展它们来提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspect和AbstractTransactionAspect方面的源代码。作为一个例子,下面的摘录展示了如何编写一个方面来配置域模型中定义的所有对象实例,使用与完全限定类名匹配的prototype 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 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方面的@可配置支持来实现。一旦方面实例有了由AspectJ运行时创建的bean, spring-aspects.jar将配置它们。

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

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

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

11.8.4 Load-time weaving with AspectJ in the Spring Framework

略。。。。。。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值