RedCloud Help

5.使用Spring进行切面编程

面向切面编程(AOP)是对面面向对象编程(OOP)的补充,它提供了另一种思考程序结构的方法。在OOP中,模块化的关键单位是类,而在AOP中,模块化的单位是方面。方面可以将跨越种类型和对象的关注点(如事务管理)模块化。(这类问题在AOP文献中通常被称为“横切”问题)。 AOP框架是Spring的关键组件之一。虽然Spring IOC容器并不依赖于AOP(也就是说,如果你不想使用AOP,就不需要使用它),但AOP与Spring IOC相辅相成,提供了一个非常强大的中间件解决方案。

在Spring框架中,AOP被用于:

  • 提供声明式企业服务。其中最重要的服务就是声明式事务管理。

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

5.1 AOP概念

首先,让我们定义一些AOP核心概念和术语。这些术语并非Spring专用。遗憾的是,AOP术语并不特别直观。不过,如果Spring使用自己的术语,那就更令人困惑了。

  • Aspect:跨多个类的模块化关注点。事务管理就是企业java应用程序中跨领域问题的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于模式的方法)或使用@Aspect注解的常规类(@AspectJ风格)来实现的。

  • join point:程序执行过程中的一个点,如方法的执行或异常的处理。在spring AOP中,连接点是代表一个方法的执行。

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

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

  • Introduction:代表类型声明复杂方法或字段。Spring AOP允许你为任何建议对象引入新的接口(以及相应的实现)。例如,你可以使用引入来使bean实现IsModified接口,以简化缓存。(在AspectJ社区,不愿说被称为类型间声明)。

  • Target object:被一个或多大个切面建议的对象。也称为“建议对象”。由于Spring AOP是通过使用运行时代理来实现的,因此该对象始终是一个车代理对象。

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

  • Weaving: 将各个方面与其他应用程序万里行或对象连接起来,创建一个建议对象。这可以在编译时(例如使用AspectJ编译器)、加载时或运行时完成。Spring AOP与其他纯java AOp框架一样,在运行时执行编织。

Spring AOP 包括以下几种建议:

  • before advice: 在连接点之前运行的建议,单不能阻止执行流进入连接点(除非抛出异常)。

  • after returning:在连接点正常完成后运行的建议(例如,如果方法返回时没有抛出异常)

  • after throwing advice:在方法抛出异常退出时运行的建议。

  • after (finally) advice: 无论连接点以何种方式退出(正常或特殊返回),都要运行建议。

  • Around advice:围绕连接点(如方法调用)的建议。这是最强大的一种建议。周围建议可以在方法调用前后执行自定义行为。它还负责选择是继续执行连接点,还是通过返回自己的返回值或抛出异常来缩短建议方法的执行时间。

左右建议是最通用的建议类型。与AspectJ一样,Spring AOP 也提供了各种建议类型,因此我们建议你使用功能最少的建议类型来实现所需的行为。例如,如果你只需要用方法的返回值更新缓存,那么你最好使用after returning advice而不是around advice,尽管around advice也能实现相同的功能。使用最特殊的建议类型可以提供更简单的变成模型,减少出错的可能性。例如,你不想要在用于周围建议的JoinPoint上调用proceed() 方法,因此也不会调用失败。

所有建议参数都是静态类型,因此你可以使用适当类型的建议类型(例如执行方法时候,返回值类型),而不是object数组。 连接点的概念是AOP的关键所在,它使AOP有别于仅提供拦截通能的旧技术。点切分使建议可以独立于面向对象的层次结构。例如,你可以将提供声明式事务管理的建议应用于一组跨越多个对象的方法(如服务层中的所有业务操作)。

5.2 Spring AOp功能和目标

Spring AOP是用纯java实现的。不需要特殊的编译过程。Spring AOP无需控制类加载器的层次结构,因此适合在servlet容器或应用服务器中使用。 SpringAOp 目前仅支持方法执行连接点(建议在Spring Bean上执行方法)。虽然可以在不破坏Spring AOP核心API的情况下添加对字段拦截支持,但并未实现字段拦截。如果你需要建议字段访问和更新连接点,请考虑使用AspectJ等语言。 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可以很好的解决企业JAVA应用程序中大多数适用于AOP的问题。 SpringAOp从未试图与AspectJ竞争,以提供全面的AOP解决方案。我们认为,Spring AOP等基于代理的框架和AspectJ等全面的框架都很有价值,他们是互补的,而不是竞争的。Spring 将Spring AOP与AspectJ无缝集成,以便在基于Spring的一致应用架构中实现AOP的所有用途。这种集成不会影响Spring AOP API或AOP Alliance API。Spring AOP保持向后兼容。有关Spring AOP API的讨论,见下一章。

框架的核心原则之一是非入侵性。这意味着你不应被迫在业务或领域模型中引入特定于框架的类和接口。不过,在某些地方,Spring框架确实提供了代码库中引入Spring框架特定依赖关系的选项。提供这种选择的理由是,在某些情况下,以这种方式读取或编码某些特定功能可能会更加简单。然而,Spring框架总是为你提供选择:你可以自由地做出明智的决定,选择最合适你的特定用例或场景的方案。

5.3 AOP 代理

Spring AOP 默认将标准的JDK动态代理用于AOP代理。这样就可以代理任何接口(或接口集)。 Spring AOP还可以使用CGLIB代理。这是代理类而非接口所必须得。默认情况下,如果业务对象没有实现接口,就会使用CGLIB。由于针对接口而非类编程是一种良好做法,因此业务类通常会实现一个或多个业务接口。在需要向未在接口上声明的方法提供建议,或需要将代理对象作为具体类型传递给方法时(希望这种情况很少见),可以强制使用CGLIB。 了解Spring AOP基于代理这一事实非常重要。请参阅“理解AOP代理”,全面了解这一实现细节的确切含义。

5.4 @AspectJ支持

@AspectJ指的是一种将切面声明为带有注解的常规JAVA类样式。作为AspectJ5版本的一部分,AspectJ项目引入了@AspectJ风格。Spring使用AspectJ提供的库对注解进行解析和匹配,从而解释了与AspectJ5相同的注解。不过,AOP运行时仍然是纯粹的Spring AOP,不依赖于AspectJ编译器或编织器。

5.4.1 启动@AspectJ支持

要在Spring配置中使用@AspectJ方面,你需要启用Spring支持,以便根据@AspectJ切面配置Spring AOP,并根据Bean是否被这些切面建议自动配置。我们所说的自动代理是指,如果Spring确定某个Bean受一个或多个方面的建议,它会自动为该Bean生成一个代理拦截方法调用,并确保根据需要运行建议。 @AspectJ支持可通过xml或java式配置启用。无论那种情况,你都需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径上(1.8或更高版本)。该库位于AspectJ发行版的lib目录中或maven central资源库中。

使用java配置启用@AspectJ支持

要使用java @Configuration启用@AspectJ支持。请添加@EnableAspectJAutoProxy注解,如下利所示:

@Configuration @EnableAspectJAutoProxy public class AppConfig { }

使用XML配置启用@AspectJ支持

<aop:aspectj-autoproxy/>

前提是你使用了基于XML模式的配置中所述的模式支持。有关如何在aop命名空间中导入标记,请参阅AOP模式。

5.4.2 声明一个Aspect

启动@AspectJ支持后,Spring会自动检测应用程序上下文中定义的任何Bean,并使用@AspectJ aspect(具有@Aspect注解)的类来配置Spring AOP。接下来的两个示例展示了一个不常用的切面所需的最小定义。 两个示例中的第一个实例显示了应用程序上下文中的常规bean定义,该定义指向具有@AspectJ注解的bean类:

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

两个实例中的第二个实例显示了NotVeryUsefulAspect类定义,该定义使用了org.aspectj.lang.annotation.Aspect注解:

package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { }

与其他类一样,Aspects(注解为@Aspect的类)可以有方法和字段。他们还可以包含指向,建议和引用(类型间)声明。

你可以在SpringXMl配置中通过@Configuration类中的@Bean方法将切面类注册到普通Bean,也可以让Spring通过类路径扫描自动检测切面类,这与任何其他Spring管理的bean都是一样的。不过,请注意,@Aspect注解不足以在类路径中实现自动检测。为此,你需要添加单独的@Component注解(或者,根据Spring组件扫描器的规则,添加符合条件的自定义注解)。

5.4.3声明Pointcut

点切确定了兴趣的连接点,从而使我们能够控制建议的运行时间。SpringAOP支持SpringBean的方法执行连接点,因此你可将快捷键方式视为SpringBean上的方法执行相匹配。一个切点声明有两个部分组成:一个由名称和任何参数组成的签名,以及一个点切表达式,该表达式确定了我们感兴趣的方法执行。在AOP的@AspectJ注解风格中,点切入签名由常规方法定义提供,而点切入表达式则通过@Pointcut注解来表示(作为点切签名的方法必须具有void返回类型)。 下面的示例可以帮助我们清楚地区分点切签名和点切表达式。下面的示例定义了一个名为anyOldTransfer的快捷方式,该快捷方式与任何名为transfer的方法的执行相匹配:

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

构成@Pointcut注解值的点切表达式是常规的AspectJ点切表达式。有关AspectJ点切语言的全面讨论,请参阅《AspectJ编程指南》(以及有关扩展的《AspectJ5开发人员手册》)或有关AspectJ的书籍(如Colyer等人撰写的《eclipse AspectJ》或Ramnivas Laddad撰写的《AspectJ in Action》)。

支持Pointcut设计

SpringAOp支持的点切表达式中使用一下AspectJ点切分单号(PCD):

  • execution:用于匹配方法执行连接点。在使用Spring AOP时,这是主要的切点代号。

  • within:将匹配限制为某些类型中的连接点(使用Spring AOP时,在匹配类型中声明的方法的执行)。

  • this:将匹配限制在Bean引用(Spring AOP代理)是指给定类型实例的连接点(使用Spring AOP时方法的执行)。

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

  • args:将匹配限制为参数为给定类型实例的连接点(使用Spring AOP时方法的执行)。

  • @target:将匹配限制在执行对象的类具有给定类型注解的连接点(使用SpringAOP时方法的执行)。

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

  • @within:将匹配限制在具体给定注解的类型中的连接点(使用SpringAOP时,在具有给定注解的类型中声明的方法的执行)

  • @annotation:将匹配限制在连接点的主题(在Spring AOP中运行的方法)具有给定注解的连接点。

完整AspectJ切点语言支持Spring中不支持的其他切点代号:call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。在Spring AOP解释的点切表达式中使用这些点血代号会抛出IllegalArgumentException。

由于Spring AOP仅将匹配限制在方法执行连接点上,因此前面关于切点代号的讨论给出的定义比AspectJ编程指南中的定义更窄。为此,AspectJ本身具有基于类型的语义,在执行连接点上,this和target均值同一对象:执行方法的对象。Spring AOP是具于代理的系统,它区分了代理对象本身(与this绑定)和代理背后的目标对象(与target绑定)。

由于Spring的AOP框架是基于代理的,因此根据定义,目标对象内部的调用不会被拦截。对于JDK代理,只能拦截代理商的公共接口方法调用。而使用CGLIB时,代理上的公共和受保护方法调用都会被拦截(必要时甚至会拦截包可见方法)。不过,通过代理进行的常见交互应始终通过公共签名来设计。

Spring AOP还支持名为Bean的附加PCD,通过该PCD,你可以将链接点的匹配限制为某个一名名的SpringBean或一组一命名的Spring Bean(使用通配符)。beanPCD的形式如下:

bean(idOrNameOfBean)

idOrNameOfBean标记可以是任何SpringBean的名称。我们提供了使用* 字符的有限定通配符支持,因此,如果你为SpringBean建立一些命名约定,你可以编写beanPCD表达式来选择它们。与其他点切制定付一样,bean PCD也可以与&&(和)、||(或)和!(否定)操作符一起使用。

组合pointcut表达式

你可以通过使用&&,||,!组合点切表达式。你还可以通过名称来引用点切表达式。下面的实例显示了三个点切表达式:

@Pointcut("execution(public * *(..))") private void anyPublicOperation() {} @Pointcut("within(com.xyz.myapp.trading..*)") private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {}
  • anyPublicOperation 如果方法执行连接点代表执行,则匹配的任何公共方法。

  • inTrading与交易模块中的方法执行匹配

  • tradingOperation 如果一个方法的执行代表了该方法中的任何公共方法,责任匹配交易模块。

如前所述,最佳做法是奖较小的命名组建构建成为更复杂的快捷方式表达式。当通过名称引用点切时,使用正常的java可见性规则(你可以在同一类型中看到私有点切,在层次结构中看到受保护的点切,在任何地方看到公有点切,在任何地方看到公有点切,等等)。可见性并不影响点切匹配。

共享常用pointcut定义

在使用企业应用程序时,开发人员经常希望在多个方面中引用应用程序的模块和特定操作集。为此,我们建议定义一个CommonPointcuts方面,用于捕获常用的指向表达式。这种方面通常类似于下面的示例:

package com.xyz.myapp; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class CommonPointcuts { /** * A join point is in the web layer if the method is defined * in a type in the com.xyz.myapp.web package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.web..*)") public void inWebLayer() {} /** * A join point is in the service layer if the method is defined * in a type in the com.xyz.myapp.service package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.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.myapp.dao package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.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.myapp.abc.service and com.xyz.myapp.def.service) then * the pointcut expression "execution(* com.xyz.myapp..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.myapp..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.myapp.dao.*.*(..))") public void dataAccessOperation() {} }

你可以在任何需要使用快捷方式表达式的地方引用在这切面中定义的快捷方式。例如,要使服务层具有事务性,可以编写一下内容:

<aop:config> <aop:advisor pointcut="com.xyz.myapp.CommonPointcuts.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:configaop:advisor元素。事务元素在事务管理中讨论。

实例

Spring AOP用户可能最长使用execution切点代号。执行表达式的格式如下:

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

除了返回类型模式(前面代码段中的ret-type-pattern) 、名称模式和参数模式外,其他部分都是可选的。返回类型模式决定了方法的返回类型。全称类型名称只有在方法返回给定类型时才匹配。名称模式与方法名称匹配。可以使用 通配符作为名称模式的全部或部分。如果制定了声明类型模式,请在尾部加上.将其连接到名称模式组件。参数模式稍微复杂一些:()与不带参数的方法相撇充沛,而(..) 则与任意数量(零或更多)的参数相匹配。( )模式匹配只接受一个任意类型的参数的方法。(*,String) 匹配接受两个参数的方法。第一个参数可以是任何类型,而第二个参数必须是String。有关详细信息,请参阅《AspectJ编程指南》中的“语言语义”部分。 下面的示例展示了一些常见的点切表达式:

  1. execution(public * *(..))执行任何公共方法。

  2. execution(* set*(..))执行任何名称以set开头的方法

  3. execution(* com.xyz.service.AccountService.*(..))执行由AccountService接口定义的任何方法。

  4. execution(* com.xyz.service.*.*(..))执行service软件包中定义的任何方法。

  5. execution(* com.xyz.service..*.*(..))执行服务包或其自爆中定义的任何方法。

  6. within(com.xyz.service.*)服务包内的任何连接点(仅在Spring AOP中执行方法)

  7. within(com.xyz.service..*)服务包或其子包内的任何连接点(仅在Spring AOP中执行方法)。

  8. within(com.xyz.service..*)服务包或其子包内的任何连接点(仅在Spring AOP中执行方法)。

  9. this(com.xyz.service.AccountService)代理实现AccountService接口的任何连接点(仅在Spring AOP中执行方法)。

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

  1. args(java.io.Serializable)任何接受单个参数运行时传递的参数为Serializable的连接点(仅在Spring AOP中执行方法)。

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

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

  2. @within(org.springframework.transaction.annotation.Transactional)目标对象的声明类型具有@Transactional注解的任何链接点(仅在Spring AOP中执行方法)。

  3. @annotation(org.springframework.transaction.annotation.Transactional)执行方法具有@Transactional注解的任何链接点(仅在Spring AOP中执行方法)。

  4. args(com.xyz.security.Classified) :任何接受单个参数的链接点(仅在Spring AOP中执行方法),且所传递的参数的运行时类型具有@Classified注解:

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

  6. bean(*Service) :再调名称匹配通配符表达式*Service的Spring Bean上的任何连接点(仅在Spring AOp中执行方法).

写出好的切入点

在编译过程中,AspectJ会对连接点进行处理,以优化匹配性能。检查代码并确定每个连接点是否与给定的点切匹配(静态或动态)是一个代价高昂的过程。(动态匹配意味着无法通过静态分析完全确定匹配(静态或动态)是一个代价高昂的过程。(动态匹配意味着无法通过静态分析完全确定匹配,因此需要在代码中进行测试,以确保代码运行时是否存在实际匹配)。在首次遇到点切声明时,AspectJ将其重写为匹配过程的最佳形式。这意味着什么呢?基本上指向代码会被改写为DNF(Disjunctive Normal Form)形式,并且指向代码的组件会被排序,以便首先检查那些评估成本较低的组件。这意味着你不必担心了解各种点切代号的性能,可能在点切生命中以任何顺序提供这些代号。 然后,AspectJ之后根据它被告知的内容工作。要想获得最佳的匹配性能,就应该考虑他们想要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指定符自然分为三类:同类指定符、范围指定符和上下文指定符:

  • 类型代号选择特定的连接点:execution 、get 、set、call 和handler

  • 范围代号选择一组感兴趣的连接点:within和withincode

  • 上下文代号根据上下文进行匹配(选择绑定):this 、target 和@annotation

写得好的快捷方式至少应包括前两种类型(种类和范围)。你可以包含上下文指定符,以便根据连接点上下文进行匹配,或绑定上下文以便在建议中使用。值提供种类指定符或只提供上下文指定符是可行的,单由于要进行额外的处理和分析,可能会影响编织性能(所有时间和内存)。范围代号的匹配速度非常快,使用范围代号意味着AspectJ可以非常快速地剔除不应进一步处理的连接点组。如果可能得话,一个好的连接点切入应该总是包含一个连接点切入。

5.4.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.CommonPointcuts.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注解来声明它:

mport org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.CommonPointcuts.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.CommonPointcuts.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } }

returning属性中使用的名称必须与建议方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给建议方法。returning子句还将匹配范围限制为仅返回指定类型值的方法执行(在本例中为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.CommonPointcuts.dataAccessOperation()") public void doRecoveryActions() { // ... } }

通常,你希望仅在抛出给定类型的异常时才运行建议,而且你还经常需要在建议正文中访问抛出的异常。你可以使用throwing属性来限制匹配(如果需要,请使用Throwable作为异常类型),并将抛出的异常绑定到建议参数。下面的实例展示了如何做到这一点:

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing( pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } }

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

请注意,@AfterThrowing并不表示一般的异常处理回调。具体来说,@AfterThrowing建议方法只能接受来自连接点(用户声明的目标方法)本身的异常,而不能接受来自附带的@After/@AfterReturning方法的异常。

After (Finally) Addvice

在匹配的方法执行退出时,运行After建议。它通过使用@After注解来声明。After advice 必须能处理正常和异常返回条件。它通常用于释放资源和类似用途。下面的示例展示了如何使用after finally建议:

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.After; @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doReleaseLock() { // ... } }

请注意,AspectJ中的@After建议被定义finally之后的建议,类似try-catch语句中的finally块。它将对连接点(用户声明的目标方法)抛出的任何结果、正常返回或异常进行调用,而@AfterReturning仅适用于成功的正常返回。

Around Advice

最优一种建议是环绕建议。环绕建议“环绕”匹配的很爱的执行而运行。它有机会再方法运行之前和之后执行工作,并决定方法何时,如何甚至是否真正运行。如果需要以线程安全的方式共享方法执行前后状态,例如启动和停止计时器,通常会使用周围建议。

通过使用@Around注解对方法进行注解,可声明左右建议。方法应声明Object为其返回类型,方法的第一个参数必须是ProceedingJoinPoint类型。在建议方法的主体中,必须ProceedingJoinPoint上调用proceed() 才能运行底层方法。调用不带参数的proceed()将导致在调用底层方法时提供调用者的原始参数。对于高级用例,proceed() 方法有一个接受参数的数组的重载变体(Object[])。当底层方法被调用时,数组中的值被用作底层方法的参数。

调用proceed时,其行为与AspectJ编译器变异的周围建议proceed的行为略有不同,对于使用传统AspectJ语言编程的围绕建议,传递给proceed的参数必须与传递给围绕建议的参数数相匹配(而不是底层连接点所使用的参数),并且在给定参数位置传递给参数相匹配(而不是底层连接点所使用的参数),并且在给定参数位置传递给继续的值将取代该值所绑定实体的连接点上的原始值(如果现在还不明白,请不要担心)。

周围建议返回的值就是方法调用这看到的返回值。例如,如果有缓存,一个简单的缓存方面可以从缓存中返回一个值;如果没有缓存,则调用proceed(并返回该值)。请注意,proceed可以调用一次,多次或根本不在环绕建议的正文中调用。搜友这些都是合法的。

如果你将环绕建议方法的返回类型声明为void,那么null将始终返回给调用者,从而实际上忽略了任何调用proceed的结果。因此,建议方法声明Object返回类型。建议方法通常应返回调用proceed所返回的值,即使底层方法的返回类型为void。但是,建议可根据使用情况选择返回缓存值、包装值或其他值。

下面的示例展示了如何使用环绕建议:

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.CommonPointcuts.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } }

Advice Parameters

Spring 提供完全类型化建议,这意味你可以在建议签名中声明所需的参数(正如我们签名看到的返回和抛出示例),而无需一直使用Object[] 数组。我们将在本节稍后部分了解如何向建议体提供参数和其他上下文值。首先,我们来看看如何编写通用建议,以了解建议当前正在建议的方法

Access to the Current JoinPoint

任何建议方法都可以声明一个org.aspectj.lang.JoinPoint类型的参数作为其第一个参数。需要注意的是,建议环绕需要声明一个ProceedingJoinPoint类型的首参数,而ProceedingJoinPoint是JoinPoint的子类。 JoinPoint接口提供了许多有用的方法:

  • getArgs():返回方法参数。

  • getThis():返回代理对象。

  • getTarget():返回目标对象。

  • getSignature():返回建议使用的方法的描述。

  • toString():打印有关建议方法的有用说明。

Passing Parameters to Advice

我们已经了解了如何绑定返回值获异常值(使用返回后和抛出后建议)。要使建议体可以使用参数值,可以使用args的绑定形式。如果在args表达式中使用参数名代替类型名,那么在调用建议时,相应参数的值将作为参数值传递。举个例子可以更清楚地说明这一点。假设你要建议执行将account对象作为第一个参数的DAO操作,并且需要访问建议正文中的账户。你可以编写如下内容:

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

poincut表达式中的args(account,...)部分有两个作用。首先,它将匹配限制在方法至少需要一个参数,且传递给该参数是Account实例的方法执行中。其次,它通过account参数向建议提供实际的Account对象。 另一种写法是声明一个点切。这种写法如下:

@Pointcut("com.xyz.myapp.CommonPointcuts.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 }

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

Determining Argument Names

建议调用中的参数绑定依赖于指向表达式中使用的名称与建议和指向方法签名中声明的参数名称相匹配。

Spring AOp使用一下ParameterNameDiscoverer实现来确定参数名称。每个发现者都有机会发现参数名,第一个成功的发现者获胜。如果已注册的发现者都无法确定参数名,则会出现异常。

  1. AspectJAnnotationParameterNameDiscoverer:使用用户通过相应建议或点切注解中的argNames属性显式指定的参数名称。详情请参阅“显式参数名”。

  2. KotlinReflectionParameterNameDiscoverer:使用Kotlin反射API确定参数名称。只有当类路径中存在此类API时,才会使用该发现器。GraalVM本机镜像不支持。

  3. StandardReflectionParameterNameDiscoverer:使用标准java.lang.reflect.Parameter API确定参数名称。要求代码在编译时使用-parameters标志来表示javac。建议在java 8+上使用。

LocalVariableTableParameterNameDiscoverer:分析建议类字节码中的局部变量表,根据调试信息确定参数名称。要求使用调试符号(-g: vars)编译代码。自Spring Framework 6.0起已被弃用,Spring Framework 6.1将移除该功能,转而使用-parameters编译代码。不支持GraalVM本地镜像,除非相应的类文件作为资源存在于镜像中。

  1. AspectJAdviceParameterNameDiscoverer:根据指向表达式、returning和throwing子句推导出参数名称。有关所用算法的详细信息,请参阅javadoc。

Explicit Argument Names

@AspectJ advice 和pointcut注解有一个可选的argNames属性,可用于指定注解方法的参数名。

下面的示例展示了如何使用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 }

Proceeding with Arguments

我们在前面提到过,我们将介绍如何编写一个带有参数的proceed调用,该D调用可在SpringAOp和AspectJ中一致运行。解决方法是确保建议前面按顺序绑定每个方法参数。下面的实例展示了如何做到这一点:

@Around("execution(List<Account> find*(..)) && " + "com.xyz.myapp.CommonPointcuts.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相同的优先级规则来决定建议的执行顺序。再调“进入”时,优先级最高的建议优先执行(因此,在给定两个建议之前,优先级最高的建议优先执行)。在从连接点“离开”时,优先级最高的建议最后执行(因此,在给定两个后建议的情况下,优先级最高的建议第二执行)。 当定义在不同方面的两条建议都需要在同一个连接点运行时,除非你另行指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。这是通过切面类中实现org.springframework.core.Ordered接口或使用@Order注解来实现的。在给定两个方面的情况下,从Ordered.getOrder() (或注解)返回JNDI值的方面具有较高的优先级。

@Before、@After、@AfterReturning、@AfterThrowing。但是请注意,@After建议方法将在同一方面中的任何@AfterReturning或@AfterThrowing建议方法之后被有效调用的,这遵循AspectJ对@After的“after

当定义在同一@Aspect类中的两个相同类型的建议(例如,两个@After建议方法)都需要在同一连接点运行时,排序是未定义的(因为对于javac编译的类,无法通过反射检索代码声明顺序)。请将考虑将此类建议方法折叠为每个@Aspect类中每个连接点的一个建议方法,或者这些建议重构为单独的@Aspect类,你可以通过Ordered或@Order在方面级别对其进行排序。

5.4.5 介绍

引入(在AspectJ中成为类型间声明)使一个方面能够声明建议对象实现给定接口,并代表这些对象提供该接口的实现。 你可以使用@DeclareParents注解进行介绍。该注解用于声明匹配类型有一个新的父类(因此得名)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked的接口实现,一下方面声明服务接口的所有实现者也实现UsageTracked接口(例如,通过JMX进行统计):

@Aspect public class UsageTracking { @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) public static UsageTracked mixin; @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)") public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } }

要实现的接口有注解字段的类型决定。@DeclareParents注解的value属性是一种AspectJ类型模式。任何匹配类型的Bean都会实现UsageTracked接口。请注意,在前面示例的前面建议中,服务Bean可以直接用作UsageTracked接口的实现。如果以变成方式访问Bean,你可以编写以下内容:

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

5.4.6 Aspect Instantiation Models

默认情况下,应用程序上下问中的每个方面都有一个实例。AspectJ将此成为单利实例化模型。我们可以定义具有不同声明周期的方面。Spring支持AspectJ的perthis和pertarget实例化模型;目前不支持percflow、percflowbelow和pertypewitehin。 你可以通过在@Aspect注解中指定perthis子句来声明perthis方面。请看下面的示例:

@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())") public class MyAspect { private int someState; @Before("com.xyz.myapp.CommonPointcuts.businessService()") public void recordServiceUsage() { // ... } }

在前面的实例中,perthis子句的作用是为每个执行业务服务的唯一服务对象创建一个方面实例(再调与点切表达式匹配的连接点上绑定到this的每个唯一对象)。在服务对象上首次调用方法时会创建切面实例。当服务对象退出作用域时,切面也会退出作用域。在创建切面实例之前,其中的任何建议都不会运行。一旦创建了切面实例,其中声明的建议就会在匹配的连接点上运行,单只有当服务对象与此方面相关联的对象时才会运行。有关per子句的更多信息,请参阅《AspectJ编程指南》。 pertarget实例化模型的工作方式与perthis完全相同,但它会在匹配的连接点上为每个唯一的目标对象创建一个切面实例。

5.4.7 AOP实例

既然你已经了解了所有组成部分的工作原理,我们就可以把他们组合起来做一些有用的事情了。 业务服务的执行有时会因并发问题(如死锁失败)而失败。如果重试操作,下一次就有可能成功。对于在这种情况下适合重试的业务服务(无需返回用户解决冲突的惰性操作),我们希望透明地重试操作,以避免客户端看到PessimisticLockingFailureException。这一要求显然跨越了服务层中的多个服务,因此非常适合通过一个切面来实现。 由于我们希望重试操作,因此需要使用左右建议,以便多次调用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.CommonPointcuts.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和order属性均由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>

如果要对方面进行改进,使其只重试幂等操作,我们可以定义一下Idempotent注解

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

然后,我们就可以使用注解来注解服务操作的实现。要对只重试幂等操作的方面进行修改,就需要对点切表达式进行精炼,以便只有@Idempotent操作才能匹配,具有如下:

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

5.5 基于模式的AOP支持

如果你更喜欢基于XML的格式,Spring还支持使用aop命名空间标记来定义切面。它支持与使用@AspectJ样式时完全相同的指向表达式和建议类型。因此,本节中,我们将重点讨论该语法,并请读者参考上一节的讨论(@AspectJ支持),以便了解如何编写切点表达式和绑定建议参数。 要使用本节所述的aop命名空间标记,需要导入spring-aop模式,如基于XML模式的配置中所述。有关如何导入aop命名空间中的标记,请参阅aop模式。 在spring配置中,所有方面和顾问元素都必须置于aop:config元素中(在应用程序上下文配置中可以有多个aop:config 元素)。 aop:config元素可包含pointcut、advisor和aspect元素(请注意,这些元素必须按顺序声明)。

类型的配置大量使用了Spring的自动代理机制。如果你已经通过使用BeanNameAutoProxyCreator或类似方法使用显式自动代理,这可能会导致一些问题(例如建议无法编织)。推荐的使用模式是只使用aop:config

5.5.1 声明一个切面

使用模式支持,切面是一个常规java对象,定义为Spring应用上下文中的一个Bean。在对象的字段和方法中捕获状态和行为,在对象的字段和方法中捕获状态和行为,在XML中捕获指向和建议信息。 你可以使用aop:aspect元素声明一个切面,并使用ref属性引用后盾Bean,如下例所示:

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

支持切面的Bean(本例中为Bean)当然可以像其他Spring Bean一样进行配置和注入依赖关系。

5.5.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.CommonPointcuts.businessService()"/> </aop:config>

假设你有一个CommonPointcuts切面,如共享常用切点方式定义中所述。 然后,在一个切面中声明一个点切入与声明一个顶层点切入非常相似,正如下面的实力所示:

<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子表达式时,&amp;&amp在XML文档中显得笨拙,因此可以使用and 、or和not关键字分别替代&&amp;||和!。例如,前面的切点方式可以写更好:

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

建议必须声明接受收集到的连接点上下文,方法是假如名称匹配的参数,如下所示:

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

在组合 pointcut 子表达式时, && 在 XML 文档中显得笨拙,因此可以使用 and 、 or 和 not 关键字分别代替 && 、 || 和 ! 。例如,前面的快捷方式可以写得更好:

<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.5.3 Declaring Advice

基于模式的AOP支持使用了与@AspectJ风格相同的五种建议,他们语义也完全相同。

Before Advice

Before建议匹配的方法执行前运行。它通过使用aop:before元素aop:aspect中声明,如下例所示:

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

这里,dataAccessOperation是在顶层(《aop:config》)定义的切点的id。若要以内联方式定义该切点切线,请将pointcut-ref属性替换为pointcut属性,如下所示:

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

正如我们在讨论@AspectJ样式时提到的,使用命名的指向函数可以大大提高代码的可读性。 method属性标识了提供建议正文的方法(doAccessCheck)。该方法必须为包含建议的切面元素所引用的bean所定义。在执行数据访问操作(与点切表达式匹配的方法执行连接点)之前,会调用切面bean上的doAccessCheck方法。

After Returning Advice

返回建议后,在匹配的方法执行正常完成时运行。它在aop:aspect中的声明方式与advice之前的声明方式相同。下面的实例展示了如何声明它:

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

与@AspectJ样式一样,你可以在建议正文中获取返回值。为此,请使用returning属性指定应传递返回值的参数名称,如下例所示:

<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

当你匹配的方法通过抛出异常退出执行时,会在抛出建议后运行。它通过使用after-throwing元素在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

后(Finally)建议无论匹配的方法执行如何退出都会运行。你可以使用after元素来声明它,如下例所示:

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

Around Advice

最后一种建议是环绕建议。环绕建议“环绕”匹配方法的执行而运行。它有机会在方法运行之前和之后执行,并决定方法何时、如何甚至是否真正运行。如果需要以线程安全的方式共享方式执行前后的状态,例如启动和停止计时器,通常会使用周围建议。

你可以使用aop: around元素围绕建议进行声明。建议方法应声明Object为其返回类型,方法的第一个参数必须是ProceedingJoinPoint类型。在建议方法的主体中,必须在ProceedingJoinPoint上调用proceed() 才能运行底层方法。调用不带参数的proceed()将导致在调用底层方法时提供调用者的原始参数。对于高级用例,proceed() 方法有一个接受参数数组的重载变体(Object[])。在调用底层方法时,数组中的值将被用作该方法的参数。有关使用Object[] 调用proceed的注意事项,请参阅《左右建议》。 下面的实例展示了如何在XML中围绕建议进行声明:

<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属性相同(如确定参数名称中所述)。下面的实例展示了如何在XML中指定参数名称:

<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 PersonService { Person getPerson(String personName, int age); } public class DefaultPersonService implements PersonService { public Person getPerson(String name, int age) { return new Person(name, age); } }

接下来是方面。请注意,profile(..) 方法接受大量强类型参数,其中第一个参数恰好是用于继续方法调用的连接点。如下例所示,该参数的存在表明profile(..)将用作around的建议:

package x.y; import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.util.StopWatch; public class SimpleProfiler { public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable { StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'"); try { clock.start(call.toShortString()); return call.proceed(); } finally { clock.stop(); System.out.println(clock.prettyPrint()); } } }

最后,下面的XML配置示例会对特定连接点执行上述建议产生影响:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- this is the object that will be proxied by Spring's AOP infrastructure --> <bean id="personService" class="x.y.service.DefaultPersonService"/> <!-- this is the actual advice itself --> <bean id="profiler" class="x.y.SimpleProfiler"/> <aop:config> <aop:aspect ref="profiler"> <aop:pointcut id="theExecutionOfSomePersonServiceMethod" expression="execution(* x.y.service.PersonService.getPerson(String,int)) and args(name, age)"/> <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod" method="profile"/> </aop:aspect> </aop:config> </beans>

请看下面的驱动程序脚本:

import org.springframework.beans.factory.BeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import x.y.service.PersonService; public final class Boot { public static void main(final String[] args) throws Exception { BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml"); PersonService person = (PersonService) ctx.getBean("personService"); person.getPerson("Pengo", 12); } }

有了这样一个Boot类,我们就能在标准输出上得到类似下面的输出结果:

StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)

Advice Ordering

当需要在同一连接点(执行方法)运行多个建议时,排序规则如建议排序中所述。切面之间的优先级通过aop:aspect 元素中的order属性确定,或者通过向支持切面的Bean添加@order注解,或者通过让Bean实现Ordered接口来确定。

5.5.4 介绍

引入(在AspectJ中称为类型间声明)可让一个切面声明建议对象实现给定接口,并代表这些对象提供该接口的实现。 你可以在aop:aspect中使用aop:declare-parents元素来进行介绍。你可以使用aop: declare-parens元素来声明匹配类型有一个新的父类(因此得名)。例如,在给定了名为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.CommonPointcuts.businessService() and this(usageTracked)" method="recordUsage"/> </aop:aspect>

这样,支持usageTracking Bean的类将包含以下方法:

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

要实现的接口由implement-interface属性决定。types-matching属性的值是AspectJ类型模式。任何匹配类型的Bean都会实现usageTracked接口。请注意,在前面的实例的前面建议中,服务Bean可以直接作用UsageTracked接口的时间。要以编程方式访问Bean,可以编写一下代码:

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

5.5.5 Aspect Instantiation Models

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

5.5.6 Advisors

顾问的概念来自于Spring中定义的AOp支持,在AspectJ中并没有直接的对应的概念。顾问就像一个小型的自包含的切面,它只是一条建议。建议本身由Bean表示,并且必须实现《Spring中的建议类型》( advice types in Spring)算描述的建议接口之一。顾问可以利用AspectJ的切点表达式。 Spring通过aop:advisor元素支持顾问的概念。你最常见到的是它与事务性建议结合使用,后者在Spring中也有自己的命名空间支持。下面的实例展示了一个顾问:

<aop:config> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> <aop:advisor pointcut-ref="businessService" advice-ref="tx-advice"/> </aop:config> <tx:advice id="tx-advice"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice>

出了前面示例中使用的pointcut-ref属性外,你还可以使用pointcut属性来定义内联点切分表达式。 要定义建议的优先级,以便建议可以参与排序,请使用order属性定义建议的Ordered值。

5.5.7 AOP Schema Example

本节将展示《一个AOP示例》中的并发锁定失败重试示例在使用模式支持后的重写效果。 业务服务的执行有时会因并发问题(如死锁失败)而失败。如果重试操作,下一次就有可能成功。对于在这种情况下适合重试的业务(无需返回用户解决冲突的惰性操作),我们希望透明地重试操作,以避免客户端看到PessimisticLockingFailureException。这一要求显然跨越了服务层中的多个服务,因此非常适合通过一个切面来实现。 由于我们希望重试操作,因此需要使用环绕建议,以便多次调用proceed。下面的列表显示了基本方面的实现(这是一个使用模式支持的普通java类):

public class ConcurrentOperationExecutor implements Ordered { private static final int DEFAULT_MAX_RETRIES = 2; private int maxRetries = DEFAULT_MAX_RETRIES; private int order = 1; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int numAttempts = 0; PessimisticLockingFailureException lockFailureException; do { numAttempts++; try { return pjp.proceed(); } catch(PessimisticLockingFailureException ex) { lockFailureException = ex; } } while(numAttempts <= this.maxRetries); throw lockFailureException; } }

请注意,切面实现了Ordered接口,因此我们可以将切面的优先级设置为高于事务建议(我们希望每次重试都有一个新事务)。maxRetries和order属性均由Spring配置。主要操作发生在doConcurrentOperation周围的建议方法中。我们尝试继续。如果PessimisticLockingFailureException失败,我们会再试一次,除非我们已经尝试了所有的重试。

相应的Spring配置如下:

<aop:config> <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor"> <aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..))"/> <aop:around pointcut-ref="idempotentOperation" method="doConcurrentOperation"/> </aop:aspect> </aop:config> <bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor"> <property name="maxRetries" value="3"/> <property name="order" value="100"/> </bean>

请注意,我们暂时假定所有业务服务都是幂等性的。如果情况并非如此,我们可以通过引入Idempotent注解并使用该注解来注解服务操作的实现,从而完善该方面,使其只重试真正的幂等操作,如下例所示:

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

对只重试幂等操作方面的修改涉及到对点切表达式进行细化,以便只有@Idempotent操作才匹配,具体如下:

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

5.6 选择使用哪种AOP声明样式

一旦确定切面是实现给定需求的最佳方法,那么如何决定使用Spring AOP还是AspectJ,如何确定采用切面语言(代码)风格、@AspectJ注解风格还是Spring XML风格?这些决定受到多种因素的影响,包括应用需求、开发和团队对AOP的熟悉程度。

5.6.1 Spring AOP还是完整的AspectJ

使用最简单的方法。Spring AOP比使用完整的AspectJ更简单,因为无需在开发和构建流程中引入AspectJ编译器/编织器。如果你只需要建议在Springbean上执行操作,那么Spring AOP就是正确的选择。如果你需要向非Spring 容器管理对象(如典型的域对象)提供建议,则需要使用AspectJ。如果你希望为简单方法执行以外的连接点提供建议(例如,字段获取或设置连接点等),也需要使用AspectJ。 使用AspectJ时,你可以选择AspectJ语言语法(也成为“代码样式”)或@AspectJ注解样式。很明显,如果你不适用java 5+,你已经做出了选择:使用代码样式。如果切面在你的设计中扮演重要角色,并且你可以使用eclipse的AspectJ Development Tools(AJDT)插件,那么AspectJ语言语法是首选。这种语言更简洁,因为它是专门为编写切面而设计的。如果你不适用eclipse,或者只有几个方面在应用程序中不起作用,你可能需要考虑使用@AspectJ样式,在集成开发环境中坚持使用常规java编译,并在构建脚本中添加切面编辑阶段。

5.6.2 @AspectJ or XML for Spring AOP?

如果你选择使用Spring AOP,你可以选择@AspectJ或XML风格。这其中需要考虑各种权衡因素。 现有的Spring用户可能最熟悉XML风格,而且它有真正的POJO支持。当使用AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个很好的校验标准是,你是否切点表达式视为你配置的一部分,而你可能希望对其进行独立更改)。使用XML风格,可以说,从配置中可以更清楚地看出系统中存在哪些方面。 Xml风格有两个缺点。首先,它不能将需求的实现完全封装在一个地方。DRY原则任务,系统中的任何支持都应该是一个单一的,明确的,权威的表述。使用XML样式时,有关如何实现需要的只是会被分割到后备bean类的声明和配置文件中的XML中。使用@AspectJ样式时,这些信息被封装在一个模块:切面。其次,与@AspectJ样式相比,XML样式所能表达的内容略显有限:它只支持“单例”切面实例化模型,而且无法将XML中声明的命名指向结合起来。例如,在@AspectJ样式中,你可以写出如下内容:

@Pointcut("execution(* get*())") public void propertyAccess() {} @Pointcut("execution(org.xyz.Account+ *(..))") public void operationReturningAnAccount() {} @Pointcut("propertyAccess() && operationReturningAnAccount()") public void accountPropertyAccess() {}

在XML样式中,你可以声明前两个pointcuts:

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

XML方法的缺点是不能通过组合这些定义来定义accountPropertyAccess切点。 @AspectJ风格支持更多的实例化模型和更丰富的点切分组合。它的优点是将切面保持我一个模块化单元。它还有一个优点,那就是@AspectJ切面既能被Spring AOP理解,也能被AspectJ使用。因此,如果你后来决定需要AspectJ的功能来实现额外的需求,你可以轻松迁移到经典的AspectJ设置。总的来说,除了简单的企业服务配置之外,Spring团队更强项于使用@AspectJ风格来实现自定义切面。

5.7 Mixing Aspect Types

通过使用自动代理支持、模式定义的aop:aspect方面、声明的aop:advisor 顾问,甚至是同一配置中其他样式的代理和拦截器,完全可以混合使用@AspectJ样式的切面。所有这些都是通过使用相同的底层支持机制实现的,可以毫无困难地共存。

5.8 Proxying Mechanisms

Spring Aop 使用JDK动态代理或CGLIB为给定目标对象创建代理。JDK动态代理内置于JDK中,而CGLIB是一种常见的开源类定义库(重新打包为spring-core)。 如果要代理的目标对象至少实现了一个接口,就会使用JDK动态代理。目标类型实现的所有接口都会被代理。如果目标对象没有实现接口,则会创建一个CGLIB代理。 如果您想强制使用CGLIB代理(例如,代理目标对象定义的所有方法,而不仅仅是哪些由其接口实现的方法),你可以这样做。单应考虑以下问题:

  • 使用CGLIB时,不能建议使用final方法,因为在运行时生成的子类中无法重载这些方法。

  • 从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"/>

5.8.1 了解AOP代理

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

如果在对象引用上调用方法,该方法将直接在该对象引用上调用,如下图和列表所示: img.png

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

当客户端代码的引用时代理时,情况略微不同。请看下面的代码和图: img_1.png

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())都将针对this引用而非代理进行调用。这具有重要的意义。这意味着自我调用不会导致与方法调用相关的建议有机会运行。 好吧,那该怎么办呢?最好的办法(这里的“最好”一词用得比较宽泛)是重构代码,避免发生自我调用。这确实需要做一些工作,单这是最好、最不伤身体的方法。下一种方法绝对可怕的,我们迟迟不敢之处,正式因为它太可怕了。你可以(虽然对我们来说是痛苦的)将类中的逻辑完全绑定到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.addInterface(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.9 以编程方式创建@AspectJ代理

除了在配置中使用aop:configaop:aspectj-autoproxy 声明切面外,还可以通过编程创建向对象提供建议的代理。有关Spring的AOPApI的全部细节,请参阅下一章。在此,我们将重点介绍通过使用@AspectJ切面自动创建代理的功能。 你可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ切面建议的目标对象创建代理。如下例所示,该类的基本用法非常简单:

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

更多信息请参见javadoc。

5.10 在Spring应用程序中使用Aspect

本章到目前为止所介绍的都是纯粹的Spring AOP。在本节中,我们将讨论如果你的需求超过了Spring AOP单独提供的功能,你可以如何使用AspectJ编译器或编织器来代替或补充Spring AOP。 Spring随附了一个小型AspectJ方面库,该库在发行版中以spring-aspects.jar的形式独立提供。你需要将其添加到classpath中,以便使用其中的切面。使用AspectJ与Spring依赖注入领域对象和《AspectJ的其他Spring方面》讨论了该库的内容以及如何使用它。通过使用Spring IOC配置AspectJ切面讨论了如何对使用AspectJ编译器编织的AspectJ切面进行依赖注入。最后,《在Spring框架中使用AspectJ进行加载时编织》介绍了如何为使用AspectJ的Spring应用程序进行加载时编织。

5.10.1 使用AspectJ与Spring一起依赖注入领域对象

Spring容器会实例化和配置应用程序上下文定义的Bean。你也可以要求Bean工厂配置一个预先存在的对象,并给出包含要应用的配置的Bean定义的名称。spring-aspects.jar包含了一个注解驱动的切面,它利用这一功能允许对任何对象进行依赖注入。该支持旨在用于在任何内容控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用new操作符以编程方式创建的,或者是由ORM工具根据数据库查询结果创建的。 @Configurable注解将一个类标记为符合Spring驱动的配置条件。在最简单的情况下,你可以纯粹将其用作标记注解,如下例所示:

package com.xyz.myapp.domain; import org.springframework.beans.factory.annotation.Configurable; @Configurable public class Account { // ... }

以这种方式作为标记接口时,Spring会通过使用与全称类型名称(com.xyz.myapp.domain.Account) 相同的bean定义(通常是原型作用域)来配置注解类型(Account,在本例中)新的实例。由于bean的默认名称是其类型的全称,因此声明原型定义的一种便捷方式是省略id属性,如下例所示:

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

如果要显式指定要使用的原型bean定义的名称,可以直接在注解中指定,如下例所示:

package com.xyz.myapp.domain; import org.springframework.beans.factory.annotation.Configurable; @Configurable("account") public class Account { // ... }

现在,Spring会查找名为account的Bean定义,并将其用作配置新的Account实例的定义。 你还可以使用自动布线来避免指定专门的Bean定义。要让Spring应用自动注入,请使用@Configurable注解的autowire属性。你可以指定@Configurable( autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME) 分别用于按类型名称自动注入。作为替代方法,你最好在字段或方法级别通过@Aurowired或@Inject为@Configuable Bean显示的、注解驱动的依赖注入(更多详情信息参见基于注解的容器配置) 最后,你可以使用dependencyCheck属性为新创建和配置的对象中的对象引用启用Spring依赖性检查。如果此属性设置为true,Spring会在配置后验证是否已设置所有属性(非基元或集合)。 请注意,单独使用注解不会产生任何作为。是spring-aspects.jar中的AnnotationBeanConfigurerAspect作用于注解的存在。实质上,该切面是说:“在使用@Configurable注解的类型的新对象初始化返回后,根据注解的属性使用Spring配置新创建的对象。”在此上下文中,“初始化”指的是新实例化的对象(例如,使用new操作符实例化的对象)以及正在进行反序列化(例如,通过readresolve() 进行反序列化)的Serializable对象

上段中的一个关键短语是“实质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是没有问题的。在这种情况下,“初始化后”意味着依赖关系是在对象构造完成后注入的。这意味着依赖项不能在类的构造函数体中作用。如果希望在构造函数主题运行之前注入依赖关系,从而使依赖关系可用于构造函数主体,则需要在@Configurable声明中定义这一点,如下所示:

为此,必须使用AspectJ编织器编织注解类型。你可以使用构建时的Ant或Maven任务(例如,请参阅《AspectJ开发环境指南》)或加载时编织(请参阅《Spring框架中AspectJ的加载时编织》)来实现这一目的。AnnotationBeanConfigurerAspect本身需要由Spring进行配置(以便获得用于配置新对象的Bean工厂的引用)。如果你使用基于java的配置,你可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:

@Configuration @EnableSpringConfigured public class AppConfig { }

如果你更喜欢基于XML的配置,Spring context命名空间定义了一个方便的context:spring-configured元素,你可以如下使用:

<context:spring-configured/>

在配置切面之前创建的@Configurable对象实例会导致向调试日志发送一条信息,并且不会对该对象进行配置。例如,Spring配置中的Bean在被Spring初始化时会创建域对象。在这种情况下,你可以使用depends-on Bean属性手动指定Bean依赖于配置方面。下面的示例展示了如何使用depends-on属性:

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

单元测试 @Configurable对象

支持@Configurable的目的之一是实现域对象的独立单元测试,而不会出现与硬编码查找相关的困难。如果AspectJ没有编织@Configurable类型,那么在单元测试期间,注解不会产生任何影响。你可以在被测对象中设置模拟或存根属性引用,然后照常进行测试。如果AspectJ已编织了@Configurable类型,你仍可在容器外进行正常的单元测试,但每次构建@Configurable对象时,你都会看到一条警告消息,表明Spring未对其进行配置。

working with multiple application contexts

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是AspectJ的单利切面。单利切面的我尊三与与static成员的作用域相同:每个定义类型的类加载器都有一个切面实例。这意味着,如果你在同一classloader层次结构中定义了多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfigured Bean以及在classpath上将spring-aspects.jar放在何处。 考虑一个典型的Spring Web应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用业务服务以及支持这些服务所需的所有内容,并为每个servlet提供一个子应用程序上下文(其中包含该servlet的特定定义)。所有这些上下文都共存于同一个类加载器层次结构中,因此AnnotaionBeanConfigurerAspect只能对其中一个上下文进行引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured Bean。这定义了你可以希望注入到域对象中的服务。这样做的后果是,你无法通过使用@Configurable机制(无论如何,这可能都不是你想要做的),使用对子(特定于服务)上下文中定义的Bean的引用来配置域对象。 在同一容器中部署多个web应用程序时,请确保每个web应用程序都使用自己的类加载器加载spring-aspect.jar中的类型(例如,将spring-aspects.jar放在WEB-INF/lib中)。如果spring-aspects.jar只添加到整个容器的classpath中(因此由共享的父类加载器加载),那么所有Web应用程序将共享同一个方面实例(这可能不是你想要的)。

5.10.2 AspectJ的其他Spring切面

除@Configurable切面外,spring-aspects.jar还包含了一个AspectJ切面,你可以使用该切面为使用@Transactional注解的类型和方法驱动Spring的事务管理。这主要面向希望在Spring容器之外使用Spring框架事务支持的用户。 解释@Transactional注解的切面是AnnotationTransactionAspect。使用切面时,你必须注解实现类(或该类中的方法或两者),而不是改实现的接口(如果有的话)。AspectJ遵循java的规则,即接口上的注解不会被继承。 类上的@Transactional注解指定了执行类中任何公共操作的默认事务语义。 类中方法的@Transactional注解会覆盖类注解(如果存在)问出的默认事务语义。任何可见性的方法都可以注解,包括私有方法。直接注解非公开方法是为此类方法的执行获取欧事务分界的唯一方法。

4.2以来,spring-aspects提供了一个类型的切面,为标准javax.transaction.Transactional注解提供了完全相同的功能。有关详细信息,请查看JtaAnnotationTransactionAspect。

对于希望使用Spring配置和事务管理支持但又不想使用注解的AspectJ程序员,spring-aspects.jar还包含了abstract切面,你可以对其进行扩展,已提供自己的切点定义。有关详细信息,请参阅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(..)) && CommonPointcuts.inDomainModel() && this(beanInstance); }

5.10.3 使用Spring IOC配置Aspect的各个切面

当你在spring应用程序中使用AspectJ切面时,自然希望并期望能够通过spring配置这些切面。AspectJ运行时本身负责切面的创建,而通过spring配置AspectJ创建的切面的方法取决于切面所使用的AspectJ实例化模型(pre-xxx子句)。 AspectJ的大多数切面都是单利切面。这些切面的配置非常简单。你可以创建一个正常引用切面类型的bean定义,并包含factory-method=" aspectOf" bean属性。这将确保spring通过请求AspectJ获取切面实例,而不是尝试自己创建实例。下面的实例展示了如何使用factory-method=" aspectOf"属性:

<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进行编织(例如,对领域模型类型使用加载时编织),而其他@AspectJ切面要与Spring AOP一起使用,并且这些切面都在Spring中进行了配置,那么你需要告诉SpringAOp @AspectJ自动代理支持,配置中大快朵颐的@AspectJ切面的那个子集应被用于自动代理。为此,尼克在aop:aspectj-autoproxy/ 生命中使用一个或多个元素。每个元素都制定了一个名称模式,de/>。下面的实例展示了如何使用<代理 Aop自动 元素:

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

5.10.4 在Spring框架中使用AspectJ进行加载时编织

加载时编织(LTW)是指在应用程序的类文件加载到java虚拟机(JVM)时,将AspectJ切面便知道类文件中的过程。本届的重点是在Spring框架的特定上下文中配置和使用LTW。本届并非LTW的一般介绍。有关LTW的具体细节以及仅使用AspectJ配置LTW(完全不涉及Spring)的全部详情,请参阅《AspectJ开发环境指南》中的LTW部分。 Spring Framework为AspectJ LTW带了的价值在于它能对编织过程进行更精细的控制。Vanilla' AspectJ LTW是通过使用java(5+)代理来实现的,在启动JVM时,通过制定VM参数来开启代理。因此,他是一种JVM范围内的设置,在某些情况下可能没问题,但往往有点过于粗糙。启用了SpringLTW可让你在每个classloader的基础上开启LTW,这种设置更加精细,在“单JVM多应用”环境(如单行的应用服务器环境)中更有意义。 此外,在某些环境中,这种支持可实现加载时编织,而无需对应用服务器的启动脚本进行任何修改,这些脚本是添加-/to/aspectjweaver.jar或-javaagent: path/to/spring-instrument.ajr(如本节后面所述)所必须的。开发人员可以配置应用程序上下文以启用加载时编织,而无需依赖管理员,因为管理员通常负责部署配置(如启动脚本)。 现在推销已经结束,让我们先看一个使用Spring的AspectJ LTW快速驶离,然后详细介绍示例中引入的元素。有关完整实例,请参阅Petclinic实例应用程序。

第一个例子

假设你是一个应用程序开发人员,负责真的让人系统中某些性能问题的原因。与其使用剖析工具,我们不如使用简单的剖析工具,以便快速获得一些性能指标。之后,我们可以立即对该特定区域应用更精细大额剖析工具。

这里介绍的实例使用的是XML配置。你也可以使用java配置来配置和使用@AspectJ。具体来说,你可以使用@EnableLoadTimeWeaving注解来替代context:load-time-waver/

下面的实例展示的是剖析切面,它并不花哨。它是一个机遇时间的剖析器,使用@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编织到我们的类中。这种文件惯例,即在java classpath中存在一个(或多个)名为META-INF/aop.xml的文件,是标准的AspectJ。下面的实例显示了aop.xml文件:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- a service object; we will be profiling its methods --> <bean id="entitlementCalculationService" class="foo.StubEntitlementCalculationService"/> <!-- this switches on the load-time weaving --> <context:load-time-weaver/> </beans>

现在所有必要的工件(切面、META-INF/aop.xml文件和Spring配置)都已到位,我们可以常见一下带有main(..)方法的驱动程序类,已显示LTW的运行:

package foo; import org.springframework.context.support.ClassPathXmlApplicationContext; public final class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class); EntitlementCalculationService entitlementCalculationService = (EntitlementCalculationService) ctx.getBean("entitlementCalculationService"); // the profiling aspect is 'woven' around this method execution entitlementCalculationService.calculateEntitlement(); } }

我们还有最后一件事要做。在本节的引言中,我们确实提到可以使用spring按ClassLoader选择性地开启LTW,事实也确实如此。不过,在本例中,我们使用java代理(随Spring提供)来开启LTW。我们使用一下命令运行前面显示的Main类:

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

-javaagent是一个标记,用于指定和启用代理,已检测在JVM上运行的程序。Spring Framework随附了这样一个代理,及instrumentationSavingAgent,他被打包在spring-instrument.jar中,在签名的实例中,该代理被提供为-javaagent参数的值。 Main程序的执行输出与下例类似。(我在calculateEntitlement()实现中引入了Thread.sleep(..) )语句,一边跑吸气实际捕获0毫秒以外的内容(01234毫秒并非aop引入的开销)。下面的列表显示了我们运行跑吸气时得到的输出结果:

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

由于此LTW是通过使用完整的Aspect来实现的,因此我们不局限与建议使用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支持的所有基础知识,本节的其余部分将详细解释没想配置的使用背后的原因。

Aspects

在LTW中使用的切面必须是AspectJ切面。你可以使用AspectJ语言本身编写,也可以使用@AspectJ样式编写。这样,你的切面就即使有效的AspectJ切面,也是有效的Spring AOP切面。此外,编译后的切面类必须在类路径上可用。

META-INF/aop.xml

AspectJ LTW基础框架是通过使用java lasspath上的一个或多个META-INF/aop.xml文件(直接或更常见的是jar文件)来配置的。 该文件的结构和内容详见AspectJ参考文档的LTW部分。由于aop.xml文件是100%AspectJ文件,因此我们再次不再对其做进一步描述。

Required libraries(JARS)

要使用Spring框架对AspectJ LTW 的支持,至少需要以下库:

  • spring-aop.jar

  • aspectjweaver.jar 如果使用Spring提供的代理来启动仪器,还需要

  • spring-instrument.jar

Spring Configuration

Spring LTW支持的关键组件是LoadTimeWeaver接口(位于org.springframework,instrument.classloading包中),以及Spring发行版中附属的大量实现。LoadTimeWeaver负责在运行时将一个或多个java.lang.instrumetn.CalssFileTransformers添加到ClassLoader中,这为各种有趣的应用打开了大门,而LTW正是其中之一。

为特定的ApplicationContext配置LoadTimeWeaver就像添加一行代码一样简单。(请注意,你几乎肯定需要使用ApplicationContext作为Spring容器,通常情况下,BeanFactory是不够的,因为LTW支持使用BeanFactoryPostProcessors。) 要启用Spring Framework的LTW支持,你需要配置LoadTimeWeaver,通常使用@EnableLoadTimeWeaving注解来的完成,如下所示

@Configuration @EnableLoadTimeWeaving public class AppConfig { }

另外,如果你更喜欢基于XMl的配置,可使用context:load-time-weaver/ 元素。请注意,钙元素是在context命名空间中定义的。下面的实例展示了如何使用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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:load-time-weaver/> </beans>

前面的的配置为你自动定义和注册了许多LTW特有的基础结构Bean,例如LoadTimeWeaver和AspectJWeavingEnabler。默认的LoadTimeWeaver是DefaultContextLoadTimeWeaver类,它试图装饰自动检测到的LoadTimeWeaver。自动检测到的LoadTimeWeaver的确切类型取决于运行环境。下表总结了各种LoadTimeWeaver实现:

运行环境

LoadTimeWeaver实现

Tomcat中运行

TomcatLoadTimeWeaver

在GlassFish中运行(仅限于EAR部署)

GlassFishLoadTimeWeaver

在RedHat的JBoss AS或WildFly中运行

JbossLoadTimeWeaver

在IBM的WebSphere中运行

WebSphereLoadTimeWeaver

在Oracle WebLogic中运行

WebLogicLoadTimeWeaver

jVM以Spring InstrumentationSavingAgent启动

InstrumentationLoadTimeWeaver

后退,希望底层的ClassLoader遵循用惯例(即addtransformer和可选的getThrowawayClassLoader方法

ReflectiveLoadTimeWeaver

请注意,该表仅列出了使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWeavers。你可以准确指定要使用的LoadTimeWeaver视线。 要使用java配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWe挨着人()方法。下面的实例制定了一个ReflectiveLoadTimeWeaver:

@Configuration @EnableLoadTimeWeaving public class AppConfig implements LoadTimeWeavingConfigurer { @Override public LoadTimeWeaver getLoadTimeWeaver() { return new ReflectiveLoadTimeWeaver(); } }

如果使用基于XML的配置,可以将完全限定的类名指定为context:load-time-weaver/ 元素上weaver-class属性的值。同样,下面的示例制定了一个ReflectiveLoadTimeWeaver类名:

<?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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://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是CalssPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包)类。有关详细信息,请参阅ClassPreProcessorAgentAdapter类的类级javadoc,因此实际编织的具体方法超出了本文档的范围。 配置中还有最后一个属性需要讨论:aspectjWeaving属性(如果使用XML,则为aspectj-weaving)。该属性控制是否启用LTW。它接受三种可能值之一,如果不存在该属性,默认值为autodetect。下表总结了三种可能得值:

Annotation value

Xml Value

说明

ENABLED

on

AspectJ已开启编制功能,并在加载时根据情况编织各方面

DISABLED

off

LTW关闭。加载时不编织任何切面

AUTODETECT

autodetect

如果Spring LTW基础架构至少能找到一个META-INF/aop.xml文件,那么AspectJ编织就会开启。否则,则关闭。这是默认值

特定环境配置

最后一部分包含在应用服务器和web容器等环境中使用Spring的LTW支持时所需的其他设置和配置。

Tomcat、JBoss、WebSphere、WebLogic

Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和Oracle WebLogic Server都提供了能够进行本地仪表化的通用应用程序ClassLoader。Spring的本地LTW可以利用这些ClassLoader实现来提供AspectJ结构。如前所述,你只需启用加载时植入即可。具体来说,你无需修改JVM启动脚本添加 -javaagent:path/to/spring-instrument.jar。 请注意,在JBoss上,你可能需要禁用应用程序服务器扫描,以防止他在应用程序实际启动钱加载类。一个快速的解决方案是在你的构建中添加一个名为WEB-INF/jboss-scanning.xml的文件,内容如下:

<scanning xmlns="urn:jboss:scanning:1.0"/>

通用Java应用程序

当你需要特定LoadTimeWeaver实现不支持的环境中进行类检测室,jVM代理是通用的解决方案。在这种情况下,spring提供了instrumentationLoadTimeWeaver,它需要一个特定于Spring(但非常通用)的JVM代理,即spring-instrument.jar,由常见的@EnableLoadTimeWeaving和context:load-time-weaver/ 设置自动检测。 要使用它,必须通过提供一下JVM选项启动带有Spring代理的虚拟机:

-javaagent:/path/to/spring-instrument.jar

请注意,这需要修改JVM启动脚本,这可能会妨碍你在应用服务器环境中使用此功能(取决于你的服务器和操作策略)。不过,对于单应用程序单JVM部署(如独立的Spring Boot应用程序)来说,无论如何,你通常都能控制整个JVM设置。

5.11 Further Resources

Adrian Colyer等人编写的《Eclipse AspectJ》(addison-Wesley,2005年)提供了有关AspectJ语言的全面介绍和参考资料。 强烈推荐Ramnivas Laddad所著的《AspectJ in Action》(第二版)(Manning,2009年)。这本书的重点是AspectJ,但也讨论了很多通用AOP主题(有一定深度)。

03 May 2025