Spring 框架 核心技术 使用 Spring 进行面向方面编程 @AspectJ 支持 声明切入点 声明切入点 切入点确定感兴趣的连接点,从而使我们能够控制建议的运行时间。Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此可以将切入点视为匹配 Spring Bean 上方法的执行。切入点声明包含两部分:包含名称和任何参数的签名,以及确定我们感兴趣的具体方法执行的切入点表达式。在 AOP 的 @AspectJ 注释样式中,切入点签名由常规方法定义提供,切入点表达式通过使用 @Pointcut 注释指示(用作切入点签名的该方法必须具有 void 返回类型)。 一个示例可能有助于明确切入点签名和切入点表达式之间的区别。以下示例定义了一个名为 anyOldTransfer 的切入点,它匹配任何名为 transfer 的方法的执行 Java Kotlin @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature @Pointcut("execution(* transfer(..))") // the pointcut expression private fun anyOldTransfer() {} // the pointcut signature 构成 @Pointcut 注释值的切入点表达式是一个常规 AspectJ 切入点表达式。有关 AspectJ 切入点语言的完整讨论,请参阅 AspectJ 编程指南(以及扩展,请参阅 AspectJ 5 开发人员笔记本)或有关 AspectJ 的书籍之一(例如,Colyer 等编写的 Eclipse AspectJ 或 Ramnivas Laddad 编写的 AspectJ in Action)。 支持的切入点指示符 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 AOP 解释的切入点表达式中使用这些切入点指示符会导致抛出 IllegalArgumentException。 Spring AOP 支持的切入点设计器集合可能会在未来版本中扩展,以支持更多 AspectJ 切入点设计器。 由于 Spring AOP 仅将匹配限制为方法执行连接点,因此前面对切入点设计器的讨论给出的定义比你在 AspectJ 编程指南中找到的定义更窄。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点中,this 和 target 都引用同一对象:执行该方法的对象。Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到 this)和代理后面的目标对象(绑定到 target)。 由于 Spring 的 AOP 框架基于代理,因此根据定义,不会拦截目标对象内的调用。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB 时,会拦截代理上的公共和受保护的方法调用(如果需要,甚至会拦截包可见方法)。但是,通过代理进行的常见交互应始终通过公共签名设计。 请注意,切入点定义通常与任何拦截的方法匹配。如果切入点严格来说仅限于公共方法,即使在 CGLIB 代理场景中通过代理进行潜在的非公共交互,也需要相应地定义切入点。 如果你的拦截需求包括目标类中的方法调用甚至构造函数,请考虑使用 Spring 驱动的 原生 AspectJ 织入,而不是 Spring 的基于代理的 AOP 框架。这构成了具有不同特征的不同 AOP 使用模式,因此在做出决定之前,请务必让自己熟悉织入。 Spring AOP 还支持一个名为 bean 的附加 PCD。此 PCD 允许你将连接点的匹配限制为特定的命名 Spring bean 或一组命名 Spring bean(使用通配符时)。bean PCD 具有以下形式 bean(idOrNameOfBean) idOrNameOfBean 令牌可以是任何 Spring bean 的名称。提供了使用 * 字符的有限通配符支持,因此,如果你为 Spring bean 建立了一些命名约定,则可以编写一个 bean PCD 表达式来选择它们。与其他切入点设计器一样,bean PCD 也可以与 &&(与)、||(或)和 !(否定)运算符一起使用。 bean PCD 仅在 Spring AOP 中受支持,在原生 AspectJ 织入中不受支持。它是 AspectJ 定义的标准 PCD 的 Spring 特定扩展,因此对于在 @Aspect 模型中声明的方面不可用。 bean PCD 在实例级别(建立在 Spring bean 名称概念之上)而不是仅在类型级别(基于织入的 AOP 受限于此)上操作。基于实例的切入点设计器是 Spring 基于代理的 AOP 框架的特殊功能,并且与 Spring bean 工厂紧密集成,在其中按名称识别特定 bean 自然且直接。 组合切入点表达式 你可以使用 &&, || 和 ! 组合切入点表达式。你还可以按名称引用切入点表达式。以下示例显示三个切入点表达式 Java Kotlin package com.xyz; public class Pointcuts { @Pointcut("execution(public * *(..))") public void publicMethod() {} (1) @Pointcut("within(com.xyz.trading..*)") public void inTrading() {} (2) @Pointcut("publicMethod() && inTrading()") public void tradingOperation() {} (3) } 1 如果方法执行连接点表示任何公共方法的执行,则 publicMethod 匹配。 2 如果方法执行在交易模块中,则 inTrading 匹配。 3 如果方法执行表示交易模块中的任何公共方法,则 tradingOperation 匹配。 package com.xyz class Pointcuts { @Pointcut("execution(public * *(..))") fun publicMethod() {} (1) @Pointcut("within(com.xyz.trading..*)") fun inTrading() {} (2) @Pointcut("publicMethod() && inTrading()") fun tradingOperation() {} (3) } 1 如果方法执行连接点表示任何公共方法的执行,则 publicMethod 匹配。 2 如果方法执行在交易模块中,则 inTrading 匹配。 3 如果方法执行表示交易模块中的任何公共方法,则 tradingOperation 匹配。 最佳实践是根据较小的命名切入点构建更复杂的切入点表达式,如上所示。在按名称引用切入点时,应用正常的 Java 可见性规则(你可以在同一类型中看到 private 切入点,在层次结构中看到 protected 切入点,在任何地方看到 public 切入点,依此类推)。可见性不影响切入点匹配。 共享命名切入点定义 在使用企业应用程序时,开发人员通常需要从多个方面引用应用程序的模块和特定操作集。我们建议定义一个专门的类,用于捕获为此目的常用的命名切入点表达式。此类通常类似于以下 CommonPointcuts 示例(尽管类的名称由你决定) Java Kotlin package com.xyz; import org.aspectj.lang.annotation.Pointcut; public class CommonPointcuts { /** * A join point is in the web layer if the method is defined * in a type in the com.xyz.web package or any sub-package * under that. */ @Pointcut("within(com.xyz.web..*)") public void inWebLayer() {} /** * A join point is in the service layer if the method is defined * in a type in the com.xyz.service package or any sub-package * under that. */ @Pointcut("within(com.xyz.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.dao package or any sub-package * under that. */ @Pointcut("within(com.xyz.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.abc.service and com.xyz.def.service) then * the pointcut expression "execution(* com.xyz..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..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.dao.*.*(..))") public void dataAccessOperation() {} } package com.xyz import org.aspectj.lang.annotation.Pointcut class CommonPointcuts { /** * A join point is in the web layer if the method is defined * in a type in the com.xyz.web package or any sub-package * under that. */ @Pointcut("within(com.xyz.web..*)") fun inWebLayer() {} /** * A join point is in the service layer if the method is defined * in a type in the com.xyz.service package or any sub-package * under that. */ @Pointcut("within(com.xyz.service..*)") fun inServiceLayer() {} /** * A join point is in the data access layer if the method is defined * in a type in the com.xyz.dao package or any sub-package * under that. */ @Pointcut("within(com.xyz.dao..*)") fun 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.abc.service and com.xyz.def.service) then * the pointcut expression "execution(* com.xyz..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..service.*.*(..))") fun 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.dao.*.*(..))") fun dataAccessOperation() {} } 你可以通过引用类的完全限定名称与 @Pointcut 方法的名称相结合,在需要切入点表达式的任何地方引用在此类中定义的切入点。例如,要使服务层具有事务性,你可以编写以下内容,它引用 com.xyz.CommonPointcuts.businessService() 命名切入点 <aop:config> <aop:advisor pointcut="com.xyz.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:config> 和 <aop:advisor> 元素在 基于模式的 AOP 支持 中讨论。事务元素在 事务管理 中讨论。 示例 Spring AOP 用户很可能会最常使用execution切入点设计器。执行表达式的格式如下 execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) 除了返回类型模式(前一个代码段中的ret-type-pattern)、名称模式和参数模式之外,所有部分都是可选的。返回类型模式确定连接点匹配时方法的返回类型必须是什么。*最常作为返回类型模式使用。它匹配任何返回类型。完全限定的类型名称仅在方法返回给定类型时匹配。名称模式匹配方法名称。你可以将*通配符用作名称模式的全部或部分。如果你指定声明类型模式,请包含一个尾随.将其连接到名称模式组件。参数模式稍微复杂一些:()匹配不带任何参数的方法,而(..)匹配任意数量(零个或更多)的参数。(*)模式匹配采用任何类型的一个参数的方法。(*,String)匹配采用两个参数的方法。第一个可以是任何类型,而第二个必须是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。有关如何在建议正文中使注释对象可用,请参阅声明建议部分。 Spring bean 上的任何连接点(在 Spring AOP 中仅限方法执行),其名称为 tradeService bean(tradeService) Spring bean 上的任何连接点(在 Spring AOP 中仅限方法执行),其名称与通配符表达式 *Service 匹配 bean(*Service) 编写良好的切入点 在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码运行时将测试放入代码中以确定是否存在实际匹配)。在首次遇到切入点声明时,AspectJ 会将其重写为匹配过程的最佳形式。这意味着什么?基本上,切入点以 DNF(析取范式)重写,并且切入点的组件按顺序排列,以便首先检查那些更便宜的组件。这意味着您不必担心了解各种切入点指示符的性能,并且可以在切入点声明中按任何顺序提供它们。 但是,AspectJ 只能处理它被告知的内容。为了实现匹配的最佳性能,您应该考虑您试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然地分为三组:类型、范围和上下文 类型指示符选择特定类型的连接点:execution、get、set、call 和 handler。 范围指示符选择一组感兴趣的连接点(可能是多种类型):within 和 withincode 上下文指示符基于上下文进行匹配(并可选绑定):this、target 和 @annotation 一个写得很好的切入点至少应包括前两种类型(类型和范围)。您可以包括上下文指示符以根据连接点上下文进行匹配,或绑定该上下文以在建议中使用。仅提供类型指示符或仅提供上下文指示符可以工作,但可能会影响编织性能(使用的時間和内存),这是由于额外的处理和分析。范围指示符匹配非常快,使用它们意味着 AspectJ 可以非常快速地丢弃不应进一步处理的连接点组。如果可能,一个好的切入点应该始终包含一个。 声明 Aspect 声明建议