面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对像的编程(OOP)。OOP中模块化的关键单元是类,而在AOP中模块化是切面。切面使关注点(例如事务管理)的模块化可以跨越多种类型和对象。(这种关注在AOP文献中通常被称为“跨领域”关注。)
Spring的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着如果你不想使用AOP,就不需要使用AOP),但AOP对Spring IoC进行了补充,提供了一个非常强大的中间件解决方案。
具有AspectJ切入点的Spring AOP
Spring提供了使用基于schema的方法或@AspectJ注解样式来编写自定义切面的简单而强大的方法。这两种样式都提供了完全类型化的建议,并使用了AspectJ切入点语言,同时仍然使用Spring AOP进行编织。
本章讨论基于
schema
和基于@AspectJ
的AOP支持。下一章将讨论较低级别的AOP支持。
AOP在Spring框架中用于:
如果你只对通用声明性服务或其他预包装的声明性中间件服务(例如池)感兴趣,则无需直接使用Spring AOP,并且可以跳过本章的大部分内容。
让我们首先定义一些主要的AOP概念和术语。这些术语不是特定于Spring的。不幸的是,AOP术语并不是特别直观。但是,如果使用Spring自己的术语,将会更加令人困惑。
@Aspect
注解(@AspectJ样式)注释的常规类来实现的。around
”,“before
”和“after
”通知。(通知类型将在后面讨论。)包括Spring在内的许多AOP框架都将通知建模为拦截器,并在连接点周围维护一系列拦截器。AspectJ
切入点表达语言。AspectJ
社区中称为类型间声明。)JDK
动态代理或CGLIB
代理。AspectJ
编译器),加载时或在运行时完成。像其他纯Java AOP框架一样,Spring AOP在运行时执行编织。Spring AOP包括以下类型的通知:
环绕通知是最通用的通知。由于Spring AOP与AspectJ
一样,提供了各种通知类型,因此我们建议你使用功能最弱的建议类型,以实现所需的行为。例如,如果你只需要使用方法的返回值更新缓存,则最好使用后置通知而不是环绕通知,尽管环绕通知可以完成相同的事情。使用最具体的通知类型可提供更简单的编程模型,并减少出错的可能性。例如,你不需要在用于环绕通知的JoinPoint
上调用proceed()
方法,因此,你不会失败。
所有通知参数都是静态类型的,因此你可以使用适当类型(例如,从方法执行返回的值的类型)而不是对象数组的 通知参数。
切入点匹配的连接点的概念是AOP的关键,它与仅提供拦截功能的旧技术不同。切入点使通知的目标独立于面向对象的层次结构。例如,你可以将提供声明性事务管理的环绕通知应用于跨越多个对象(例在服务层中的所有业务操作)的一组方法。
Spring AOP是用纯Java实现的。不需要特殊的编译过程。Spring AOP不需要控制类加载器的层次结构,因此适合在Servlet容器或应用程序服务器中使用。
Spring AOP当前仅支持方法执行连接点(通知在Spring Bean上执行方法)。尽管可以在不破坏核心Spring AOP API的情况下添加对字段拦截的支持,但并未实现字段拦截。如果需要通知字段访问和更新连接点,请考虑使用诸如AspectJ
之类的语言。
Spring AOP的AOP方法不同于大多数其他AOP框架。目的不是提供最完整的AOP实现(尽管Spring AOP相当强大)。相反,其目的是在AOP实现和Spring IoC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
因此,例如,通常将Spring Framework的AOP功能与Spring IoC容器结合使用。通过使用常规bean定义语法来配置切面(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的关键区别。使用Spring AOP不能轻松或有效地完成一些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ
是最佳选择。但是,我们的经验是,Spring AOP为AOP可以解决的企业Java应用程序中的大多数问题提供了出色的解决方案。
Spring AOP从未努力与AspectJ
竞争以提供全面的AOP解决方案。我们认为,基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ
)都是有价值的,它们是互补的,而不是竞争。Spring无缝地将Spring AOP和IoC与AspectJ
集成在一起,以在基于Spring的一致应用程序架构中支持AOP的所有功能。这种集成不会影响Spring AOP API或AOP Alliance
API。Spring AOP仍然向后兼容。请参阅下一章,以讨论Spring AOP API。
Spring框架的中心宗旨之一是非侵入性。这就是不应该强迫你将特定于框架的类和接口引入你的业务或领域模型的思想。但是,在某些地方,Spring Framework确实为你提供了将特定于Spring Framework的依赖项引入代码库的选项。提供此类选项的理由是,在某些情况下,以这种方式阅读或编码某些特定功能可能会变得更加容易。但是,Spring框架(几乎)总是为你提供选择:你可以自由地就哪个选项最适合你的特定用例或场景做出明智的决定。
与本章相关的一种选择是选择哪种AOP框架(以及哪种AOP样式)。你可以选择
AspectJ
和或Spring AOP。你也可以选择@AspectJ
注解样式方法或Spring XML配置样式方法。本章选择首先介绍@AspectJ
风格的方法,这不能表明Spring团队比Spring XML配置风格更喜欢@AspectJ
注释风格的方法(译者:使用AspectJ
编写例子不能说明Spring团队更喜欢AspectJ
注解编程)。有关每种样式的“来龙去脉”的更完整讨论,请参见选择要使用的AOP声明样式。
Spring AOP默认将标准JDK动态代理用于AOP代理。这使得可以代理任何接口(或一组接口)。
Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在某些情况下(可能极少发生),你需要通知在接口上未声明的方法,或需要将代理对象作为具体类型传递给方法,则可以强制使用CGLIB。
掌握Spring AOP是基于代理的这一事实很重要。请参阅了解AOP代理以全面了解此实现细节的实际含义。
@AspectJ
是一种将切面声明为带有注解的常规Java类的样式。@AspectJ
样式是AspectJ项目在AspectJ 5版本中引入的。Spring使用AspectJ
提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注解。但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ
编译器或编织器。
使用
AspectJ
编译器和编织器可以使用完整的AspectJ
语言,有关在Spring Applications中使用AspectJ进行了讨论。
@AspectJ
支持要在Spring配置中使用@AspectJ
切面,你需要启用Spring支持以基于@AspectJ
切面配制Spring AOP,并根据这些切面是否通知对Bean进行自动代理。通过自动代理,我们的意思是,如果Spring确定一个或多个切面通知一个bean,它会自动为该bean生成一个代理来拦截方法调用并确保按需执行通知。
可以使用XML或Java样式的配置来启用@AspectJ
支持。无论哪种情况,你都需要确保AspectJ
的Aspectjweaver.jar
库位于应用程序的类路径(版本1.8或更高版本)上。该库在AspectJ
发行版的lib
目录中或从Maven Central存储库中获取。
通过Java配置激活@AspectJ
通过Java @Configuration
启用@AspectJ
支持,请添加@EnableAspectJAutoProxy
注解,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通过XML配置激活@AspectJ
通过基于XML的配置启用@AspectJ
支持,请使用
元素,如以下示例所示:
<aop:aspectj-autoproxy/>
假定你使用基于XML Schema的配置中所述的架构支持。有关如何在aop名称空间中导入标签的信息,请参见AOP schema。
启用@AspectJ
支持后,Spring会自动检测在应用程序上下文中使用@AspectJ
切面(有@Aspect
注解)的类定义的任何bean,并用于配置Spring AOP。接下来的两个示例显示了一个不太有用的切面所需的最小定义。
两个示例中的第一个示例显示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect
注解的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
bean>
这两个示例中的第二个示例显示了NotVeryUsefulAspect
类定义,该类定义使用org.aspectj.lang.annotation.Aspect
注解进行注释;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(使用@Aspect
注解的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点、通知和引入(类型间)声明。
通过组件扫描自动检测切面
你可以将切面类注册为Spring XML配置中的常规bean,也可以通过类路径扫描自动检测它们-与其他任何Spring管理的bean一样。但是,请注意,
@Aspect
注解不足以在类路径中进行自动检测。为此,你需要添加一个单独的@Component
注解(或者,按照Spring的组件扫描程序的规则,有条件的自定义构造型注解)。向其他切面提供通知?
在Spring AOP中,切面本身不能成为其他切面的通知目标。类上的
@Aspect
注解将其标记为一个切面,因此将其从自动代理中排除。
切入点确定了感兴趣的连接点,从而使我们能够控制何时执行通知。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开发人员手册)或有关AspectJ的书籍之一(如Colyer等人的Eclipse AspectJ,或《 AspectJ in Action》 ,由Ramnivas Laddad撰写)。
支持的切入点指示符
Spring AOP支持以下在切入点表达式中使用的AspectJ
切入点指示符(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中正在执行的方法)具有给定注解的连接点。 其他切入点
完整的
AspectJ
切入点语言支持Spring不支持的其他切入点指示符:call
,get
,set
,preinitialization
,staticinitialization
,initialization
,handler
,adviceexecution
,withincode
,cflow
,cflowbelow
,if
,@this
和@withincode
(译者:意思是Spring不支持这些指示符)。在Spring AOP解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException
。Spring AOP支持的切入点指示符集合可能会在将来的版本中扩展,以支持更多的
AspectJ
切入点指示符。
由于Spring AOP仅将匹配限制为仅方法执行连接点,因此前面对切入点指示符的讨论所给出的定义比在AspectJ
编程指南中所能找到的要窄。此外,AspectJ
本身具有基于类型的语义,并且在执行连接点处,this
和target
都引用同一个对象:执行该方法的对象。Spring AOP是基于代理的系统,可区分代理对象本身(绑定到此对象)和代理背后的目标对象(绑定到目标)。
由于Spring的AOP框架基于代理的性质,因此根据定义,不会拦截目标对象内的调用。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,将拦截代理上的公共方法和受保护的方法调用(必要时甚至包可见的方法)。但是,通常应通过公共签名设计通过代理进行的常见交互。
请注意,切入点定义通常与任何拦截方法匹配。如果严格地将切入点设置为仅公开使用,即使在CGLIB代理方案中通过代理可能存在非公开交互,也需要相应地进行定义。
如果你的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用Spring驱动的本地AspectJ编织,而不是Spring的基于代理的AOP框架。这构成了具有不同特征的AOP使用模式,因此在做出决定之前一定要熟悉编织。
Spring AOP还支持其他名为bean的PCD。使用PCD,可以将连接点的匹配限制为特定的命名Spring Bean或一组命名Spring Bean(使用通配符时)。Bean PCD具有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean
标记可以是任何Spring bean的名称。提供了使用*
字符的有限通配符支持,因此,如果为Spring bean建立了一些命名约定,则可以编写bean PCD表达式来选择它们。与其他切入点指示符一样,bean PCD可以与&&
(和)、||
(或)、和!
(否定)运算符一起使用。
Bean PCD仅在Spring AOP中受支持,而在本地
AspectJ
编织中不受支持。它是AspectJ
定义的标准PCD的特定于Spring的扩展,因此不适用于@Aspect
模型中声明的切面。Bean PCD在实例级别(基于Spring bean名称概念构建)上运行,而不是仅在类型级别(基于编织的AOP受其限制)上运行。基于实例的切入点指示符是Spring基于代理的AOP框架的特殊功能,并且与Spring bean工厂紧密集成,因此可以自然而直接地通过名称识别特定bean。
组合切入点表达式
你可以使用&&
、||
和!
组合切入点表达式。你还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} //1
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {} //2
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //3
anyPublicOperation
匹配。inTrading
匹配。tradingOperation
匹配。最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。按名称引用切入点时,将应用常规的Java可见性规则(你可以看到相同类型的private切入点,层次结构中protected的切入点,任何位置的public切入点,等等)。可见性不影响切入点匹配。
共享通用切入点定义
在企业级应用中,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议为此定义一个“ SystemArchitecture”切面,以捕获常见的切入点表达式意图。这样的切面通常类似于以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
你可以在需要切入点表达式的任何地方引用在此切面定义的切入点。例如,要使服务层具有事务性,你可以编写以下内容:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
tx:attributes>
tx:advice>
在基于schema的AOP支持中讨论了
和
元素。事务管理中讨论了事务元素。
实例
Spring AOP用户可能最常使用execution
切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(前面的代码片段中的ret-type-pattern
),名称模式(name-pattern
)和参数模式(param-pattern
)以外的所有部分都是可选的。返回类型模式确定要匹配连接点、方法的返回类型必须是什么。最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,标准类型名称才匹配。名称模式与方法名称匹配。你可以将通配*
符用作名称模式的全部或一部分。如果你指定了声明类型模式,请在其后加上.
将其加入名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而(..)
匹配任意数量(零个或多个)的参数。(*)
模式与采用任何类型的一个参数的方法匹配。(*,String)
与采用两个参数的方法匹配。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请查阅AspectJ编程指南的“语言语义”部分。
以下示例显示了一些常用的切入点表达式:
任何公共方法的执行:
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”通常以绑定形式使用。有关如何使目标对象在建议正文中可用的信息,请参见“声明通知”部分。
任何采用单个参数并且在运行时传递的参数为Serializable
的连接点(仅在Spring AOP中执行方法):
args(java.io.Serializable)
“ args”通常以绑定形式使用。有关如何使方法参数在通知正文中可用的信息,请参见“声明通知”部分。
请注意,此示例中给出的切入点与execution(* *(java.io.Serializable))不同。如果在运行时传递的参数为Serializable,则args版本匹配;如果方法签名声明一个类型为Serializable的参数,则执行版本匹配。
目标对象具有@Transactional
注解的任何连接点(仅在Spring AOP中方法执行):
@target(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用“ @target”。有关如何使注解对象在建议正文中可用的信息,请参见“声明通知”部分。
目标对象的声明类型具有@Transactional
注解的任何连接点(仅在Spring AOP中方法执行):
@within(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用“ @within”。有关如何使注解对象在通知正文中可用的信息,请参见“声明通知”部分。
任何执行方法带有@Transactional
注解的连接点(仅在Spring AOP中是方法执行):
@annotation(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用“ @annotation”。有关如何使注解对象在通知正文中可用的信息,请参见“声明通知”部分。
任何采用单个参数的连接点(仅在Spring AOP中是方法执行),并且传递的参数的运行时类型具有@Classified
注解:
@args(com.xyz.security.Classified)
你也可以在绑定形式中使用“ @args”。请参阅“声明通知”部分,如何使通知对象中的注解对象可用。
名为tradeService
的Spring bean上的任何连接点(仅在Spring AOP中执行方法):
bean(tradeService)
Spring Bean上具有与通配符表达式* Service
匹配的名称的任何连接点(仅在Spring AOP中才执行方法):
bean(*Service)
写一个好的连接点
在编译期间,AspectJ
处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个耗时的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行了测试以确定在运行代码时是否存在实际匹配)。首次遇到切入点声明时,AspectJ
将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜(消耗最小)的组件。这意味着你不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
但是,AspectJ
只能使用所告诉的内容。为了获得最佳的匹配性能,你应该考虑他们试图达到的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类之一:同类、作用域和上下文:
Kinded
指示器选择特定类型的连接点:execution
、 get
、 set
、call
和 handler
。
Scoping
指示器选择一组感兴趣的连接点(可能是多种类型的):within
和withincode
Contextual
指示符根据上下文匹配(和可选绑定):this
、target
和@annotation
编写正确的切入点至少应包括前两种类型(Kinded
和Scoping
)。你可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在通知中使用。仅提供Kinded
的标识符或仅提供Contextual
的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。Scoping
指定符的匹配非常快,使用它们意味着AspectJ
可以非常迅速地消除不应进一步处理的连接点组。一个好的切入点应尽可能包括一个切入点。
参考代码:
com.liyong.ioccontainer.starter.AopIocContiner
通知与切入点表达式关联,并且在切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是在适当位置声明的切入点表达式。
前置通知
你可以使用@Before
注解在一个切面中声明前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
返回通知
在当匹配方法正常的执行返回时,返回通知运行。你可以使用@AfterReturning
注解进行声明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
你可以在同一切面内拥有多个通知声明(以及其他成员)。在这些示例中,我们仅显示单个通知声明,以及其中每个通知的效果。
有时,你需要在通知正文中访问返回的实际值。你可以使用@AfterReturning
的形式绑定返回值以获取该访问,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
返回属性中使用的名称必须与advice
方法中的参数名称相对应。当方法执行返回时,返回值将作为相应的参数值传递到通知方法。returning
也将匹配限制为仅返回指定类型值的方法执行(在这种情况下为Object,它匹配任何返回值)。
请注意,当使用返回后通知时,不可能返回完全不同的引用。
异常后置通知
在抛异常通知后,当匹配的方法执行通过抛出异常退出时运行。你可以使用@AfterThrowing
注解进行声明,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,你希望通知仅在引发给定类型的异常时才运行,并且你通常还需要访问通知正文中的引发异常。你可以使用throwing
属性来限制匹配(如果需要)(否则,请使用Throwable
作为异常类型),并将抛出的异常绑定到通知的参数。以下示例显示了如何执行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing
属性中使用的名称必须与通知方法中的参数名称相对应。当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知的方法。throwing
还将匹配仅限制为抛出指定类型的异常(在这种情况下为DataAccessException
)的方法执行。
最终通知
当匹配的方法执行退出时,通知(最终)运行。通过使用@After
注解声明它。之后必须准备处理正常和异常返回条件的通知。它通常用于释放资源和类似目的。以下示例显示了最终通知的用法:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
环绕通知
最后一种通知是环绕通知。环绕通知在匹配方法的执行过程中“环绕”运行。它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真的执行方法。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用环绕通知。始终使用能力最小的通知来满足你的要求(也就是说,在通知可以使前置通知时,请勿用环绕通知)。
通过使用@Around
注解来声明环绕通知。通知方法的第一个参数必须是ProceedingJoinPoint
类型。在通知的正文中,在ProceedingJoinPoint
上调用proceed()
会使底层(真正的执行方法)方法执行。proceed
方法也可以传入Object []
。数组中的值用作方法执行时的参数。
当用
Object []
进行调用时,proceed
的行为与AspectJ
编译器所编译的around
通知的proceed
为略有不同。对于使用传统AspectJ
语言编写的环绕通知,传递给proceed
的参数数量必须与传递给环绕通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。Spring采取的方法更简单,并且更适合其基于代理的,仅执行的语义。如果你编译为Spring编写的@AspectJ
切面,并在AspectJ
编译器和weaver
中使用参数进行处理,则只需要意识到这种区别。有一种方法可以在Spring AOP和AspectJ
之间100%兼容,并且在下面有关通知参数的部分中对此进行了讨论。
以下示例显示了如何使用环绕通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
环绕通知返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存切面有一个值,则它可以从缓存中返回一个值,如果没有,则调用proceed()
。请注意,在环绕通知的正文中,proceed
可能被调用一次,多次或完全不被调用。所有这些都是合法的。
参考代码:
com.liyong.ioccontainer.starter.AopIocContiner
通知参数
Spring提供了完全类型化的通知,这意味着你可以在通知签名中声明所需的参数(如我们先前在返回和抛出示例中所看到的),而不是一直使用Object []
数组。我们将在本节的后面部分介绍如何使参数和其他上下文值可用于通知主体。首先,我们看一下如何编写通用通知,以了解该通知当前通知的方法。
获取当前JoinPoint
任何通知方法都可以将org.aspectj.lang.JoinPoint
类型的参数声明为其第一个参数。请注意,环绕通知声明ProceedingJoinPoint
类型为第一个参数,该参数是JoinPoint
的子类。JoinPoint
接口提供了许多有用的方法:
getArgs()
: 返回方法参数。getThis()
: 返回代理对象。getTarget()
: 返回目标对象。getSignature()
: 返回通知使用的方法的描述。toString()
: 打印有关所有通知方法的有用描述。有关更多详细信息,请参见javadoc。
传递参数给通知
我们已经看到了如何绑定返回的值或异常值(在返回之后和引发通知之后)。要使参数值可用于通知正文,可以使用args
的绑定形式。如果在args
表达式中使用参数名称代替类型名称,则在调用通知时会将相应参数的值作为参数值传递。一个例子应该使这一点更清楚。假设你要通知以Account
对象作为第一个参数的DAO
操作的执行,并且你需要在通知正文中访问该帐户。你可以编写以下内容:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入点表达式的args(account,..)
部分有两个用途。首先,它将匹配限制为仅方法采用至少一个参数且传递给该参数的参数为Account
实例的那些方法执行。其次,它通过account
参数使实际的Account
对象可用于通知。
写这个的另一种方法是声明一个切入点,当它匹配一个连接点时提供Account
对象值,然后从通知中引用命名的切入点。如下所示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
有关更多详细信息,请参见AspectJ
编程指南。
代理对象(this
)、目标对象(target
)和注解(@within
,@target
,@annotation
和@args
)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用@Auditable
注解的方法的执行并提取审计代码:
这两个示例中的第一个显示了@Auditable
注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
这两个示例中的第二个示例显示了与@Auditable
方法的执行相匹配的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知参数和泛型
Spring AOP可以处理类声明和方法参数中使用的泛型。假设你具有如下通用类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
你可以通过在要拦截方法的参数类型中键入advice参数,将方法类型的拦截限制为某些参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
这种方法不适用于泛型集合。因此,你不能按以下方式定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
为了使这项工作有效,我们将不得不检查集合的每个元素,这是不合理的,因为我们也无法决定通常如何处理null
。要实现类似的目的,你必须将参数键入Collection
并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。
通过Java反射无法获得参数名称,因此Spring AOP使用以下策略来确定参数名称:
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
}
使用’argNames
‘属性有点笨拙,因此,如果未指定’argNames
'属性,Spring AOP将查找该类的调试信息,并尝试从局部变量表中确定参数名称。只要已使用调试信息(至少是“ -g:vars”)编译了类,此信息就会存在。 启用此标志时进行编译的后果是:(1)你的代码稍微易于理解(逆向工程),(2)类文件的大小略大(通常无关紧要),(3)编译器未应用删除未使用的局部变量的优化。换句话说,通过启用该标志,你应该不会遇到任何困难。
如果即使没有调试信息,
AspectJ
编译器(ajc)都已编译@AspectJ
切面,则无需添加argNames
属性,因为编译器会保留所需的信息。
如果在没有必要调试信息的情况下编译了代码,Spring AOP将尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且advice方法仅接受一个参数,则配对很明显)。如果在给定可用信息的情况下变量的绑定不明确,则抛出AmbiguousBindingException
。
如果以上所有策略均失败,则抛出IllegalArgumentException
。
proceed参数
前面我们提到过,我们将描述如何编写一个在Spring AOP和AspectJ
中始终有效的参数的proceed
调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:
@Around("execution(List find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在许多情况下,无论如何都要进行此绑定(如上例所示)。
通知顺序
当多条通知都希望在同一连接点上运行时会发生什么? Spring AOP遵循与AspectJ
相同的优先级规则来确定通知执行的顺序。优先级最高的通知在进入时首先运行(因此,给定两个before
通知,优先级最高的通知首先运行)。在从连接点出来的过程中,优先级最高的通知最后运行(因此,给定两个after
通知,优先级最高的通知将排在第二)。
在不同切面定义的两个通知都需要在同一个连接点上运行时,除非另行指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。通过在切面类中实现org.springframework.core.Ordered
接口或使用Order注解对其进行注解,可以通过常规的Spring方法来完成。给定两个切面,从Ordered.getValue()
(或注解值)返回较低值的切面具有较高的优先级
当在同一个切面中定义的两个通知都需要在同一个连接点上运行时,顺序是未定义的(因为无法通过java编译类的反射检索声明顺序)。考虑将此类通知方法分解为每个切面类中的每个连接点的一个通知方法,或者将通知片段重构为可以在切面级别排序的单独切面类。
引入(在AspectJ中称为类型间声明)使能够声明已通知的对象实现给定接口,并代表这些对象提供该接口的实现。
你可以使用@DeclareParents
注解进行介绍。此注解用于声明匹配类型具有新的父类(因此具有名称)。例如,给定一个名为UsageTracked
的接口和该接口的一个名为DefaultUsageTracked
的实现,下面的切面声明了服务接口的所有实现者也实现了UsageTracked
接口(例如通过JMX
公开统计信息):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由带注释的字段的类型确定。@DeclareParents
注解的value
属性是AspectJ
类型的模式。匹配类型的任何bean都实现UsageTracked
接口。注意,在前面示例的before
通知中,服务bean可以直接用作UsageTracked
接口的实现。如果以编程方式访问bean,则应编写以下内容:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
参考代码:
com.liyong.ioccontainer.starter.AopDeclareParentsIocContiner
这是一个高级主题。如果你刚开始使用AOP,则可以放心地跳过它,直到以后。
默认情况下,应用程序上下文中每个切面都有一个实例。 AspectJ将此称为单例实例化模型。可以使用bean生命周期来定义切面。Spring支持AspectJ的perthis
和pertarget
实例化模型(当前不支持percflow
,percflowbelow
和pertypewithin
)。
你可以通过在@Aspect
注解中指定perthis
来声明perthis
切面。考虑以下示例:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,“ perthis
”子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的连接点绑定到“ this
”的唯一对象)创建一个切面实例。切面实例是在服务对象上首次调用方法时创建的。当服务对象超出范围时,切面将超出范围。在创建切面实例之前,其中的任何通知都不会执行。一旦创建了切面实例,在其中声明的通知就会在匹配的连接点上执行,但仅当服务对象与此切面相关联时才执行。有关每个子句的更多信息,请参见AspectJ编程指南。
pertarget
实例化模型的工作方式与perthis
完全相同,但是它在匹配的连接点为每个唯一目标对象创建一个切面实例。
现在你已经了解了所有组成部分是如何工作的,我们可以将它们组合在一起做一些有用的事情。
有时由于并发问题(例如,死锁失败),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作,以避免客户端看到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.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
请注意,切面实现了Ordered
接口,以便我们可以将切面的优先级设置为高于事务通知的优先级(每次重试时都需要一个新的事务)。maxRetries
和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>
为了完善切面,使其仅重试幂等操作,我们可以定义以下幂等注解:
@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 {
// ...
}
如果你更喜欢基于XML的格式,Spring还提供了使用新的aop名称空间标签定义切面的支持。支持与使用@AspectJ
样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点放在新语法上,并使读者参考上一节中的讨论(@AspectJ
支持),以了解编写切入点表达式和通知参数的绑定。
要使用本节中描述的aop名称空间标签,你需要导入spring-aop
模式,如基于XML Schema的配置中所述。有关如何在aop名称空间中导入标签的信息,请参见AOP schema。
在你的Spring配置中,所有切面和advisor
元素都必须放在
元素内(在应用程序上下文配置中可以有多个
元素)。
元素可以包含切入点,advisor
和aspect
元素(请注意,必须按此顺序声明它们)。
的配置样式大量使用了Spring的自动代理机制。如果你已经通过
BeanNameAutoProxyCreator
或类似的工具使用了显式的自动代理,那么这可能会导致一些问题(比如没有编织通知)。推荐的用法模式是仅使用样式或仅使用
AutoProxyCreator
样式并且不要混合使用。
使用schema
支持时,切面是在Spring应用程序上下文中定义为Bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和通知信息在XML中捕获。
你可以通过使用< aop:aspect>
元素来声明一个切面,并通过使用ref
属性来引用后台bean,如下面的示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
aop:aspect>
aop:config>
<bean id="aBean" class="...">
...
bean>
支持切面的bean(在本例中为aBean
)当然可以像配置其他Spring bean一样进行配置并注入依赖项。
你可以在
元素内声明一个命名的切入点,让切入点定义多个切面和advisors
之间共享。
可以定义代表服务层中任何业务服务的执行的切入点:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
aop:config>
请注意,切入点表达式本身使用的是@AspectJ
支持中所述的AspectJ切入点表达式语言。如果使用基于schema的声明样式,则可以引用在切入点表达式中的类型(@Aspects
)中定义的命名切入点。定义上述切入点的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
aop:config>
假定你具有“共享通用切入点定义”中所述的SystemArchitecture
方面。
然后,在切面内声明切入点与声明顶级切入点非常相似,如以下示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
aop:aspect>
aop:config>
与@AspectJ
切面几乎相同,通过使用基于schema的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集此对象作为连接点上下文,并将其传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
aop:aspect>
aop:config>
必须声明该通知以通过包含匹配名称的参数来接收收集的连接点上下文,如下所示:
public void monitor(Object service) {
// ...
}
在组合切入点子表达式时,&&
在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引用,并且不能用作命名切入点以形成复合切入点。因此,基于schema的定义样式中的命名切入点支持比@AspectJ
样式所提供的更受限制。
基于schema的AOP支持使用与@AspectJ
样式相同的五种通知,并且它们具有完全相同的语义。
前置通知
在执行匹配的方法之前,先运行通知。使用
元素在
中声明它,如以下示例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
aop:aspect>
在这里,dataAccessOperation
是在最高(
)级别定义的切入点ID。要定义内联切入点,请使用以下方法将pointcut-ref
属性替换为pointcut
属性:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
aop:aspect>
正如我们在@AspectJ
样式的讨论中所指出的那样,使用命名的切入点可以显着提高代码的可读性。method
属性标识提供通知正文的方法(doAccessCheck
)。必须为包含通知的Aspect元素所引用的bean定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用方面bean上的doAccessCheck
方法。
返回后通知
返回的通知在匹配的方法执行正常完成时运行。它以与前置通知相同的方式在
中声明。以下示例显示了如何声明它:
<aop:aspect 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
元素在
中声明它,如以下示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
aop:aspect>
与@AspectJ
样式一样,你可以在通知正文中获取引发的异常。为此,请使用throwing
属性指定异常应传递到的参数的名称,如以下示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
aop:aspect>
doRecoveryActions
方法必须声明一个名为dataAccessEx
的参数。该参数的类型以与@AfterThrowing
中所述相同的方式约束匹配。例如,方法签名可以声明如下:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
最终通知
无论最终如何执行匹配的方法,最终通知都会运行。你可以使用after
元素声明它,如以下示例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
aop:aspect>
环绕通知
最后一种通知是环绕通知。环绕通知在匹配的方法执行“环绕”运行。它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真的执行方法。环绕通知通常用于以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态。总是使用最弱的形式的通知来满足你的要求(译者:最小范围使用)。不要使用环绕的通知,如果前置通知可以做的工作。
你可以使用
元素声明环绕通知。通知方法的第一个参数必须是ProceedingJoinPoint
类型。在通知的正文中,在ProceedingJoinPoint
上调用proceed()
会使底层方法执行。还可以使用Object []
调用proceed
方法。数组中的值用作方法执行时的参数。有关调用Object []
的注意事项,请参见“环绕通知”。以下示例显示了如何在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;
}
通知参数
基于schema的声明样式以与@AspectJ
支持相同的方式支持完全类型的通知,即通过名称与通知方法参数匹配切入点参数来实现。有关详细信息,请参见通知参数。如果你希望显式指定通知方法的参数名称(不依赖于先前描述的检测策略,则可以通过使用advice元素的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 DefaultFooService implements FooService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
接下来是切面。请注意profile(..)
方法接受许多强类型参数的事实,其中第一个恰好是用于进行方法调用的连接点。此参数的存在表明profile(..)
将用作通知,如以下示例所示:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
最后,以下示例XML配置影响了特定连接点的上述通知的执行:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<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)
通知顺序
当需要在同一连接点(执行方法)上执行多个通知时,排序规则如“通知顺序”中所述。切面之间的优先级是通过将Order
注解添加到支持切面的Bean或通过使Bean实现Ordered
接口来确定的。
简介(在AspectJ中称为类型间声明)使切面可以声明通知的对象实现给定的接口,并代表那些对象提供该接口的实现。
你可以通过在
中使用
元素进行引入。你可以使用
元素声明匹配类型具有新的父类(因此而得名)。例如,给定一个名为UsageTracked
的接口和该接口名为DefaultUsageTracked
的实现,以下切面声明服务接口的所有实现者也都实现UsageTracked
接口。(例如,为了通过JMX公开统计信息。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.SystemArchitecture.businessService()
and this(usageTracked)"
method="recordUsage"/>
aop:aspect>
支持usageTracking
bean的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
要实现的接口由Implement-interface
属性确定。类型匹配属性的值是AspectJ类型模式。匹配类型的任何bean都实现UsageTracked
接口。请注意,在前面示例的之前通知中,服务Bean可以直接用作UsageTracked
接口的实现。要以编程方式访问bean,可以编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
模式定义切面唯一受支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。
`
advisors
的概念来自于Spring中定义的AOP支持,在AspectJ中没有直接的对等物。advisors
就像一个小的自包含的切面,只有一条通知。通知本身由bean表示,并且必须实现Spring的“通知类型”中描述的通知接口之一。advisors
可以利用AspectJ切入点表达式。
Spring通过
元素支持advisors
概念。你通常会看到它与事务通知结合使用,事务通知在Spring中也有其自己的名称空间支持。以下示例显示advisors
:
<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
属性内联定义一个pointcut
表达式。
要定义advisor
的优先级以便通知可以参与排序,可以使用order
属性来定义advisor
的排序值。
本节将展示AOP示例中的并发锁定失败重试示例在使用模式支持重写时的例子。
有时由于并发问题(例如,死锁失败者),业务服务的执行可能会失败。如果重试该操作,则很可能在下一次尝试中成功。对于适合在这种情况下重试的业务(不需要为解决冲突而需要返回给用户幂等操作),我们希望透明地重试该操作,以避免客户端看到PessimisticLockingFailureException
。这项要求明确地跨越了服务层中的多个服务,因此非常适合通过一个切面实现。
因为我们想重试该操作,所以我们需要使用周围建议,以便可以多次调用proceed
。下面的清单显示了基本的切面实现(它是一个使用schema支持的常规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
异常失败了,我们会再次尝试,除非我们已经耗尽了所有的重试尝试。
该类与
@AspectJ
示例中使用的类相同,但是除去了注解。
相应的Spring配置如下:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
aop:aspect>
aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
bean>
请注意,目前我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以改进切面,以便通过引入等幂注解并使用注解来注释服务操作的实现,使其仅重试真正的幂等操作,如以下示例所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
切面更改为仅重试幂等操作涉及更改切入点表达式,以便仅@Idempotent
操作匹配,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
一旦确定切面是实现给定需求的最佳方法,你如何在使用Spring AOP或AspectJ以及在Aspect语言(代码)样式,@AspectJ
注解样式或Spring XML样式之间做出选择?这些决定受许多因素影响、包括应用程序需求、开发工具和团队对AOP的熟悉程度。
使用最简单的方法即可。 Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/编织器。如果你只需要通知在Spring bean上执行操作,则Spring AOP是正确的选择。如果你需要通知不受Spring容器管理的对象(通常是领域对象),则需要使用AspectJ。如果你希望通知除简单方法执行之外的连接点(例如,字段get或set连接点等),则还需要使用AspectJ (译者:对字段属性进行通知使用AspectJ )。
使用AspectJ时,可以选择AspectJ语言语法(也称为“代码样式”)或@AspectJ
注解样式。显然,如果你不使用Java 5+,则已经为你做出了选择:使用代码样式。如果切面在你的设计中起着重要作用,并且你能够使用Eclipse的AspectJ开发工具(AJDT)插件,则AspectJ语言语法是首选。它更干净、更简单,因为该语言是专为编写切面而设计的。如果你不使用Eclipse或只有少数几个切面在你的应用程序中,那么你可能要考虑使用@AspectJ
样式,在IDE中坚持常规Java编译,并向其中添加切面编织阶段你的构建脚本。
@AspectJ
或 XML
选择?如果你选择使用Spring AOP,则可以选择@AspectJ
或XML
样式。有各种折衷考虑。
XML样式可能是现有Spring用户最熟悉的,并且得到了真正的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样式中,你可以声明前两个切入点:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
XML方法的缺点是你无法通过组合这些定义来定义accountPropertyAccess
切入点(译者:不能组合多个切面)。
@AspectJ
样式支持其他实例化模型和更丰富的切入点组合。它具有将切面保持为模块化单元的优势。它还具有的优点是,Spring AOP和AspectJ都可以理解@AspectJ
方面。因此,如果你以后决定需要AspectJ的功能来实现其他要求,则可以轻松地迁类到AspectJ设置。总而言之,Spring团队在自定义方面更喜欢@AspectJ
样式,而不是简单地配置企业服务。
通过使用自动代理支持,模式定义的
切面、
声明的advisors,甚至是同一配置中其他样式的代理和拦截器,完全可以混合@AspectJ
样式的切面。所有这些都是通过使用相同的底层支持机制实现的,并且可以毫无困难地共存。
Spring AOP使用JDK动态代理或CGLIB创建给定目标对象的代理。JDK动态代理内置在JDK中,而CGLIB是常见的开源类定义库(重新包装到spring-core
中)。
如果要代理的目标对象实现至少一个接口,则使用JDK动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建CGLIB代理。
如果要强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),可以这样做。但是,你应该考虑以下问题:
final
方法,因为不能在运行时生成的子类中覆盖它们。Objenesis
创建的,因此不再调用代理对象的构造函数两次。只有在你的JVM不允许绕过构造函数的情况下,你才可能从Spring的AOP支持中得到两次调用和相应的调试日志条目。要强制使用CGLIB代理,请将
元素的proxy-target-class
属性的值设置为true,如下所示:
<aop:config proxy-target-class="true">
aop:config>
要在使用@AspectJ
自动代理支持时强制CGLIB代理,请将
元素的proxy-target-class
属性设置为true
,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
多个
部分在运行时折叠到一个统一的自动代理创建器中,该创建器将应用任何
部分(通常来自不同的XML bean定义文件)指定的最强的代理设置。这也适用于
和
元素。
为了清楚起见,在
、
或
元素上使用
proxy-target-class =“true”
会强制对所有三个元素使用CGLIB代理其中。
Spring AOP是基于代理的。在编写自己的切面或使用Spring Framework随附的任何基于Spring AOP的切面之前,掌握最后一条语句实际含义的语义至关重要。
首先考虑以下情况:你有一个普通的、未经代理的、无特殊要求的直接对像引用,如以下代码片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// 调用当前对象的bar方法
this.bar();
}
public void bar() {
// some logic...
}
}
如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图清单所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utaKui3h-1597633914610)(https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/images/aop-proxy-plain-pojo-call.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-2qP9hMNL-1597633914611)(https://docs.spring.io/spring/docs/5.2.6.RELEASE/spring-framework-reference/images/aop-proxy-call.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
引用而不是代理进行调用。这具有重要意义。这意味着自调用不会导致与方法调用相关的通知得到执行的机会(译者:通过代理调用才能出发相关的通知)。
Okay,那么该怎么办?最佳方法(在这里宽松地使用术语“最佳”)是重构代码,以免发生自调用。这确实需要你做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。你可以(这对我们来说很痛苦)将类中的逻辑完全绑定到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框架。
@AspectJ
代理除了通过使用
或
声明配置中的各个切面外,还可以通过编程方式创建通知目标对象的代理。有关Spring的AOP API的完整详细信息,请参阅下一章。在这里,我们要重点介绍使用@AspectJ
切面自动创建代理的功能。
你可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory
类为一个或多个@AspectJ
切面通知的目标对象创建代理。此类的基本用法非常简单,如以下示例所示:
// 创建一个工厂,这个工厂可以为给定对象生成代理
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// 增加切面,这个切面必须被标注@AspectJ注解
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// 添加存在的切面,这个切面对象背心支持@AspectJ
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
有关更多信息,请参见javadoc。
到目前为止,本章介绍的所有内容都是纯Spring AOP。在本节中,我们将研究如果你的需求超出了Spring AOP所提供的功能,那么如何使用AspectJ编译器或weaver代替Spring AOP或除Spring AOP之外使用。
Spring附带了一个小的AspectJ切面库,该库在你的发行版中可以作为spring-aspects.jar
独立使用。你需要将其添加到类路径中才能使用其中的切面。使用AspectJ依赖于Spring和AspectJ的其他Spring切面来注入域对象以及AspectJ讨论该库的内容以及如何使用它。使用Spring IoC配置AspectJ切面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ切面。最后,Spring Framework中使用AspectJ进行的加载时编织为使用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)分别按类型或名称进行自动装配。或者,最好在字段或方法级别通过@Autowired
或@Inject
为@Configurable
bean指定显式的,注解驱动的依赖项注入(有关更多详细信息,请参见基于注释的容器配置)。最后,你可以使用dependencyCheck
属性(例如,@Configurable(autowire = Autowire.BY_NAME,dependencyCheck = true)
)为新创建和配置的对象中的对象引用启用Spring依赖检查。如果该属性设置为true
,则Spring在配置后验证所有属性(不是原生类型或集合)是否已经设置。
请注意,单独使用注解不会执行任何操作。spring-aspects.jar
中的AnnotationBeanConfigurerAspect
会对注解的存在起作用。本质上,切面的意思是,在初始化一个带有@Configurable注解的类型的新对象之后,使用Spring根据注解的属性配置新创建的对象。在这种情况下,“初始化
”是指新实例化的对象(例如,用new运算符实例新的对象)以及正在进行反序列化(例如,通过readResolve()
的可序列化的对象)。
上述段落中的一个关键短语是in essence(本质)。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。如果你希望在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在
@Configurable
声明中对此进行定义,如下所示:@Configurable(preConstruction = true)
你可以在《 AspectJ编程指南》的此附录中找到有关各种切入点类型的语言语义的更多信息。
为此,必须将带注解的类型与AspectJ编织器编织在一起。你可以使用构建时的Ant或Maven任务来执行此操作(例如,参见《 AspectJ开发环境指南》),也可以使用加载时编织(请参见Spring Framework中的使用AspectJ进行加载时编织)。Spring需要配置AnnotationBeanConfigurerAspect
自身(以便获得对将用于配置新对象Bean工厂的引用)。如果使用基于Java的配置,则可以将@EnableSpringConfigured
添加到任何@Configuration
类中,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果你更喜欢基于XML的配置,则Spring context namespace定义了一个方便的context:spring-configured
元素,你可以按以下方式使用它:
<context:spring-configured/>
在配置切面之前创建的@Configurable
对象实例导致向调试日志发出消息,并且不进行对象配置。一个例子可能是Spring配置中的一个bean,当它由Spring初始化时会创建域对象。在这种情况下,你可以使用depends-on
bean属性来手动指定bean依赖于配置切面。下面的示例显示如何使用depends-on
属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
bean>
除非你真的想在运行时依赖它的语义,否则不要通过bean configurer方面激活
@Configurable
处理。特别是,请确保不要在通过容器注册为常规Spring bean的bean类上使用@Configurable
。这样做会导致两次初始化,一次是通过容器,一次是通过切面。
单元测试@Configurable
对象
@Configurable
支持的目标之一是实现领域对象的独立单元测试,而不会需要复杂的硬编码查找。如果AspectJ尚未编织@Configurable
类型,则注解在单元测试期间不起作用。你可以在被测对象中设置mock
或stub
属性引用,然后照常进行。如果AspectJ编织了@Configurable
类型,你仍然可以像往常一样在容器外部进行单元测试,但是每次构造@Configurable
对象时,你都会看到一条警告消息,指示该对象尚未由Spring配置。
使用多个应用程序上下文
用于实现@Configurable
支持的AnnotationBeanConfigurerAspect
是AspectJ单例切面。单例切面的范围与静态成员的范围相同:每个类加载器都有一个切面实例定义类型。这意味着,如果你在同一个类加载器层次结构中定义多个应用程序上下文则需要考虑在何处定义@EnableSpringConfigured
bean,以及在哪里将spring-aspects.jar
放置在类路径上。
考虑一个典型的Spring Web应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务、支持那些服务所需的一切、以及每个Servlet的一个子应用程序上下文(其中包含该Servlet的特定定义)。所有这些上下文共存于同一类加载器层次结构中,因此AnnotationBeanConfigurerAspect
只能保存对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured
bean。这定义了你可能想注入领域对象的服务。结果是,你无法使用@Configurable
机制来配置领域对象,该领域对象引用的是在子(特定于servlet的)上下文中定义的Bean的引用(无论如何,这可能不是你想要做的)。
在同一容器中部署多个Web应用程序时,请确保每个Web应用程序通过使用其自己的类加载器(例如,将spring-aspects.jar
放置在“ WEB-INF / lib
”中)将其类型加载到spring-aspects.jar
中。如果将spring-aspects.jar
仅添加到容器级的类路径中(并因此由共享的父类加载器加载),则所有Web应用程序都共享相同的切面实例(可能不是你想要的)。
除了@Configurable
切面之外,spring-aspects.jar
还包含一个AspectJ切面,你可以使用该切面来驱动Spring的事务管理,以使用@Transactional
注解来注释类型和方法。这主要适用于希望在Spring容器之外使用Spring Framework的事务支持的用户。
解析@Transactional
注解的切面是AnnotationTransactionAspect
。使用此切面时,必须注解实现类(或该类中的方法或两者),而不是注释实现类所实现的接口(如果有)。AspectJ遵循Java的规则,即不继承接口上的注解。
类上的@Transactional
注解指定用于执行该类中任何公共操作的默认事务语义。可以注解任何可见性的方法,包括私有方法。直接注解非公共方法是执行此类方法而获得事务划分的唯一方法。
从Spring Framework 4.2开始,
spring-aspects
提供了一个相似的切面,为标准javax.transaction.Transactional
注解提供了完全相同的功能。检查JtaAnnotationTransactionAspect
了解更多详细信息。
对于希望使用Spring配置和事务管理支持但又不想(或不能)使用注解的AspectJ编程者,spring-aspects.jar
也包含抽象切面,你可以扩展它们以提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspect
和AbstractTransactionAspect
切面的资源。作为示例,以下摘录显示了如何编写切面来使用与完全限定的类名匹配的类型bean定义来配置域模型中定义的对象的所有实例:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
SystemArchitecture.inDomainModel() &&
this(beanInstance);
}
当你将AspectJ切面与Spring应用程序一起使用时,既自然又希望能够使用Spring配置置这些切面。AspectJ运行时本身负责切面的创建,并且通过Spring配置AspectJ创建的切面的方法取决于切面所使用的AspectJ实例化模型(per-xxx子句)。
AspectJ的大多数切面都是单例切面。这些切面的配置很容易。你可以创建一个bean定义,该bean定义按常规引用切面类型,并包括factory-method =“aspectOf”
bean属性。这样可以确保Spring通过向AspectJ获取实例,而不是尝试自己创建实例。以下示例显示如何使用factory-method =“aspectOf”
属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf"> //1
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
bean>
factory-method =“aspectOf”
属性非单例切面更难配置。但是,可以通过创建原型Bean定义并使用spring-aspects.jar
中的@Configurable
支持来实现,一旦它们由AspectJ运行时创建了Bean,就可以配置切面实例。
如果你有一些要与AspectJ编织的@AspectJ
切面(例如,对域模型类型使用加载时编织)以及要与Spring AOP一起使用的其他@AspectJ
切面,那么这些切面都已在Spring中配置,你需要告诉Spring AOP @AspectJ
自动代理支持,应使用配置中定义的@AspectJ
切面的确切子集进行自动代理。你可以通过在
声明中使用一个或多个
元素来做到这一点。每个
元素都指定一个名称模式,只有名称与至少一个模式匹配的bean才可用于Spring AOP自动代理配置。以下示例显示了如何使用
元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
aop:aspectj-autoproxy>
不要被
元素的名称所迷惑。使用它可以创建Spring AOP代理。这里使用的是
@AspectJ
样式的切面声明,但不涉及AspectJ运行时。
加载时编织(LTW)是指在将AspectJ切面加载到应用程序的类文件中时将其编织到Java虚拟机(JVM)中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是LTW的一般介绍。有关LTW的详细信息以及仅使用AspectJ配置LTW(完全不涉及Spring)的详细信息请参阅《 AspectJ开发环境指南》的LTW部分。Spring框架为AspectJ LTW带来的价值在于能够对编织过程进行更精细的控制。“ Vanilla” AspectJ LTW通过使用Java(5+)代理来实现,该代理在启动JVM时通过指定VM参数来切换。因此,它是一个JVM范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用spring的LTW允许你在每个类加载器的基础上打开LTW,这是更细粒度的,在“单jvm -多应用程序”环境中更有意义(比如在典型的应用程序服务器环境中)。
此外,在某些环境中,此支持激活装载时编织不需要应用程序服务器的启动脚本进行任何修改,也不需要添加-javaagent:path/to/aspectjweaver.jar
(如本节稍后所述)或-javaagent:path/to/spring-instrument.jar
。开发人员将应用程序上下文配置为启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。
现在介绍结束了,让我们首先浏览一个使用Spring的AspectJ LTW的快速示例,然后详细介绍示例中引入的元素。有关完整的示例,请参见Petclinic示例应用程序。
第一个例子
假设你是一位负责诊断系统中某些性能问题的原因的应用程序开发人员。我们将打开一个简单的配置切面,而不是使用配置文件工具,使我们能够快速获取一些性能指标。然后,我们可以立即在该特定区域应用更细粒度的分析工具。
此处提供的示例使用XML配置。你还可以配置
@AspectJ
并将其与Java配置一起使用。具体来说,你可以使用@EnableLoadTimeWeaving
注解替代(有关详细信息,请参见下文)。
下面的示例显示了配置切面的信息,这并不理想。这是一个基于时间的探查器,它使用@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类路径上称为META-INF/aop.xml
的文件,是标准的AspectJ。以下示例显示aop.xml文件:
<aspectj>
<weaver>
<include within="foo.*"/>
weaver>
<aspects>
<aspect name="foo.ProfilingAspect"/>
aspects>
aspectj>
现在,我们可以继续进行配置中特定于Spring的部分。我们需要配置一个LoadTimeWeaver
(稍后说明)。该加载时织布器是必不可少的组件,负责将一个或多个META-INF/aop.xml
文件中的切面配置编织到应用程序的类中。好处是,它不需要很多配置(你可以指定一些其他选项,但是稍后会详细介绍),如以下示例所示:
<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">
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>
<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框架附带了这样的代理工具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是通过使用成熟的AspectJ来实现的,因此我们不仅限于为Spring Bean提供通知。在Main程序上进行以下细微改动会产生相同的结果:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
请注意,在前面的程序中,我们如何引导Spring容器,然后完全在Spring上下之外创建StubEntitlementCalculationService
的新实例。剖析通知仍会被应用。
诚然,这个例子很简单。但是,在前面的示例中已经介绍了Spring对LTW支持的基础,本节的其余部分详细解释了每个配置和用法背后的“原因”。
在此示例中使用的
ProfilingAspect
可能是基本的,但它非常有用。它是开发时间方面的一个很好的例子,开发人员可以在开发期间使用它,然后很容易地从部署到UAT或生产中的应用程序的构建中排除它。
切面
你在LTW中使用的切面必须是AspectJ切面。你可以使用AspectJ语言本身来编写它们,也可以使用@AspectJ
风格来编写切面。这样,你的切面是有效的AspectJ和Spring AOP方面。此外,编译的切面类需要在类路径上可用。
’META-INF/aop.xml’
通过使用Java类路径上的一个或多个META-INF/aop.xml
文件(直接或通常在jar文件中)来配置AspectJ LTW基础结构。
该文件的结构和内容在AspectJ参考文档的LTW部分中进行了详细说明。由于aop.xml
文件是100%AspectJ,因此在此不再赘述。
所需的库(JARS)
至少,你需要使用以下库来使用Spring Framework对AspectJ LTW的支持:
spring-aop.jar
aspectjweaver.jar
如果使用Spring提供的代理来启用检测,则还需要:
spring-instrument.jar
Spring配置
Spring的LTW支持的关键组件是LoadTimeWeaver
接口(在org.springframework.instrument.classloading
包中),以及Spring发行版附带的众多实现。LoadTimeWeaver
负责在运行时将一个或多个java.lang.instrument.ClassFileTransformers
添加到ClassLoader
,这为各种有趣的应用程序打开了大门,其中之一就是方面的LTW。
如果你不熟悉运行时类文件转换的概念,请在继续操作之前参阅
java.lang.instrument
的javadoc API文档。尽管该文档并不全面,但至少你可以看到关键的接口和类(在你通读本节文档作为参考)。
为特定的ApplicationContext
配置LoadTimeWeaver
就像添加一行一样容易。(请注意,你几乎肯定需要将ApplicationContext
用作Spring容器-通常,仅BeanFactory
是不够的,因为LTW支持使用BeanFactoryPostProcessors
。)
要启用Spring Framework的LTW支持,你需要配置一个LoadTimeWeaver
,通常通过使用@EnableLoadTimeWeaving
注解来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
另外,如果你喜欢基于XML的配置,请使用
元素。注意,该元素是在上下文名称空间中定义的。以下示例显示了如何使用
:
<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 实现 |
---|---|
运行在 Apache Tomcat | TomcatLoadTimeWeaver |
运行在 GlassFish (限于EAR部署) | GlassFishLoadTimeWeaver |
运行在 Red Hat的 JBoss AS 或 WildFly | JBossLoadTimeWeaver |
运行在 IBM的 WebSphere | WebSphereLoadTimeWeaver |
运行在 Oracle的 WebLogic | WebLogicLoadTimeWeaver |
JVM从Spring开始InstrumentationSavingAgent (java -javaagent:path/to/spring-instrument.jar ) |
InstrumentationLoadTimeWeaver |
期望基础ClassLoader 遵循通用约定 (即addTransformer 和可选的getThrowawayClassLoader 方法) |
ReflectiveLoadTimeWeaver |
请注意,该表仅列出使用DefaultContextLoadTimeWeaver
时自动检测到的LoadTimeWeavers
。你可以确切指定要使用的LoadTimeWeaver
实现。
要使用Java配置指定特定的LoadTimeWeaver
,请实现LoadTimeWeavingConfigurer
接口并覆该getLoadTimeWeaver()
方法。
以下示例指定了ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
如果使用基于XML的配置,则可以在
元素上将全限定的类名指定为weaver-class
属性的值。同样,以下示例指定了ReflectiveLoadTimeWeaver
:
<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
从Spring容器中检索由配置定义和注册的LoadTimeWeaver
。请记住,LoadTimeWeaver
仅作为Spring LTW基础结构添加一个或多个ClassFileTransformers
的一种机制而存在。执行LTW的实际ClassFileTransformer
是ClassPreProcessorAgentAdapter
(来自org.aspectj.weaver.loadtime
包)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter
类的类级javadoc,因为实际上如何实现编织的细节不在本文档的讨论范围之内。
剩下要讨论的配置的最后一个属性:aspectjWeaving
属性(如果使用XML,则为Aspectj-weaving
)。此属性控制是否启用LTW。它接受三个可能的值之一,如果属性不存在,则默认值为autodetect
。如果属性不存在。下表总结了三个可能的值:
注解值 | XML 值 | Explanation |
---|---|---|
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和
设置自动检测到。
要使用它,必须通过提供以下JVM选项来使用Spring代理启动虚拟机:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改JVM启动脚本,这可能会阻止你在应用程序服务器环境中使用它(取决于你的服务器和你的操作策略)。也就是说,对于每个JVM一个应用程序的部署(例如独立的Spring Boot应用程序),无论如何,你通常都可以控制整个JVM的设置。
可以在AspectJ网站上找到有关AspectJ的更多信息。
Adrian Colyer等人(Addison-Wesley, 2005)编写的Eclipse AspectJ提供了AspectJ语言的全面介绍和参考。
强烈推荐AspectJ in Action
,由Ramnivas Laddad (Manning, 2009)编写的第二版。这本书的重点是AspectJ,但是也探讨了许多一般的AOP主题(有一定的深度)
个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。
博客地址:http://youngitman.tech
CSDN:https://blog.csdn.net/liyong1028826685
微信公众号: