带有AspectJ切入点的Spring AOP
Spring通过使用基于模式的方法 schema-based approach或@AspectJ注释样式@AspectJ annotation style提供了编写定制方面的简单而强大的方法。这两种风格都提供了完全类型化的建议和使用AspectJ切入点语言,同时仍然使用Spring AOP进行编织。
本章讨论了基于schema和@ aspectj的AOP支持。下一章将讨论较低级别的AOP支持。
AOP在Spring框架中用于:
提供声明性企业服务。此类服务中最重要的是声明性事务管理。
让用户实现自定义方面,用AOP补充他们对OOP的使用。
注意:如果您只对通用声明性服务或其他预打包的声明性中间件服务(如池)感兴趣,那么您不需要直接使用Spring AOP,并且可以跳过本章的大部分内容。
= = AOP概念
让我们从定义一些核心的AOP概念和术语开始。这些术语不是特定于spring的。不幸的是,AOP术语不是特别直观。然而,如果Spring使用自己的术语,则会更加混乱。
- Aspect:跨越多个类的关注的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,方面是通过使用常规类(基于模式的方法)或使用@Aspect注释的常规类(@AspectJ样式)来实现的。
- Join point:程序执行过程中的一个点,如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法执行。
- Advice:方面在特定连接点上采取的操作。不同类型的建议包括“around”、“before”、“after”的advice。(稍后将讨论通知类型。)许多AOP框架,包括Spring,将通知建模为拦截器,并维护围绕连接点的拦截器链。
- Pointcut:匹配连接点的谓词。通知与切入点表达式相关联,并在与切入点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。连接点与切入点表达式匹配的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
- Introduction:代表类型声明其他方法或字段。Spring AOP允许您将新的接口(和相应的实现)引入任何被建议的对象。例如,您可以使用一个介绍来让一个bean实现一个IsModified接口,以简化缓存。(介绍在AspectJ社区中称为类型间声明。)
- Target object:被一个或多个方面通知的对象。也称为“被通知对象”。因为Spring AOP是通过使用运行时代理来实现的,所以这个对象总是一个代理对象。
- AOP proxy:AOP框架为了实现方面契约(通知方法执行等等)而创建的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
- Weaving:将方面与其他应用程序类型或对象链接,以创建建议的对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行编织。
Spring AOP包括以下类型的通知:
- Before advice:在连接点之前运行的通知,但是不能阻止执行流继续到连接点(除非抛出异常)。
- After returning advice:在连接点正常完成后运行的通知(例如,如果一个方法没有抛出异常返回)。
- After throwing advice:如果方法通过抛出异常退出,则执行通知。
- After (finally) advice:不管连接点以何种方式退出(正常或异常返回),都要执行的通知。
- Around advice:围绕连接点(如方法调用)的通知。这是最有力的建议。Around通知可以在方法调用前后执行自定义行为。它还负责选择是继续到连接点,还是通过返回它自己的返回值或抛出异常来简化建议的方法执行。
Around advice是最普遍的建议。因为Spring AOP和AspectJ一样,提供了各种各样的通知类型,所以我们建议您使用最不强大的通知类型来实现所需的行为。例如,如果只需要用方法的返回值更新缓存,则最好实现after return通知,而不是around通知,尽管around通知可以完成相同的工作。使用最特定的通知类型可以提供更简单的编程模型,减少出错的可能性。例如,您不需要在用于around通知的连接点上调用proceed()方法,因此,您不可能不调用它。
所有的通知参数都是静态类型的,这样您就可以使用适当类型的通知参数(例如方法执行返回值的类型),而不是Object数组。
由切入点匹配的连接点的概念是AOP的关键,它将AOP与只提供拦截的旧技术区分开来。切入点使通知能够独立于面向对象的层次结构。例如,可以将提供声明性事务管理的around建议应用于一组跨多个对象的方法(例如服务层中的所有业务操作)。
== Spring AOP的功能和目标
Spring AOP是在纯Java中实现的。不需要特殊的编译过程。Spring AOP不需要控制类装入器层次结构,因此适合在servlet容器或应用服务器中使用。
Spring AOP目前只支持方法执行连接点(建议在Spring bean上执行方法)。虽然可以在不破坏核心Spring AOP api的情况下添加对字段拦截的支持,但是没有实现字段拦截。如果需要通知字段访问和更新连接点,请考虑AspectJ之类的语言。
Spring AOP的AOP方法与大多数其他AOP框架不同。其目的不是提供最完整的AOP实现(尽管Spring AOP非常强大)。相反,其目标是提供AOP实现和Spring IoC之间的紧密集成,以帮助解决企业应用程序中的常见问题。
因此,例如,Spring框架的AOP功能通常与Spring IoC容器一起使用。方面是通过使用普通的bean定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的一个重要区别。使用Spring AOP不能轻松或有效地完成某些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最佳选择。然而,我们的经验是,Spring AOP为企业Java应用程序中大多数适合AOP的问题提供了一个优秀的解决方案。
Spring AOP从未试图与AspectJ竞争来提供全面的AOP解决方案。我们认为基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争的。
Spring将Spring AOP和IoC与AspectJ无缝集成,从而在一致的基于Spring的应用程序体系结构中支持AOP的所有使用。这种集成并不影响Spring AOP API或AOP Alliance API。Spring AOP保持向后兼容。有关Spring AOP api的讨论,请参阅下一章。
Spring框架的核心原则之一是无创性。您不应该被迫将特定于框架的类和接口引入到您的业务或域模型中。然而,在某些地方,Spring框架确实为您提供了将特定于Spring框架的依赖项引入代码库的选项。之所以提供这些选项,是因为在某些场景中,以这种方式阅读或编写某些特定功能的代码可能更容易。然而,Spring框架(几乎)总是为您提供这样的选择:您可以自由地做出明智的决定,即哪种选择最适合您的特定用例或场景。
与本章相关的一个选择是选择哪种AOP框架(以及哪种AOP风格)。您可以选择AspectJ、Spring AOP,或者两者兼而有之。您还可以选择@AspectJ注释样式方法或Spring XML配置样式方法。本章选择首先介绍@AspectJ风格的方法,这并不意味着Spring团队更喜欢@AspectJ注释风格的方法,而不是Spring XML配置风格。
关于每种风格的“为什么和为什么”的更完整的讨论,请参见 [aop-choosing]。
= = AOP代理
Spring AOP默认为AOP代理使用标准JDK动态代理。这允许代理任何接口(或接口集)。
Spring AOP还可以使用CGLIB代理。这对于代理类而不是接口是必要的。默认情况下,如果业务对象没有实现接口,则使用CGLIB。由于对接口而不是类进行编程是一种良好的实践,所以业务类通常实现一个或多个业务接口。强制使用CGLIB( force the use of CGLIB)是可能的,在那些(希望很少)情况下,您需要建议一个没有在接口上声明的方法,或者需要将经过代理的对象作为具体类型传递给方法。
理解Spring AOP是基于代理的这一事实是很重要的。参见 [aop-understanding-aop-proxies],以彻底检查这个实现细节的实际含义。
= = @ aspectj的支持
@AspectJ指的是将方面声明为用注释注释的常规Java类的样式。@AspectJ样式是由AspectJ项目作为AspectJ 5发行版的一部分引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。但是AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。
注意:使用AspectJ编译器和weaver可以使用完整的AspectJ语言, [aop-using-aspectj]中对此进行了讨论。
===启用@AspectJ支持
要在Spring配置中使用@AspectJ方面,您需要启用Spring对基于@AspectJ方面配置Spring AOP的支持,并根据这些方面是否建议自动代理bean。通过自动代理,我们的意思是,如果Spring确定一个bean由一个或多个方面通知,它将自动为该bean生成一个代理来拦截方法调用,并确保根据需要执行通知。
可以通过XML或java风格的配置启用@AspectJ支持。在这两种情况下,您还需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径上(版本1.8或更高)。这个库可以从AspectJ发行版的lib目录或Maven中央存储库中获得。
===使用Java配置启用@AspectJ支持
要使用Java @Configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注释,如下面的示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
===启用@AspectJ支持XML配置
要使用基于xml的配置启用@AspectJ支持,可以使用aop:aspectj-autoproxy元素,如下面的示例所示:
<aop:aspectj-autoproxy/>
这假设您使用 XML Schema-based configuration中描述的模式支持。有关如何导入AOP名称空间中的标记,请参阅 the AOP schema。
===声明一个方面
启用@AspectJ支持后,在应用程序上下文中定义的任何bean,只要类是@AspectJ方面(具有@Aspect注释),就会被Spring自动检测到,并用于配置Spring AOP。接下来的两个例子展示了一个不太有用的方面所需要的最小定义。
这两个示例中的第一个示例显示了应用程序上下文中的一个常规bean定义,它指向一个具有@Aspect注释的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
两个示例中的第二个示例显示NotVeryUsefulAspect类定义,它是用org.aspectj.lang.annotation注释的。注释方面;
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(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
形成@Pointcut注释值的切入点表达式是一个常规的AspectJ 5切入点表达式。关于AspectJ的切入点语言的完整讨论,请参阅AspectJ编程指南 AspectJ Programming Guide(对于扩展,请参阅AspectJ 5开发人员的笔记本 AspectJ 5 Developer’s Notebook)或关于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中不支持的其他切入点指示器:调用、获取、设置、预初始化、静态初始化、初始化、处理程序、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。在由Spring AOP解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException。
在将来的版本中,Spring AOP支持的一组切入点设计器可以得到扩展,以支持更多的AspectJ切入点设计器。
因为Spring AOP将匹配限制为只匹配方法执行连接点,前面关于切入点设计器的讨论给出了比AspectJ编程指南中更窄的定义。此外,AspectJ本身具有基于类型的语义,在执行连接点上,这个和目标都引用同一个对象:执行方法的对象。Spring AOP是一个基于代理的系统,它区别于代理对象本身(绑定到它)和代理背后的目标对象(绑定到目标)。
由于Spring的AOP框架具有基于代理的特性,目标对象中的调用根据定义不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,可以截获代理上的public和protected方法调用(如果需要,甚至可以截获包可见的方法)。然而,通过代理的公共交互应该始终通过公共签名来设计。
注意,切入点定义通常与任何被拦截的方法相匹配。如果严格地说切入点是公共的,那么即使在CGLIB代理场景中,通过代理进行潜在的非公共交互,也需要对它进行相应的定义。
如果您的拦截需要包括目标类中的方法调用甚至构造函数,那么考虑使用Spring驱动的本机AspectJ编织,而不是Spring的基于代理的AOP框架。这构成了具有不同特征的AOP使用的不同模式,所以在做决定之前一定要熟悉编织。
Spring AOP还支持另外一个名为bean的PCD。此PCD允许您将连接点的匹配限制为特定的命名Spring bean或一组命名Spring bean(在使用通配符时)。bean PCD的形式如下:
bean(idOrNameOfBean)
ideofbean令牌可以是任何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
//1 如果方法执行连接点表示任何公共方法的执行,则anyPublicOperation匹配。
//2 如果方法执行在交易模块中,则在交易中匹配。
//3 如果方法执行代表交易模块中的任何公共方法,则tradingOperation匹配。
最佳实践是从较小的命名组件构建更复杂的切入点表达式,如前面所示。当通过名称引用切入点时,应用普通的Java可见性规则(您可以在同一类型中看到私有切入点、层次结构中的受保护切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。
===共享公共切入点定义
在使用企业应用程序时,开发人员通常希望从几个方面引用应用程序的模块和特定的操作集。我们建议定义一个“系统架构”方面,它可以捕获用于此目的的公共切入点表达式。这样一个方面通常类似于下面的例子:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
/
@Pointcut("within(com.xyz.someapp.web..)")
public void inWebLayer() {}
/
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
/
@Pointcut("within(com.xyz.someapp.service..)")
public void inServiceLayer() {}
/
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
/
@Pointcut("within(com.xyz.someapp.dao..)")
public void inDataAccessLayer() {}
/
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service..(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution( com.xyz.someapp..service..(..))")
public void businessService() {}
/*
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution( com.xyz.someapp.dao..(..))")
public void dataAccessOperation() {}
}
您可以在任何需要切入点表达式的地方引用在这样一个方面中定义的切入点。例如,要使服务层具有事务性,可以编写以下代码:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>和<aop:advisor>元素在[aop-schema]中讨论。事务管理中讨论事务元素。
= = = =的例子
Spring AOP用户可能最经常使用执行切入点指示器。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(前面代码段中的rt -type-pattern)、名称模式和参数模式之外,其他所有部分都是可选的。返回类型模式确定要匹配连接点,方法的返回类型必须是什么。*是最常用的返回类型模式。它匹配任何返回类型。完全限定类型名仅在方法返回给定类型时才匹配。名称模式与方法名称匹配。您可以使用*通配符作为名称模式的全部或部分。如果指定了声明类型模式,请包含尾随。将其连接到名称模式组件。参数模式稍微复杂一点:()匹配不带参数的方法,而(..)匹配任意数量(零或更多)的参数。(*)模式匹配接受任意类型参数的方法。(*,String)匹配一个带有两个参数的方法。第一个可以是任何类型,第二个必须是字符串。有关更多信息,请参阅AspectJ编程指南的语言语义部分。
下面的例子展示了一些常见的切入点表达式:
执行任何公用方法:
execution(public * *(..))
任何以set开头的方法的执行:
execution(* set*(..))
执行AccountService接口定义的任何方法:
execution(* com.xyz.service.AccountService.*(..))
执行服务包中定义的任何方法:
execution(* com.xyz.service.*.*(..))
执行服务包或其中一个子包中定义的任何方法:
execution(* com.xyz.service..*.*(..))
服务包内的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service.*)
服务包或其子包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service..*)
代理实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
this(com.xyz.service.AccountService)
注意:“this”更常用于绑定形式。有关如何使代理对象在通知正文中可用,请参阅 [aop-advice]一节。
目标对象实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
target(com.xyz.service.AccountService)
注意:‘target’更常用于绑定形式。请参阅[aop-advice]一节,了解如何使目标对象在advice主体中可用。
任何接受单个参数的连接点(只有在Spring AOP中才执行方法),并且在运行时传递的参数是可序列化的:
args(java.io.Serializable)
注意:“args”通常用于绑定形式。请参阅[aop-advice]一节,了解如何使方法参数在advice主体中可用。
注意,本例中给出的切入点不同于执行(* *(java.io.Serializable))。如果在运行时传递的参数是可序列化的,则args版本匹配;如果方法签名声明一个可序列化类型的参数,则执行版本匹配。
目标对象具有@Transactional注释的任何连接点(仅在Spring AOP中执行方法):
@target(org.springframework.transaction.annotation.Transactional)
注意:您还可以在绑定表单中使用'@target'。请参阅[aop-advice]一节,了解如何使注释对象在通知正文中可用。
目标对象的声明类型具有@Transactional注释的任何连接点(仅在Spring AOP中执行方法):@within(org.springframework.transaction.annotation.Transactional)
注意:您还可以在绑定表单中使用'@within'。请参阅[aop-advice]一节,了解如何使注释对象在通知正文中可用。
任何连接点(仅在Spring AOP中执行方法),其中执行方法具有@Transactional注释:
@annotation(org.springframework.transaction.annotation.Transactional)
任何接受单个参数的连接点(仅在Spring AOP中执行方法),其中传递的参数的运行时类型有@ classification注释:
@args(com.xyz.security.Classified)
在名为tradeService的Spring bean上的任何连接点(仅在Spring AOP中执行方法):
bean(tradeService)
在具有匹配通配符表达式*Service的名称的Spring bean上的任何连接点(仅在Spring AOP中执行方法):
bean(*Service)
===编写好的切入点
在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否与给定的切入点匹配(静态或动态)是一个开销很大的过程。(动态匹配意味着不能通过静态分析完全确定匹配,在代码中放置一个测试,以确定在代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ会将它重写为匹配过程的最佳形式。这是什么意思?基本上,切入点是用DNF(析取范式)重写的,切入点的组件是排序的,这样可以先检查那些比较便宜的组件。这意味着您不必担心理解各种切入点指示器的性能,并且可以在切入点声明中以任意顺序提供它们。
然而,AspectJ只能处理它被告知的内容。为了获得最佳匹配性能,您应该考虑他们试图实现什么,并在定义中尽可能缩小匹配的搜索空间。现有的指示器可以自然地分为三类:kinded、scoping和context:
- Kinded指示符选择特定类型的连接点:执行、获取、设置、调用和处理程序。
- 作用域指定符选择一组感兴趣的连接点(可能有多种):inside和withincode
- 上下文指示符根据上下文(this、target和@annotation)匹配(也可以绑定)
编写良好的切入点至少应该包括前两种类型(类型和范围)。您可以包含基于连接点上下文进行匹配的上下文指示符,或者将该上下文绑定在通知中使用。只提供一种类型的指示符或只提供上下文指示符,但由于需要额外的处理和分析,可能会影响编织性能(所使用的时间和内存)。作用域指示器匹配起来非常快,使用它们意味着AspectJ可以非常快地取消不应该进一步处理的连接点组。好的切入点应该尽可能包含一个切入点。
= = =声明的Advice
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。切入点表达式可以是对指定切入点的简单引用,也可以是在适当位置声明的切入点表达式。
= = = =Before Advice
你可以使用@Before注释在方面中声明before通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果我们使用一个插入的切入点表达式,我们可以将前面的例子重写为下面的例子:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao..(..))")
public void doAccessCheck() {
// ...
}
}
====After Returning Advice
返回后,当匹配的方法执行正常返回时,将运行通知。你可以使用@AfterReturning注释来声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
注意:您可以有多个通知声明(以及其他成员),它们都在同一个方面中。在这些示例中,我们只显示一个通知声明,以集中说明每个通知的效果。
有时,您需要在advice主体中访问返回的实际值。您可以使用@ afterreturn的形式绑定返回值来获得访问权限,如下面的例子所示:
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方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给advice方法。return子句还将匹配限制为只匹配那些返回指定类型值的方法执行(在本例中为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() {
// ...
}
}
通常,您希望仅在抛出给定类型的异常时才运行通知,并且常常需要在通知正文中访问抛出的异常。您可以使用抛出属性来限制匹配(如果需要,可以使用Throwable作为异常类型),并将抛出的异常绑定到一个advice参数。下面的例子演示了如何做到这一点:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
抛出属性中使用的名称必须与advice方法中的参数名称相对应。当方法执行通过抛出异常退出时,异常将作为相应的参数值传递给advice方法。抛出子句还将匹配限制为仅抛出指定类型的异常的方法执行(本例中为DataAccessException)。
====After (Finally) Advice
当匹配的方法执行退出时,将运行After (finally)通知。它是使用@After注释声明的。After通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似的目的。下面的例子展示了如何使用after finally advice:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
= = = = Around Advice
最后一种建议是关于建议的。Around通知“绕过”一个匹配的方法的执行。它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真正执行方法。如果需要以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态,通常会使用Around通知。始终使用最不强大的建议形式来满足您的需求(也就是说,如果before建议可以的话,就不要使用around建议)。
Around通知是使用@Around注释声明的。通知方法的第一个参数必须是类型为procedure edingjoinpoint的。在通知的主体中,对过程ingjoinpoint调用proceed()会导致底层方法执行。proceed方法也可以传递一个对象[]。当方法执行时,数组中的值用作方法执行的参数。
用Object[]调用proceed时的行为与用AspectJ编译器编译around通知时的行为稍有不同。建议使用传统的AspectJ语言编写,左右进行传递的参数的数量必须匹配的参数传递到周围的建议(不是参数由底层连接点的数量),并继续在一个给定的参数传递的价值立场取代原来的价值实体价值的连接点是绑定到(不要担心如果现在没有意义)。Spring采用的方法更简单,更适合其基于代理、仅执行的语义。只有在编译为Spring编写的@AspectJ方面并使用AspectJ编译器和weaver的参数时,才需要注意这种差异。有一种方法可以编写这样的方面,它在Spring AOP和AspectJ之间都是100%兼容的,下面关于通知参数的部分将对此进行讨论。
下面的例子展示了如何使用around建议:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知返回的值是方法调用者看到的返回值。例如,简单的缓存方面可以从缓存中返回一个值(如果有的话),如果没有就调用proceed()。请注意,proceed可能被调用一次、多次,或者根本不在around通知的正文中调用。所有这些都是合法的。
= = = =Advice Parameters
Spring提供了全类型的通知,这意味着您可以在通知签名中声明所需的参数(正如我们在前面看到的返回和抛出示例),而不是一直使用Object[]数组。在本节的后面部分,我们将看到如何将参数和其他上下文值提供给建议主体。首先,我们来看看如何编写通用的建议,从而找出建议当前建议的方法。
====访问当前连接点
任何通知方法都可以将org.aspectj.lang类型的参数声明为它的第一个参数。注意,需要around通知来声明类型为procedure edingjoinpoint的第一个参数,它是JoinPoint的子类。JoinPoint接口提供了许多有用的方法:
- getArgs():返回方法参数。
- getThis():返回代理对象。
- getTarget():返回目标对象。
- getSignature():返回被建议的方法的描述。
- toString():打印建议的方法的有用描述。
有关更多细节,请参见 javadoc。
====将参数传递给通知
我们已经看到了如何绑定返回值或异常值(在返回和抛出通知之后使用)。要使参数值对通知主体可用,可以使用args的绑定形式。如果在args表达式中使用参数名代替类型名,则在调用通知时将传递相应参数的值作为参数值。举个例子应该会更清楚。假设您希望通知以Account对象作为第一个参数的DAO操作的执行,并且需要访问advice主体中的Account。你可以这样写:
@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
}
为了实现这一点,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理空值。要实现类似的功能,必须将参数类型设置为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
}
如果第一个参数是连接点的,则处理ingjoinpoint或JoinPoint。您可以从argNames属性的值中省略参数的名称。例如,如果您修改前面的通知来接收连接点对象,argNames属性不需要包含它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
对连接点、加工连接点和连接点的第一个参数进行了特殊处理。对于不收集任何其他连接点上下文的通知实例,StaticPart类型特别方便。在这种情况下,可以忽略argNames属性。例如,下面的建议不需要声明argNames属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
- 使用'argNames'属性有点笨拙,因此如果没有指定'argNames'属性,Spring AOP将查看该类的调试信息,并尝试从局部变量表中确定参数名。只要使用调试信息('-g:vars')编译了类,就会出现此信息。打开这个标记进行编译的结果是:(1)您的代码稍微容易理解(反向工程),(2)类文件的大小稍微大一些(通常是无关紧要的),(3)编译器没有应用删除未使用的局部变量的优化。换句话说,您在构建时不应该遇到任何困难。
注意:如果AspectJ编译器(ajc)编译了@AspectJ方面,即使没有调试信息,也不需要添加argNames属性,因为编译器保留了所需的信息。
- 如果在没有必要的调试信息的情况下编译了代码,那么Spring AOP就会尝试将绑定变量对推断为参数(例如,如果切入点表达式中只绑定了一个变量,而advice方法只接受一个参数,那么这种配对是明显的)。如果给定可用信息,变量的绑定是不明确的,则抛出一个AmbiguousBindingException。
- 如果上述所有策略都失败,则抛出IllegalArgumentException。
====继续讨论参数
我们在前面提到过,我们将描述如何使用在Spring AOP和AspectJ中一致工作的参数来编写proceed调用。解决方案是确保通知签名按顺序绑定每个方法参数。下面的例子演示了如何做到这一点:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在许多情况下,无论如何都要进行这种绑定(如前面的示例所示)。
= = = =Advice Ordering
当多个通知都希望在同一个连接点上运行时,会发生什么情况?Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。最高优先级的通知将首先运行“on The way in”(因此,给定两个before通知,优先级最高的通知将首先运行)。从连接点“退出”时,优先级最高的通知最后运行(因此,给定两个after通知,优先级最高的通知将运行在第二位)。
当在不同方面定义的两个通知都需要在同一个连接点上运行时,除非您另外指定,否则执行的顺序是未定义的。您可以通过指定优先级来控制执行的顺序。这是通过实现org.springframework.core来实现的。方面类中的有序接口或使用有序注释对其进行注释。给定两个方面,从order . getvalue()(或注释值)返回较低值的方面具有较高的优先级。
当在相同方面中定义的两个通知都需要在相同的连接点上运行时,顺序是未定义的(因为无法通过反射为javac编译的类检索声明顺序)。考虑将这些通知方法分解为每个方面类中每个连接点的一个通知方法,或者将这些通知片段重构为可以在方面级别排序的独立方面类。
= = =介绍
引入(在AspectJ中称为类型间声明)使方面能够声明被通知的对象实现给定的接口,并代表这些对象提供接口的实现。
您可以使用@DeclareParents注释进行介绍。此注释用于声明匹配类型具有新的父类型(因此得名)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked接口的实现,以下方面声明所有服务接口的实现者也实现了UsageTracked接口(例如通过JMX公开统计信息):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由带注释字段的类型决定。@DeclareParents注释的值属性是一个AspectJ类型模式。任何匹配类型的bean都实现UsageTracked接口。注意,在前面示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。如果以编程方式访问一个bean,您将编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
=== Aspect Instantiation Models
注意:这是一个高级的主题。如果你刚开始使用AOP,你可以安全地跳过它,直到以后。
默认情况下,应用程序上下文中每个方面都有一个单独的实例。AspectJ称之为单例实例化模型。定义具有交替生命周期的方面是可能的。Spring支持AspectJ的perthis和pertarget实例化模型(当前不支持percflow、percflowbelow和pertypewithin)。
您可以通过在@Aspect注释中指定perthis子句来声明perthis方面。考虑下面的例子:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,“perthis”子句的作用是为每个执行业务服务的惟一服务对象创建一个方面实例(在切入点表达式匹配的连接点上绑定到“this”的惟一对象)。方面实例是在服务对象上第一次调用方法时创建的。当服务对象超出范围时,方面就超出范围。在方面实例创建之前,它内的任何通知都不执行。一旦方面实例被创建,在其中声明的通知就会在匹配的连接点上执行,但是只有当服务对象与方面相关联时才会执行。有关per子句的更多信息,请参阅AspectJ编程指南。
pertarget实例化模型的工作方式与perthis完全相同,但它在匹配的连接点为每个惟一的目标对象创建一个方面实例。
===一个AOP例子
既然您已经看到了所有组成部分是如何工作的,那么我们可以将它们放在一起来做一些有用的事情。
由于并发性问题(例如死锁失败者),业务服务的执行有时会失败。如果操作被重试,它很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(幂等操作不需要返回用户以解决冲突),我们希望透明地重试操作,以避免客户端看到一个悲观的lockingfailureexception异常。这是一个明显跨越服务层中的多个服务的需求,因此是通过方面实现的理想需求。
因为我们想要重试操作,所以我们需要使用around通知,以便能够多次调用proceed。下面的清单显示了基本的方面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
请注意,方面实现了有序接口,因此我们可以将方面的优先级设置为高于事务通知的优先级(我们希望每次重试时都有一个新的事务)。maxretry和order属性都是由Spring配置的。主要操作发生在围绕通知的doConcurrentOperation中。请注意,目前我们将重试逻辑应用于每个businessService()。我们尝试继续,如果我们失败了,并且出现了一个悲观的lockingfailureexception异常,我们就会再次尝试,除非我们已经用尽了所有的重试尝试。
对应的弹簧配置如下:
<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 {
// ...
}
==基于模式的AOP支持
如果您喜欢基于xml的格式,Spring还提供了使用新的aop名称空间标记定义方面的支持。支持与使用@AspectJ样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点讨论新的语法,并将读者转到前一节([aop-ataspectj])的讨论中,以了解编写切入点表达式和通知参数的绑定。
要使用本节中描述的aop名称空间标记,您需要导入spring-aop模式,正如在 XML Schema-based configuration中所描述的那样。有关如何导入AOP名称空间中的标记,请 the AOP schema。
在Spring配置中,所有方面和advisor元素都必须放在<aop:config>元素中(在应用程序上下文配置中可以有多个<aop:config>元素)。<aop:config>元素可以包含切入点、顾问和方面元素(注意,这些元素必须按顺序声明)。
<aop:config>配置风格大量使用了Spring的自动代理机制 auto-proxying。如果您已经通过使用BeanNameAutoProxyCreator或类似的东西使用了显式的自动代理,那么这可能会导致问题(比如通知没有被编织)。推荐的使用模式是只使用<aop:config>样式或者只使用AutoProxyCreator样式,并且永远不要混合使用它们。
===声明一个Aspect
当您使用模式支持时,方面是定义为Spring应用程序上下文中的bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在XML中捕获。
可以使用<aop:aspect>元素声明一个方面,使用ref属性引用支持bean,如下面的例子所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支持方面的bean(本例中是aBean)当然可以像其他任何Spring bean一样配置和注入依赖项。
===声明一个切入点
可以在<aop:config>元素中声明一个指定的切入点,让切入点定义在多个方面和顾问之间共享。
表示服务层中任何业务服务执行的切入点可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
注意,切入点表达式本身使用的是与 [aop-ataspectj]中描述的相同的AspectJ切入点表达式语言。如果您使用基于模式的声明样式,您可以引用在切入点表达式的types (@Aspects)中定义的命名切入点。定义上述切入点的另一种方法是:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
</aop:config>
假设您有一个在[aop-common-pointcuts]中描述的系统架构方面。
然后在一个方面中声明一个切入点与声明一个顶级切入点非常相似,如下面的例子所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与@AspectJ方面非常相似,通过使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,下面的切入点将这个对象收集为连接点上下文,并将其传递给通知:
<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来引用的,不能作为命名的切入点来使用,以形成复合切入点。因此,基于模式的定义样式中的命名切入点支持比@AspectJ样式提供的更有限。
= = =Declaring Advice
基于模式的AOP支持使用与@AspectJ样式相同的五种通知,而且它们具有完全相同的语义。
= = = =Before Advice
在匹配的方法执行之前运行通知。通过在>元素之前使用<aop:aspect>声明它,如下面的例子所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
这里,dataAccessOperation是在顶层(<aop:config>)定义的切入点的id。要定义内联的切入点,用切入点属性替换pointcut-ref属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
正如我们在讨论@AspectJ样式时所注意到的,使用指定的切入点可以显著提高代码的可读性。
方法属性标识提供通知主体的方法(doAccessCheck)。必须为包含通知的方面元素引用的bean定义此方法。在执行数据访问操作(由切入点表达式匹配的方法执行连接点)之前,将调用方面bean上的doAccessCheck方法。
====After Returning Advice
返回后,当匹配的方法执行正常完成时,将运行通知。它在<aop:aspect>中声明,方法与前面的通知相同。下面的例子展示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
与@AspectJ样式一样,您可以在通知体中获得返回值。为此,使用return属性指定应该向其传递返回值的参数的名称,如下面的示例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck方法必须声明一个名为retVal的参数。此参数的类型以与@ afterreturn相同的方式约束匹配。例如,你可以这样声明方法签名:
public void doAccessCheck(Object retVal) {...
====After Throwing Advice
当一个匹配的方法通过抛出异常退出时,抛出通知。它是通过使用后抛出元素在<aop:aspect>中声明的,如下面的例子所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>
与@AspectJ样式一样,您可以在通知正文中获得抛出的异常。为此,使用throw属性指定应该向其传递异常的参数的名称,如下面的示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions方法必须声明一个名为dataAccessEx的参数。此参数的类型以与@ afterthrow相同的方式约束匹配。例如,方法签名可以声明如下:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
====After (Finally) Advice
无论匹配的方法执行如何退出,都会运行After (finally)通知。你可以使用after元素来声明它,如下面的例子所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
= = = =Around Advice
最后一种建议是关于建议的。Around建议“围绕”一个匹配的方法执行。它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真正执行方法。Around建议通常用于以线程安全的方式共享方法执行前后的状态(例如,启动和停止计时器)。总是使用最不有力的建议来满足你的需求。如果事先的建议能够奏效,就不要使用迂回的建议。
可以使用aop:around元素声明around通知。通知方法的第一个参数必须是类型为procedure edingjoinpoint的。在通知的主体中,对过程ingjoinpoint调用proceed()会导致底层方法执行。也可以使用对象[]调用proceed方法。当方法执行时,数组中的值用作方法执行的参数。请参阅[aop-ataspectj-around-advice]以获得关于使用对象[]继续调用的注意事项。下面的例子演示了如何在XML中声明around通知:
<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支持相同的方式支持全类型通知——通过名称与通知方法参数匹配切入点参数。详见 [aop-ataspectj-advice-params]。如果你想显式地指定参数名称建议方法(不依赖于先前描述的检测策略),可以通过使用建议的arg-names属性元素,在相同的方式对待argNames属性在一个建议注释(如 [aop-ataspectj-advice-params-names]中描述)。下面的例子演示了如何在XML中指定参数名:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
argname属性接受以逗号分隔的参数名列表。
下面是基于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(..)将被用作around advice,如下例所示:
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);
}
}
有了这样一个引导类,我们将得到类似于下面的标准输出:
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
= = = =Advice Ordering
当多个通知需要在同一个连接点(执行方法)上执行时,排序规则如 [aop-ataspectj-advice-ordering]中所述。方面之间的优先级是通过向支持方面的bean添加Order注释或让bean实现有序接口来确定的。
= = =介绍
引入(在AspectJ中称为类型间声明)让方面声明通知对象实现给定接口,并代表这些对象提供接口的实现。
可以在aop:方面中使用aop: declour -parent元素来进行介绍。可以使用aop:declare-parents元素声明匹配类型有一个新的父类(因此有了这个名称)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked的接口的实现,以下方面声明所有服务接口的实现者也实现UsageTracked接口。(例如,为了通过JMX公开统计数据。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.SystemArchitecture.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支持usageTracking bean的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
要实现的接口由实现接口属性决定。types-matching属性的值是一个AspectJ类型模式。任何匹配类型的bean都实现UsageTracked接口。注意,在前面示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。要以编程方式访问bean,可以编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
====Aspect 实例化模型
模式定义方面惟一支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。
= = =Advisors
“Advisors”的概念来自于Spring中定义的AOP支持,在AspectJ中没有一个直接的等价物。建议者就像一个独立的小方面,只有一条建议。通知本身由一个bean表示,并且必须实现[aop-api-advice-types]中描述的一个通知接口。顾问可以利用AspectJ切入点表达式。
Spring通过<aop:advisor>元素支持advisor概念。您通常会看到它与事务性建议一起使用,在Spring中事务性建议也有自己的名称空间支持。下面的例子显示了一个advisor工具:
<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属性内联定义切入点表达式。
要定义advisor工具的优先级,以便通知可以参与排序,可以使用order属性来定义advisor工具的有序值。
===一个AOP模式示例
本节将展示使用模式支持重写时,来自[aop-ataspectj-example]的并发锁定失败重试示例的外观。
由于并发性问题(例如死锁失败者),业务服务的执行有时会失败。如果操作被重试,它很可能在下一次尝试中成功。对于适合在这种情况下重试的业务服务(幂等操作不需要返回用户以解决冲突),我们希望透明地重试操作,以避免客户端看到一个悲观的lockingfailureexception异常。这是一个明显跨越服务层中的多个服务的需求,因此是通过方面实现的理想需求。
因为我们想要重试操作,所以我们需要使用around通知,以便能够多次调用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;
}
}
请注意,方面实现了有序接口,因此我们可以将方面的优先级设置为高于事务通知的优先级(我们希望每次重试时都有一个新的事务)。maxRetries
和order属性都是由Spring配置的。主要动作发生在doConcurrentOperation around advice方法中。我们试着继续。如果我们以一个PessimisticLockingFailureException失败了,我们就会再试一次,除非我们已经用尽了所有的重试尝试。
注意:这个类与@AspectJ示例中使用的类相同,但是去掉了注释。
对应的弹簧配置如下:
<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
}
对方面的更改是只重试幂等操作,这涉及到细化切入点表达式,以便只匹配@幂等操作,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
==选择使用哪种AOP声明样式
一旦确定了方面是实现给定需求的最佳方法,那么如何决定是使用Spring AOP还是AspectJ,是使用aspect语言(代码)风格、@AspectJ注释风格还是Spring XML风格呢?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对AOP的熟悉程度。
=== Spring AOP还是Full AspectJ?
用最简单的方法。Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/编织器。如果您只需要建议在Spring bean上执行操作,那么Spring AOP是正确的选择。如果需要通知Spring容器没有管理的对象(通常是域对象),则需要使用AspectJ。如果希望通知连接点而不是简单的方法执行(例如,字段get或set连接点等),还需要使用AspectJ。
使用AspectJ时,可以选择AspectJ语言语法(也称为“代码样式”)或@AspectJ注释样式。显然,如果您不使用Java 5+,那么您已经做出了选择:使用代码样式。如果方面在您的设计中扮演了重要的角色,并且您能够使用用于Eclipse的AspectJ开发工具(AJDT)插件 AspectJ Development Tools (AJDT) ,那么AspectJ语言语法是首选的选项。它更简洁,因为该语言是专门为编写方面而设计的。如果您不使用Eclipse,或者只有几个方面在应用程序中不发挥主要作用,那么您可能希望考虑使用@AspectJ样式,在IDE中坚持常规的Java编译,并将方面编织阶段添加到构建脚本中。
=== @AspectJ还是用于Spring AOP的XML ?
如果选择使用Spring AOP,则可以选择@AspectJ或XML样式。有各种各样的权衡要考虑。
现有的Spring用户可能最熟悉XML样式,它由真正的pojo支持。当使用AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个好的测试是,您是否将切入点表达式视为您可能希望独立更改的配置的一部分)。使用XML样式,从您的配置可以更清楚地看出系统中存在哪些方面。
XML样式有两个缺点。首先,它没有将它所处理的需求的实现完全封装在一个地方。DRY原则认为,在一个系统中,任何知识都应该有一个单一的、明确的、权威的表示。在使用XML样式时,有关需求如何实现的知识在支持bean类的声明和配置文件中的XML之间进行划分。当您使用@AspectJ样式时,这些信息被封装在一个单独的模块中:方面。其次,XML样式在它所能表达的方面比@AspectJ样式稍受限制:只支持“单例”方面实例化模型,不可能组合在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样式支持额外的实例化模型和更丰富的切入点组合。它的优点是将方面作为一个模块单元。它还有一个优点,即@AspectJ方面可以被Spring AOP和AspectJ理解(从而使用)。因此,如果您以后决定需要AspectJ的功能来实现额外的需求,那么可以轻松地迁移到经典的AspectJ设置。总的来说,除了简单的企业服务配置之外,Spring团队更喜欢使用@AspectJ风格的自定义方面。
==Mixing Aspect Types
通过使用自动代理支持、模式定义的<aop:aspect>方面、<aop:advisor>声明的顾问、甚至在相同配置中的其他样式的代理和拦截器,完全可以混合使用@AspectJ样式方面。所有这些都是通过使用相同的底层支持机制实现的,并且可以毫无困难地共存。
= =代理机制
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>元素的代理目标类属性设置为true,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
多个<aop:config/>部分在运行时被分解成一个统一的自动代理创建者,它应用指定的任何<aop:config/>部分(通常来自不同的XML bean定义文件)指定的最强大的代理设置。这也适用于<tx:注释驱动/>和<aop:aspectj-autoproxy/>元素。
需要说明的是,在<tx:annotation-driven/>上使用proxy-target-class="true", <aop:aspectj-autoproxy/>,或<aop:config/>元素强制使用这三个CGLIB代理。
===理解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...
}
}
如果你在一个对象引用上调用了一个方法,该方法将直接在该对象引用上调用,如下图和清单所示:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
当客户机代码的引用是一个代理时,情况会发生轻微的变化。考虑下面的关系图和代码片段:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
这里需要理解的关键是main类的main(..)方法中的客户机代码有一个对代理的引用。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(在本例中是SimplePojo引用),它可能对自身执行的任何方法调用,如this.bar()或this.foo(),都将针对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框架。
==通过编程创建@AspectJ代理
除了通过使用<aop:config>或<aop:aspectj-autoproxy>在配置中声明方面之外,还可以通过编程方式创建通知目标对象的代理。有关Spring AOP API的完整细节,请参见 next chapter。在这里,我们希望关注通过使用@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。
==在Spring应用程序中使用AspectJ
到目前为止,我们在这一章中所讨论的一切都是纯Spring AOP。在本节中,我们将讨论如何使用AspectJ编译器或weaver来代替或补充Spring AOP,如果您的需要超出了Spring AOP所提供的功能。
Spring附带了一个小型的AspectJ方面库,可以在发行版中以Spring -aspect .jar的形式单独使用。您需要将其添加到类路径中,以便使用其中的方面。讨论这个库的内容以及如何使用它。讨论如何依赖注入使用AspectJ编译器编织的AspectJ方面。最后,[aop- j-ltw]介绍了使用AspectJ的Spring应用程序的加载时编织。
===使用AspectJ来使用Spring注入域对象
Spring容器实例化和配置在应用程序上下文中定义的bean。如果bean定义的名称包含要应用的配置,也可以要求bean工厂配置已存在的对象。spring-aspects.jar包含一个注释驱动的方面,它利用这个功能来支持任何对象的依赖注入。该支持用于在任何容器的控制之外创建的对象。域对象通常属于这一类别,因为它们通常是由新操作符以编程方式创建的,或者是由ORM工具作为数据库查询的结果创建的。
@Configurable
注解将一个类标记为适合于spring驱动的配置。在最简单的情况下,你可以使用纯它作为一个标记注释,如下例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
当以这种方式用作标记接口时,Spring通过使用与完全限定类型名(com.xyz.myapp.domain.Account)相同的bean定义(通常是原型作用域)配置带注释类型的新实例(在本例中是Account)。由于bean的默认名称是其类型的全限定名,因此声明原型定义的一种方便方法是省略id属性,如下面的示例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果需要显式指定要使用的prototype bean定义的名称,可以在注释中直接指定,如下面的示例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
Spring现在查找一个名为account的bean定义,并将其用作配置新帐户实例的定义。
您还可以使用自动装配来避免必须指定专用的bean定义。要让Spring应用自动装配,请使用@Configurable注解自动装配属性。可以指定@Configurable(autowire=Autowire.BY_TYPE)或@Configurable(autowire=Autowire.BY_NAME分别按类型或按名称自动装配的BY_NAME。另一种选择是,通过字段或方法级别上的@Autowired或@Inject为您的@Configurable bean指定显式的、注释驱动的依赖项注入(有关详细信息,请参阅Annotation-based Container Configuration)。
最后,您可以通过使用dependencyCheck属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))来启用对新创建和配置的对象中的对象引用的Spring依赖项检查)。如果将此属性设置为true,则在配置之后,Spring将验证所有属性(不是原语或集合)都已设置。
注意,单独使用注释没有任何作用。spring-aspects.jar中的AnnotationBeanConfigurerAspect对注释的存在起作用。本质上,方面是这样说的,“在初始化了一个用@Configurable注释来配置新对象之后,使用Spring根据注释的属性配置新创建的对象”。在这个上下文中,“初始化”指的是新实例化的对象(例如,用新操作符实例化的对象),以及正在反序列化的可序列化对象(例如,通过readResolve())。
注意:以上段落中的关键短语之一是“in essence”。对于大多数情况,“从新对象的初始化返回之后”的确切语义是正确的。在这个上下文中,“初始化之后”意味着在对象构造之后注入依赖项。这意味着依赖项在类的构造函数体中不可用。如果您希望在构造函数体执行之前注入依赖项,这样就可以在构造函数体中使用依赖项,那么您需要在@ configurationdeclaration上定义依赖项,如下所示:
@Configurable(preConstruction = true)
在AspectJ编程指南AspectJ Programming Guide的这个附录中 in this appendix,您可以找到关于AspectJ中各种切入点类型的语言语义的更多信息。
要实现这一点,必须使用AspectJ编织器编织带注释的类型。您可以使用构建时Ant或Maven任务来完成此任务(例如,请参阅AspectJ开发环境指南 AspectJ Development Environment Guide),也可以使用加载时编织(参见 [aop-aj-ltw])。AnnotationBeanConfigurerAspect本身需要由Spring配置(以便获得对bean工厂的引用,该引用将用于配置新对象)。如果使用基于java的配置,可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果您喜欢基于XML的配置,Spring上下文命名空间context
namespace定义了一个方便的上下文:Spring配置的元素,您可以使用如下:
<context:spring-configured/>
在配置方面之前创建的@Configurable objects的状态会导致向调试日志发出一条消息,并且不会对该对象进行任何配置。一个例子可能是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支持的目标之一是支持域对象的独立单元测试,而不需要面对与硬编码查找相关的困难。如果@Configurable类型没有被AspectJ编织,那么在单元测试期间注释就没有影响。您可以在被测试的对象中设置模拟或存根属性引用,然后正常进行。如果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中的类型(例如,将spring-aspects.jar放在“WEB-INF/lib”中)。如果spring-aspects.jar只添加到容器范围的类路径中(因此由共享的父类加载器加载),所有web应用程序共享相同的方面实例(这可能不是您想要的)。
=== AspectJ的其他Spring方面
除了@Configurable aspect之外,spring-aspects.jar还包含一个AspectJ方面,您可以使用它来驱动Spring对使用@Transactional注释注释的类型和方法的事务管理。这主要是针对那些希望在Spring容器之外使用Spring框架的事务支持的用户。
解释@Transactional注释的方面是AnnotationTransactionAspect。当您使用这个方面时,您必须注释实现类(或该类中的方法或两者),而不是类实现的接口(如果有的话)。AspectJ遵循Java的规则,接口上的注释不是继承的。
类上的@Transactional注释为类中任何公共操作的执行指定默认的事务语义。
类中的方法上的@Transactional注释覆盖类注释(如果存在)给出的默认事务语义。任何可见性的方法都可以进行注释,包括私有方法。直接注释非公共方法是获得此类方法执行的事务界定的惟一方法。
注意:从Spring Framework 4.2开始,spring-aspects
就提供了一个类似的方面,为标准javax.transaction.Transactional注释提供了完全相同的特性。查看JtaAnnotationTransactionAspect以获得更多细节。
对于希望使用Spring配置和事务管理支持但又不想(或不能)使用注释的AspectJ程序员,spring-aspects.jar还包含一些抽象方面,您可以扩展它们来提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspect和AbstractTransactionAspect方面的源代码。作为一个例子,下面的摘录展示了如何编写一个方面来配置域模型中定义的所有对象实例,方法是使用原型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);
}
===使用Spring IoC配置AspectJ方面
当您在Spring应用程序中使用AspectJ方面时,很自然地希望和期望能够用Spring配置这些方面。AspectJ运行时本身负责方面的创建,通过Spring配置AspectJ创建的方面的方法取决于方面使用的AspectJ实例化模型(per-xxx子句)。
AspectJ方面的大部分都是单例方面。配置这些方面很容易。您可以创建一个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>
//1 注意factory-method="aspectOf"属性
如果你有@ AspectJ方面你想编织与AspectJ域模型(例如,使用装入时编织类型)和其他您想要使用Spring AOP @ AspectJ方面,而这些方面都是配置在Spring,你需要告诉Spring AOP @ AspectJ auto-proxying @ AspectJ方面的支持,它精确子集定义的配置应该用于auto-proxying。可以通过在<aop:aspectj-autoproxy/>声明中使用一个或多个<include/>元素来实现这一点。每个<include/>元素指定一个名称模式,只有名称至少与其中一个模式匹配的bean才用于Spring AOP自动代理配置。下面的例子展示了如何使用<include/>元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
注意:不要被<aop:aspectj-autoproxy/>元素的名称所误导。使用它可以创建Spring AOP代理。这里使用了@AspectJ风格的方面声明,但是没有涉及AspectJ运行时。
===在Spring框架中使用AspectJ进行加载时编织
加载时编织(LTW)是指在将AspectJ方面加载到Java虚拟机(JVM)时,将它们编织到应用程序的类文件中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是LTW的一般介绍。有关LTW细节和仅使用AspectJ配置LTW(完全不涉及Spring)的详细信息,请参阅 LTW section of the AspectJ Development Environment Guide。
Spring框架给AspectJ LTW带来的价值在于对编织过程实现了更细粒度的控制。“普通的”AspectJ LTW是通过使用Java(5+)代理实现的,在启动JVM时通过指定VM参数打开代理。因此,它是一个jvm范围的设置,在某些情况下它可能很好,但通常有点太粗糙了。启用了spring的LTW允许您在每个类加载器的基础上切换LTW,这是一种更细粒度的方式,在“单jvm -多应用程序”环境中更有意义(例如在典型的应用服务器环境中)。
此外,在某些环境中 in certain environments,这种支持支持加载时编织,而不需要对应用服务器的启动脚本进行任何修改,以添加-javaagent:path/to/aspectjweaver.jar或(如我们在本节后面所述)-javaagent:path/to/spring-instrument.jar。开发人员配置应用程序上下文以启用加载时编织,而不是依赖通常负责部署配置(如启动脚本)的管理员。
既然推销已经结束了,那么让我们先简单介绍一个使用Spring的AspectJ LTW的快速示例,然后再详细介绍示例中引入的元素。有关完整的示例,请参见 Petclinic sample application。
====第一个例子
假设您是一名应用程序开发人员,您的任务是诊断系统中某些性能问题的原因。我们将打开一个简单的概要分析方面,使我们能够快速获得一些性能指标,而不是使用一个概要分析工具。然后,我们可以立即在该特定区域应用更细粒度的分析工具。
注意:这里提供的示例使用XML配置。您还可以使用Java configuration使用@AspectJ。具体来说,您可以使用@EnableLoadTimeWeaving注释作为<context:load-time-weaver/>的替代(参见下面的详细信息)。
下面的示例显示了分析方面,这并不复杂。它是一个基于时间的分析器,使用@AspectJ-style的aspect声明:
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文件:
<!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>
现在,所有需要的工件(aspect、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在每个类装载器的基础上有选择地打开LTW,这是真的。但是,对于本例,我们使用Java代理(由Spring提供)打开LTW。我们使用以下命令来运行前面显示的主类:
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
-javaagent是一个标志,用于指定和启用代理来检测在JVM上运行的程序 agents to instrument programs that run on the JVM。Spring框架附带了这样一个代理,即InstrumentationSavingAgent,它打包在spring-instrument.jar中,在前面的示例中作为-javaagent参数的值提供。
主程序执行的输出与下面的示例类似。(我已经在calculateEntitlement()实现中引入了Thread.sleep(..)语句,所以分析器实际上捕获的不是0毫秒(01234毫秒不是AOP引入的开销)。下面的清单显示了我们在运行分析器时得到的输出:
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
由于这个LTW是通过使用成熟的AspectJ实现的,所以我们不仅限于通知Spring bean。主程序上的以下微小变化产生了相同的结果:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
请注意,在前面的程序中,我们是如何引导Spring容器的,然后完全在Spring的上下文中创建StubEntitlementCalculationService的新实例的。分析建议仍然被纳入其中。
诚然,这个例子过于简单。但是,在前面的示例中已经介绍了Spring中LTW支持的基础,本节的其余部分将详细解释每个配置和使用背后的“原因”。
注意:本例中使用的ProfilingAspect可能是基本的,但是非常有用。这是一个很好的开发时方面的例子,开发人员可以在开发期间使用它,然后很容易地从部署到UAT或生产中的应用程序的构建中排除它。
==== Aspects
在LTW中使用的方面必须是AspectJ方面。您可以用AspectJ语言本身来编写它们,也可以用@AspectJ风格来编写方面。这样,方面就是有效的AspectJ和Spring AOP方面。此外,编译后的方面类需要在类路径上可用。
==== 'META-INF/aop.xml'
AspectJ LTW基础结构是通过使用Java类路径上的一个或多个META-INF/aop.xml文件来配置的(直接地或者更典型地,在jar文件中)。
这个文件的结构和内容在AspectJ参考文档的LTW AspectJ reference documentation部分中有详细说明。因为aop.xml文件是100% AspectJ的,所以我们不在这里进一步描述它。
===所需的库(jars)
至少,您需要以下库来使用Spring框架对AspectJ LTW的支持:
-
spring-aop.jar
-
aspectjweaver.jar
如果您使用spring提供的代理 Spring-provided agent to enable instrumentation来启用插装,您还需要:
- spring-instrument.jar
= = = = Spring配置
Spring的LTW支持中的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading中)。以及Spring发行版附带的许多实现。LoadTimeWeaver负责添加一个或多个java.lang.instrument.ClassFileTransformers。在运行时将classfiletransformer转换为ClassLoader,这为各种有趣的应用程序打开了大门,其中之一就是方面的LTW。
注意:如果您不熟悉运行时类文件转换的概念,请参阅java.lang的javadoc API文档java.lang.instrument。虽然该文档并不全面,但至少可以看到关键接口和类(在阅读本节时作为参考)。
为特定的ApplicationContext配置LoadTimeWeaver就像添加一行代码一样简单。(请注意,您几乎肯定需要使用ApplicationContext作为Spring容器—通常,BeanFactory是不够的,因为LTW支持使用了BeanFactoryPostProcessors。)
要启用Spring框架的LTW支持,您需要配置LoadTimeWeaver,这通常是通过使用@EnableLoadTimeWeaving注释来完成的,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
或者,如果喜欢基于xml的配置,可以使用<context:load-time-weaver/>元素。请注意,元素是在上下文名称空间中定义的。下面的例子展示了如何使用<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实现:
Runtime Environment | LoadTimeWeaver implementation |
---|---|
Running in Apache Tomcat |
|
Running in GlassFish (limited to EAR deployments) |
|
|
|
Running in IBM’s WebSphere |
|
Running in Oracle’s WebLogic |
|
JVM started with Spring |
|
Fallback, expecting the underlying ClassLoader to follow common conventions (namely |
|
注意,该表只列出了在使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWeavers。您可以精确地指定使用哪个LoadTimeWeaver实现。
要使用Java配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆盖getLoadTimeWeaver()方法。下面的例子指定了一个ReflectiveLoadTimeWeaver:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
如果使用基于xml的配置,可以将完全限定的classname指定为<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基础结构添加一个或多个classfiletransformer的机制而存在。执行LTW的实际ClassFileTransformer是ClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包)类。请参阅ClassPreProcessorAgentAdapter类的类级别javadoc以获得更多的详细信息,因为编织实际如何实现的细节超出了本文的范围。
配置还有最后一个属性需要讨论:aspectjweave属性(如果使用XML,也可以称为aspectj- weave)。该属性控制是否启用LTW。它接受三个可能的值中的一个,如果属性不存在,默认值是自动检测。下表总结了三种可能的值:
Annotation Value | XML Value | Explanation |
---|---|---|
|
|
启动了AspectJ编织,并在适当的加载时编织方面。 |
|
|
LTW关闭。加载时没有编织方面。 |
|
|
如果Spring LTW基础结构能够找到至少一个META-INF/aop.xml文件,那么AspectJ编织就开始了。否则,它是关闭的。这是默认值。 |
= = = =特定于环境的配置
最后一节包含在应用服务器和web容器等环境中使用Spring的LTW支持时需要的任何其他设置和配置。
==== Tomcat, JBoss, WebSphere, WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server和Oracle WebLogic Server都提供了能够进行本地检测的通用应用程序类加载器。Spring的本地LTW可以利用那些类加载器实现来提供AspectJ编织。如前所述,您可以简单地启用 described earlier加载时编织。具体来说,您不需要修改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设置。
= =更多的资源
有关AspectJ的更多信息可以在AspectJ网站AspectJ website上找到。
由Adrian Colyer等人(Addison-Wesley, 2005)编写的Eclipse AspectJ为AspectJ语言提供了全面的介绍和参考。
AspectJ在行动,第二版由Ramnivas Laddad(曼宁,2009)高度推荐。这本书的重点是在AspectJ上,但是也讨论了很多一般的AOP主题(有一定的深度)。
= Spring AOP api
前一章描述了Spring通过@AspectJ和基于模式的方面定义对AOP的支持。在本章中,我们将讨论更低级别的Spring AOP api。对于常见的应用程序,我们建议使用Spring AOP和AspectJ切入点,如前一章所述。
== Spring中的切入点API
本节描述Spring如何处理关键的切入点概念。
= = =概念
Spring的切入点模型支持独立于通知类型的切入点重用。您可以使用相同的切入点针对不同的通知。
org.springframework.aop.Pointcut接口是中心接口,用于将通知定向到特定的类和方法。完整界面如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
将切入点接口分割成两个部分允许重用与部分匹配的类和方法以及细粒度的组合操作(例如与另一个方法匹配器执行“union”)。
ClassFilter接口用于将切入点限制到一组给定的目标类。如果matches()方法总是返回true,则匹配所有目标类。下面的清单显示了ClassFilter接口定义:
public interface ClassFilter {
boolean matches(Class clazz);
}
MethodMatcher接口通常更重要。完整界面如下:
public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
boolean isRuntime();
boolean matches(Method m, Class targetClass, Object[] args);
}
matches(方法、类)方法用于测试这个切入点是否匹配目标类上的给定方法。在创建AOP代理以避免对每个方法调用进行测试时,可以执行这个评估。如果两个参数匹配的方法为给定的方法返回true,而方法matcher的isRuntime()方法返回true,那么在每次方法调用时都会调用三个参数匹配的方法。这让切入点在执行目标通知之前查看传递给方法调用的参数。
大多数MethodMatcher实现都是静态的,这意味着它们的isRuntime()方法返回false。在这种情况下,永远不会调用三参数匹配方法。
注意:如果可能的话,尝试让切入点成为静态的,从而允许AOP框架在创建AOP代理时缓存切入点计算的结果。
===切入点上的操作
Spring支持切入点上的操作(特别是union和交集)。
Union表示两个切入点匹配的方法。交集意味着两个切入点匹配的方法。联合通常更有用。您可以通过使用org.springframework.aop.support.Pointcuts类中的静态方法来组合切入点。或者在同一个包中使用ComposablePointcut类。但是,使用AspectJ切入点表达式通常是一种更简单的方法。
=== AspectJ表达式切入点
自2.0以来,Spring使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut。这是一个切入点,它使用AspectJ提供的库来解析AspectJ切入点表达式字符串。
有关受支持的AspectJ切入点原语的讨论,请参阅前一章 previous chapter。
===方便的切入点实现
Spring提供了几个方便的切入点实现。你可以直接使用其中的一些。其他的则打算在特定于应用程序的切入点中子类化。
= = = =静态的切入点
静态切入点基于方法和目标类,不能考虑方法的参数。对于大多数使用来说,静态切入点已经足够了,而且是最好的。Spring只能在第一次调用方法时对静态切入点求值一次。之后,就不需要在每次方法调用时重新计算切入点了。
本节的其余部分将介绍Spring中包含的一些静态切入点实现。
====正则表达式切入点
指定静态切入点的一种明显的方法是正则表达式。除了Spring之外,还有几个AOP框架使这成为可能。org.springframework.aop.support。JdkRegexpMethodPointcut是一个通用的正则表达式切入点,它使用JDK中的正则表达式支持。
使用JdkRegexpMethodPointcut类,您可以提供模式字符串的列表。如果其中任何一个匹配,切入点的计算结果为true。(因此,结果就是这些切入点的有效结合。)
下面的例子展示了如何使用JdkRegexpMethodPointcut:
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
Spring提供了一个名为RegexpMethodPointcutAdvisor的便利类,它允许我们也引用一个Advice(请记住,一个通知可以是一个拦截器,在通知之前,抛出通知,以及其他)。在幕后,Spring使用JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor简化了连接,因为一个bean封装了切入点和Advice,如下面的例子所示:
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
您可以对任何Advice类型使用RegexpMethodPointcutAdvisor。
= = = = = Attribute-driven切入点
静态切入点的一个重要类型是元数据驱动的切入点。它使用元数据属性的值(通常是源级元数据)。
= = = =动态的切入点
评估动态切入点比评估静态切入点的成本更高。它们考虑方法参数和静态信息。这意味着必须在每次方法调用时对它们求值,并且不能缓存结果,因为参数会有所不同。
主要的例子是控制流切入点。
====控制流切入点
Spring控制流切入点在概念上类似于AspectJ cflow切入点,尽管功能没那么强大。(目前没有办法指定一个切入点在与另一个切入点匹配的连接点下面执行。)控制流切入点与当前调用堆栈匹配。例如,如果连接点被com.mycompany中的方法调用,则可能com.mycompany.web或SomeCaller类。控制流切入点是通过使用org.springframework.aop.support.ControlFlowPointcut类。
注意:控制流切入点在运行时的计算成本比其他动态切入点要高得多。在Java 1.4中,成本大约是其他动态切入点的5倍。
= = =切入点超类
Spring提供了有用的切入点超类来帮助您实现自己的切入点。
因为静态切入点最有用,所以您可能应该继承StaticMethodMatcherPointcut的子类。这只需要实现一个抽象方法(尽管您可以覆盖其他方法来定制行为)。下面的例子展示了如何子类化StaticMethodMatcherPointcut:
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
还有用于动态切入点的超类。您可以对任何通知类型使用自定义切入点。
= = =自定义切入点
因为在Spring AOP中切入点是Java类,而不是语言特性(如在AspectJ中),所以可以声明定制的切入点,不管是静态的还是动态的。Spring中的定制切入点可以是任意复杂的。但是,如果可以的话,我们建议使用AspectJ切入点表达式语言。
注意:Spring的后续版本可能提供对JAC所提供的“语义切入点”的支持——例如,“更改目标对象中的实例变量的所有方法”。
==Advice API in Spring
现在我们可以研究Spring AOP如何处理通知。
= = =Advice 生命周期
每个建议都是一个Spring bean。一个通知实例可以在所有被通知对象之间共享,也可以是每个被通知对象的唯一实例。这对应于每个类或每个实例的通知。
每个类的建议是最常用的。它适用于一般的建议,例如事务顾问。它们不依赖于代理对象的状态或添加新状态。它们只是对方法和参数起作用。
每个实例的建议适用于介绍,以支持混合。在本例中,通知将状态添加到代理对象。
可以在同一个AOP代理中混合使用共享和每个实例的通知。
===通知类型
Spring提供了几种通知类型,并且可以扩展以支持任意的通知类型。本节描述基本概念和标准通知类型。
====拦截通知
Spring中最基本的通知类型是围绕通知的拦截。
Spring与AOP Alliance接口兼容,用于提供使用方法拦截的周围通知。实现MethodInterceptor和around advice的类也应该实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke()方法的方法调用参数将被调用的方法、目标连接点、AOP代理和参数暴露给方法。invoke()方法应该返回调用的结果:连接点的返回值。
下面的例子展示了一个简单的MethodInterceptor实现:
public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
注意对MethodInvocation的proceed()方法的调用。这将继续沿着拦截器链向连接点移动。大多数拦截器调用此方法并返回其返回值。但是,与任何around通知一样,MethodInterceptor可以返回不同的值或抛出异常,而不是调用proceed方法。然而,如果没有充分的理由,你是不会想这么做的。
注意:MethodInterceptor实现提供了与其他AOP联盟兼容的AOP实现的互操作性。本节其余部分讨论的其他通知类型实现了常见的AOP概念,但是是以特定于spring的方式实现的。虽然使用最特定的通知类型有一个优点,但是如果您可能希望在另一个AOP框架中运行方面,请坚持使用围绕通知的MethodInterceptor。注意,切入点目前不是框架之间的互操作,AOP联盟目前也没有定义切入点接口。
= = = =Before Advice
更简单的通知类型是before通知。这并不需要一个MethodInvocation对象,因为它只在进入方法之前被调用。
before通知的主要优点是不需要调用proceed()方法,因此不可能无意中沿着拦截器链继续下去。
下面的清单显示了MethodBeforeAdvice接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
(Spring的API设计允许字段先于通知,尽管通常的对象适用于字段拦截,而且Spring不太可能实现它。)
注意,返回类型为void。Before通知可以在连接点执行之前插入自定义行为,但不能更改返回值。如果before通知抛出异常,它将中止拦截器链的进一步执行。异常传播回拦截器链。如果未选中它,或者调用方法的签名未选中它,则直接将其传递给客户机。否则,它会被AOP代理包装在一个未检查的异常中。
下面的例子显示了Spring中的before通知,它计算了所有方法调用:
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
注意:在通知可以与任何切入点一起使用之前。
= = = =Throws Advice
如果连接点抛出异常,则在连接点返回后调用抛出建议。Spring提供了类型化的抛出建议。注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标记接口,标识给定对象实现一个或多个类型化抛出通知方法。这些文件的格式如下:
afterThrowing([Method, args, target], subclassOfThrowable)
只需要最后一个参数。方法签名可能有一个或四个参数,这取决于advice方法是否对方法和参数感兴趣。下面的两个清单显示了抛出建议的示例类。
如果抛出RemoteException(包括来自子类的异常),则调用以下通知:
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
与前面的通知不同,下一个示例声明了四个参数,因此它可以访问被调用的方法、方法参数和目标对象。如果抛出ServletException,将调用以下建议:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
最后一个示例演示了如何在一个同时处理RemoteException和ServletException的类中使用这两个方法。在一个类中可以组合任意数量的抛出建议方法。下面是最后一个例子:
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
注意:如果抛出建议方法本身抛出异常,它将覆盖原来的异常(也就是说,它将更改抛出给用户的异常)。覆盖的异常通常是一个RuntimeException,它与任何方法签名兼容。但是,如果一个throws-advice方法抛出一个已检查的异常,那么它必须与目标方法声明的异常匹配,因此在某种程度上耦合到特定的目标方法签名。不要抛出与目标方法的签名不兼容的未声明的已检查异常!
抛出通知可以与任何切入点一起使用。
====After Returning Advice
在Spring中返回通知后必须实现org.springframe .aop。AfterReturningAdvice接口,如下所示:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
返回通知可以访问返回值(不能修改)、调用的方法、方法的参数和目标。
下面的返回通知计数所有成功的方法调用没有抛出异常:
public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
此建议不会更改执行路径。如果抛出异常,则抛出拦截器链而不是返回值。
注意:返回后的通知可以与任何切入点一起使用。
= = = =Advice介绍
Spring将介绍建议视为一种特殊的拦截建议。
需要一个IntroductionAdvisor
和IntroductionInterceptor
来实现以下接口:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
继承自AOP Alliance MethodInterceptor接口的invoke()方法必须实现引入。也就是说,如果被调用的方法位于已引入的接口上,则引入拦截器负责处理方法调用—它不能调用proceed()。
引入通知不能与任何切入点一起使用,因为它只适用于类,而不是方法级别。你只能在introduction advisor中使用introduction advice,它有以下方法:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class[] getInterfaces();
}
没有方法匹配器,因此,没有与引入建议相关联的切入点。只有类过滤是合乎逻辑的。
getInterfaces()方法返回这个顾问所引入的接口。
内部使用validateInterfaces()方法来查看所引入的接口是否可以由配置的介绍性拦截器实现。
考虑一个来自Spring测试套件的例子,假设我们想要向一个或多个对象引入以下接口:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
这说明了一个混合。我们希望能够将被建议的对象强制转换为Lockable,无论它们的类型是什么,并调用lock和unlock方法。如果我们调用lock()方法,我们希望所有setter方法都抛出一个LockedException。因此,我们可以添加一个方面,它提供了使对象在不了解对象的情况下变得不可变的能力:AOP的一个很好的例子。
首先,我们需要一个能够挑起重任的拦截器IntroductionInterceptor。在本例中,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor类。我们可以直接实现IntroductionInterceptor
,但是在大多数情况下使用委派式拦截器DelegatingIntroductionInterceptor
是最好的。
委托中介拦截器DelegatingIntroductionInterceptor被设计成将中介委托给所引入接口的实际实现,隐藏了拦截的使用。可以使用构造函数参数将委托设置为任何对象。默认委托(使用无参数构造函数时)是这样的。因此,在下一个例子中,委托是delegate的LockMixin子类。对于给定的委托(默认情况下是其本身),一个delegating介绍性拦截器实例会查找委托实现的所有接口(介绍性拦截器除外),并支持针对其中任何一个进行介绍。像LockMixin这样的子类可以调用suppressInterface(类intf)方法来抑制不应该公开的接口。但是,不管一个介绍性拦截器准备支持多少个接口,介绍性advisor都会控制哪些接口实际上是公开的。方法隐藏同一接口的任何实现。
因此,LockMixin扩展了委托导入拦截器,并实现了自身的Lockable。超类会自动获得可锁定的支持,所以我们不需要指定它。我们可以用这种方式引入任意数量的接口。
注意锁定实例变量的使用。这有效地将额外的状态添加到目标对象中。
下面的例子展示了LockMixin类的例子:
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
通常,您不需要覆盖invoke()方法。通常,委派中介拦截器实现(如果方法被引入,它将调用委托方法,否则将继续向连接点前进)就足够了。在本例中,我们需要添加一个check:如果处于锁定模式,则不能调用任何setter方法。
所需的引入只需要持有一个不同的LockMixin实例并指定引入的接口(在本例中,仅为Lockable)。一个更复杂的例子可能引用引入拦截器(它将被定义为原型)。在本例中,没有与LockMixin相关的配置,因此我们使用new来创建它。下面的例子展示了我们的LockMixinAdvisor类:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
我们可以非常简单地应用这个advisor工具,因为它不需要配置。(但是,如果没有一个介绍人顾问,就不可能使用介绍人拦截器。)与介绍一样,advisor工具必须是每个实例的,因为它是有状态的。对于每个被建议的对象,我们需要一个不同的LockMixinAdvisor实例,因此也需要一个LockMixin实例。advisor工具包含被建议对象的部分状态。
我们可以通过在XML配置中使用Advised.addAdvisor()方法或(推荐的方法)以编程方式应用这个advisor,就像其他任何advisor一样。下面讨论的所有代理创建选项,包括“自动代理创建者”,正确地处理引入和有状态混合。
== Spring中的Advisor API
在Spring中,Advisor是一个方面,它只包含一个与切入点表达式相关联的advice对象。
除了介绍的特殊情况外,任何advisor工具都可以用于任何建议。org.springframework.aop.support。DefaultPointcutAdvisor是最常用的advisor类。它可以与MethodInterceptor、BeforeAdvice或ThrowsAdvice一起使用。
在同一个AOP代理中,可以在Spring中混合advisor和advice类型。例如,您可以在一个代理配置中使用围绕通知、抛出通知和before通知的拦截。Spring自动创建必要的拦截器链。
==使用ProxyFactoryBean来创建AOP代理
如果您将Spring IoC容器(一个ApplicationContext或BeanFactory)用于您的业务对象(您应该这样做!),那么您希望使用Spring的一个AOP FactoryBean实现。(请记住,工厂bean引入了一个间接层,让它创建不同类型的对象。)
注意:Spring AOP支持还在幕后使用工厂bean。
在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这样就可以完全控制切入点、应用的任何建议以及它们的顺序。但是,如果您不需要这样的控制,有一些更简单的选项是更好的选择。
= = =基础知识
与其他Spring FactoryBean实现一样,ProxyFactoryBean引入了一个间接的级别。如果定义一个名为foo的ProxyFactoryBean,那么引用foo的对象不会看到ProxyFactoryBean实例本身,而是通过实现ProxyFactoryBean中的getObject()方法创建的对象。此方法创建包装目标对象的AOP代理。
使用ProxyFactoryBean或另一个支持ioco的类来创建AOP代理的最重要的好处之一是通知和切入点也可以由IoC管理。这是一个强大的特性,支持某些在其他AOP框架中难以实现的方法。例如,通知本身可以引用应用程序对象(除了在任何AOP框架中都应该可用的目标之外),从而受益于依赖项注入提供的所有可插拔性。
= = = JavaBean属性
与Spring提供的大多数FactoryBean实现一样,ProxyFactoryBean类本身就是JavaBean。其性质用于:
- 指定要代理的目标。
- 指定是否使用CGLIB(稍后进行描述,请参阅 [aop-pfb-proxy-types])。
一些关键属性继承自org.springframework.aop.framework.ProxyConfig(Spring中所有AOP代理工厂的超类)。这些关键属性包括:
- proxyTargetClass:如果要代理的是目标类,而不是目标类的接口,则为true。如果此属性值设置为true,则创建CGLIB代理(但也请参阅 [aop-pfb-proxy-types])。
- optimize:控制是否对通过CGLIB创建的代理应用积极的优化。除非完全理解相关AOP代理如何处理优化,否则不应该轻率地使用这个设置。这目前仅用于CGLIB代理。它对JDK动态代理没有影响。
- frozen:如果冻结了代理配置,则不再允许更改配置。这对于轻微的优化和在创建代理之后不希望调用者能够(通过建议的接口)操作代理的情况都很有用。此属性的默认值为false,因此允许更改(如添加其他通知)。
- exposeProxy:确定当前代理是否应该在ThreadLocal中公开,以便目标可以访问它。如果目标需要获取代理,并且将曝光代理属性设置为true,那么目标可以使用AopContext.currentProxy()方法。
ProxyFactoryBean的其他特性包括:
- proxyInterfaces:一个字符串接口名数组。如果没有提供,则使用目标类的CGLIB代理(但也请参阅[aop-pfb-proxy-types])。
- interceptorNames:要应用的Advisor、interceptor或其他通知名称的字符串数组。订购是重要的,以先到先得为基础。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。
这些名称是当前工厂中的bean名称,包括来自祖先工厂的bean名称。这里不能提到bean引用,因为这样做会导致ProxyFactoryBean忽略通知的单例设置。
您可以使用星号(*)附加拦截器名称。这样做会导致应用所有advisor bean,它们的名称都以要应用的星号前面的部分开始。您可以在 [aop-global-advisors]中找到使用此功能的示例。
- singleton:不管getObject()方法被调用多少次,工厂是否应该返回单个对象。有几个FactoryBean实现提供了这样的方法。默认值为true。如果您想要使用有状态的通知—例如,对于有状态的混合—使用prototype advice和一个单例值false。
=== JDK-和基于cglib的代理
本节是关于ProxyFactoryBean如何为特定目标对象(要代理的对象)创建基于jdk的代理或基于cglib的代理的权威文档。
注意:ProxyFactoryBean在创建JDK或基于cglib的代理方面的行为在版本1.2之间发生了变化。x和2。0的弹簧。在自动检测接口方面,ProxyFactoryBean现在表现出与TransactionProxyFactoryBean类类似的语义。
如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则创建一个基于cglib的代理。这是最简单的场景,因为JDK代理是基于接口的,没有接口意味着JDK代理甚至是不可能的。您可以插入目标bean并通过设置interceptorNames属性指定拦截器列表。注意,即使ProxyFactoryBean的proxyTargetClass属性被设置为false,也会创建一个基于cglib的代理。(这样做毫无意义,最好从bean定义中删除,因为它在最好的情况下是冗余的,在最坏的情况下是混乱的。)
如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。
如果ProxyFactoryBean的proxyTargetClass属性被设置为true,就会创建一个基于cglib的代理。这是有道理的,也符合最少意外原则。即使ProxyFactoryBean的proxyInterfaces属性被设置为一个或多个完全限定的接口名,proxyTargetClass属性被设置为true的事实也会导致基于cglib的代理生效。
如果ProxyFactoryBean的proxyInterfaces属性被设置为一个或多个完全限定的接口名,那么将创建一个基于jdk的代理。创建的代理实现在proxyInterfaces属性中指定的所有接口。如果目标类实现的接口比在proxyInterfaces属性中指定的接口多得多,这是很好的,但是那些额外的接口没有由返回的代理实现。
如果没有设置ProxyFactoryBean的proxyInterfaces属性,但是目标类实现了一个(或多个)接口,那么ProxyFactoryBean将自动检测目标类确实实现了至少一个接口,并创建一个基于jdk的代理。实际代理的接口是目标类实现的所有接口。实际上,这与向proxyInterfaces属性提供目标类实现的每个接口的列表是一样的。然而,它大大减少了工作量,而且更不容易出现印刷错误。
= = =代理接口
考虑一个ProxyFactoryBean的简单例子。这个例子包括:
- 代理的目标bean。这是示例中的personTarget bean定义。
- 用于提供建议的
Advisor
和Interceptor
。 - 一个指定目标对象(
personTarget
bean)、代理接口和应用建议的AOP代理bean定义。
下面的清单显示了示例:
<bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<property name="target" ref="personTarget"/>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
请注意,interceptorNames属性接受一个字符串列表,其中包含当前工厂中拦截器或顾问的bean名称。您可以使用advisors, interceptors, before, after returning和throws advice objects。顾问的顺序很重要。
注意:您可能想知道为什么这个列表不包含bean引用。这样做的原因是,如果ProxyFactoryBean的单例属性设置为false,那么它必须能够返回独立的代理实例。如果顾问本身就是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。持有引用是不够的。
前面显示的person bean定义可以用来代替person实现,如下所示:
Person person = (Person) factory.getBean("person");
与普通Java对象一样,在相同的IoC上下文中的其他bean可以表示对它的强类型依赖。下面的例子演示了如何做到这一点:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
本例中的PersonUser类公开了Person类型的属性。就AOP而言,可以透明地使用AOP代理来代替“真正的”人员实现。但是,它的类将是一个动态代理类。可以将其转换为建议的接口(稍后讨论)。
您可以使用匿名内部bean来隐藏目标和代理之间的区别。只有ProxyFactoryBean的定义不同。建议仅为完整性而包含。下面的例子展示了如何使用匿名内部bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
使用匿名内部bean的优点是只有一个Person类型的对象。如果我们希望防止应用程序上下文的用户获得对未通知对象的引用,或者需要避免与Spring IoC自动装配之间的任何不确定性,这是非常有用的。还有一个优点是ProxyFactoryBean定义是自包含的,这是有争议的。然而,有时能够从工厂获得未建议的目标实际上可能是一种优势(例如,在某些测试场景中)。
= = =代理类
如果需要代理一个类,而不是一个或多个接口,该怎么办?
想象一下,在我们前面的例子中,没有Person接口。我们需要通知一个名为Person的类,它没有实现任何业务接口。在这种情况下,您可以配置Spring来使用CGLIB代理,而不是动态代理。为此,将前面显示的ProxyFactoryBean上的proxyTargetClass属性设置为true。虽然最好是对接口而不是类进行编程,但是在处理遗留代码时,建议不实现接口的类的能力可能很有用。(一般来说,春天是没有规定性的。虽然它使应用良好的实践变得容易,但它避免强制采用特定的方法。
如果愿意,您可以在任何情况下强制使用CGLIB,即使您有接口。
CGLIB代理通过在运行时生成目标类的子类来工作。Spring将这个生成的子类配置为将方法调用委托给原始目标。子类用于实现装饰器模式,在通知中编织。
CGLIB代理通常应该对用户透明。但是,有一些问题需要考虑:
- 不能建议使用最后的方法,因为它们不能被覆盖。
- 没有必要将CGLIB添加到类路径中。从Spring 3.2开始,CGLIB被重新打包幷包含在Spring -core JAR中。换句话说,与JDK动态代理一样,基于cglib的AOP工作“开箱即用”。
CGLIB代理和动态代理之间的性能差别不大。在这种情况下,业绩不应是决定性的考虑因素。
===使用“全局”Advisors
通过将一个星号附加到一个拦截器名称,所有具有与星号前部分匹配的bean名称的顾问都被添加到顾问链中。如果您需要添加一组标准的“global”advisors,那么这将非常方便。下面的例子定义了两个全局顾问:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
==简洁的代理定义
特别是在定义事务代理时,您可能会得到许多类似的代理定义。使用父bean和子bean定义,以及内部bean定义,可以产生更清晰、更简洁的代理定义。
首先,我们为代理创建一个父类、模板、bean定义,如下:
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
它从来没有实例化过,所以它实际上是不完整的。然后,需要创建的每个代理都是一个子bean定义,它将代理的目标包装为内部bean定义,因为无论如何都不会单独使用目标。下面的例子展示了这样一个子bean:
<bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>
您可以覆盖来自父模板的属性。在下面的例子中,我们覆盖了事务传播设置:
<bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
请注意,在父bean示例中,我们通过将abstract属性设置为true(如前所述)显式地将父bean定义标记为抽象,这样就不会实际实例化它。默认情况下,应用程序上下文(但不是简单的bean工厂)预先实例化所有单例。因此,重要的是(至少对於单例bean来说),如果您有一个(父)bean定义,您只打算将其用作模板,并且这个定义指定了一个类,那么您必须确保将抽象属性设置为true。否则,应用程序上下文实际上会尝试预实例化它。
==使用ProxyFactory以编程方式创建AOP代理
使用Spring以编程方式创建AOP代理是很容易的。这使您可以在不依赖于Spring IoC的情况下使用Spring AOP。
由目标对象实现的接口被自动代理。下面的清单显示了一个目标对象的代理的创建,其中有一个拦截器和一个advisor工具:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
第一步是构造一个org.springframework.aop.framework.ProxyFactory类型的对象。您可以使用目标对象(如前面的示例)来创建它,或者在备用构造函数中指定要代理的接口。
您可以添加advice(将拦截器作为一种专门的建议)、advisor,或者两者都可以,并在ProxyFactory的生命周期中对它们进行操作。如果您添加了一个IntroductionInterceptionAroundAdvisor,您可以使代理实现额外的接口。
ProxyFactory上还有一些方便的方法(继承自AdvisedSupport),可以添加其他通知类型,比如before和throw通知。AdvisedSupport是ProxyFactory和ProxyFactoryBean的超类。
注意:在大多数应用程序中,将AOP代理创建与IoC框架集成是最佳实践。我们建议您使用AOP将配置从Java代码外部化,通常您应该这样做。
==操作被建议的对象
无论您如何创建AOP代理,您都可以通过使用org.springframework.aop.framework.Advised接口来操作它们。任何AOP代理都可以被转换到这个接口,不管它实现了哪个其他接口。该接口包括以下方法:
Advisor[] getAdvisors();
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice) throws AopConfigException;
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
int indexOf(Advisor advisor);
boolean removeAdvisor(Advisor advisor) throws AopConfigException;
void removeAdvisor(int index) throws AopConfigException;
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
boolean isFrozen();
getAdvisors()方法为添加到工厂的每个Advisor、interceptor或其他建议类型返回一个Advisor。如果您添加了一个Advisor,那么在这个索引处返回的Advisor就是您添加的对象。如果您添加了拦截器或其他通知类型,Spring会用一个总是返回true的切入点将其封装到advisor中。因此,如果您添加了一个MethodInterceptor,那么为这个索引返回的advisor就是一个DefaultPointcutAdvisor,它返回您的MethodInterceptor和一个匹配所有类和方法的切入点。
addAdvisor()方法可用于添加任何顾问。通常,包含切入点和通知的advisor工具是通用的DefaultPointcutAdvisor,您可以将它用于任何建议或切入点(但不用于介绍)。
默认情况下,即使创建了代理,也可以添加或删除顾问或拦截器。惟一的限制是不可能添加或删除引入顾问,因为工厂中的现有代理没有显示接口更改。(你可以从工厂获得一个新的代理来避免这个问题。)
下面的例子显示了将AOP代理转换到被建议的接口,并检查和操作它的建议:
Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
注意:在生产环境中修改关于业务对象的建议是否明智(没有双关语的意思)是值得怀疑的,尽管毫无疑问存在合法的使用案例。但是,它在开发(例如,在测试中)中非常有用。我们有时发现,能够以拦截器或其他通知的形式添加测试代码,进入我们想要测试的方法调用是非常有用的。(例如,建议可以进入为该方法创建的事务中,在将事务标记为回滚之前,可以运行SQL来检查数据库更新是否正确。)
根据创建代理的方式,通常可以设置冻结标志。在这种情况下,被建议的isFrozen()方法返回true,任何通过添加或删除修改通知的尝试都会导致AopConfigException。冻结被建议对象的状态的能力在某些情况下是有用的(例如,防止调用代码删除安全拦截器)。
==使用“自动代理”功能
到目前为止,我们已经考虑了通过使用ProxyFactoryBean或类似的工厂bean显式地创建AOP代理。
Spring还允许我们使用“自动代理”bean定义,它可以自动代理选择的bean定义。这是在Spring的“bean后处理器”基础结构上构建的,它支持将任何bean定义修改为容器装载。
在这个模型中,您在XML bean定义文件中设置了一些特殊的bean定义来配置自动代理基础设施。这允许您声明有资格进行自动代理的目标。你不需要使用ProxyFactoryBean。
有两种方法:
- 通过使用在当前上下文中引用特定bean的自动代理创建者。
- 自动代理创建的一个特殊情况值得单独考虑:由源级元数据属性驱动的自动代理创建。
===自动代理Bean定义
本节讨论org.springframework.aop.frame .autoproxy包提供的自动代理创建者。
= = = = BeanNameAutoProxyCreator
BeanNameAutoProxyCreator类是一个BeanPostProcessor,它自动为名称与文字值或通配符匹配的bean创建AOP代理。下面的例子展示了如何创建一个BeanNameAutoProxyCreator bean:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="jdk*,onlyJdk"/>
<property name="interceptorNames">
<list>
<value>myInterceptor</value>
</list>
</property>
</bean>
与ProxyFactoryBean一样,它有一个interceptorNames属性,而不是一个拦截器列表,以允许原型顾问的正确行为。命名的“拦截器”可以是顾问或任何通知类型。
与一般的自动代理一样,使用BeanNameAutoProxyCreator的主要目的是一致地将相同的配置应用到多个对象上,而配置的体积最小。它是将声明性事务应用于多个对象的流行选择。
名称匹配的Bean定义(如前面示例中的jdkMyBean和onlyJdk)是带有目标类的普通旧Bean定义。AOP代理是由BeanNameAutoProxyCreator自动创建的。对所有匹配的bean应用相同的通知。注意,如果使用了建议器(而不是前面例子中的拦截器),切入点可能会以不同的方式应用于不同的bean。
= = = = DefaultAdvisorAutoProxyCreator
一个更一般和极其强大的自动代理的创建者是DefaultAdvisorAutoProxyCreator。这将自动地在当前上下文中应用合格的advisor工具,而不需要在auto-proxy advisor的bean定义中包含特定的bean名称。它提供了与BeanNameAutoProxyCreator相同的配置一致性和避免复制的优点。
使用这个机制包括:
- 指定DefaultAdvisorAutoProxyCreator bean定义。
- 在相同或相关上下文中指定任意数量的顾问。注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个切入点来评估,来检查每个通知到候选bean定义的资格。
DefaultAdvisorAutoProxyCreator自动计算每个advisor中包含的切入点,以查看它应该将什么(如果有的话)通知应用到每个业务对象(例如本例中的businessObject1和businessObject2)。
这意味着可以将任意数量的advisor工具自动应用于每个业务对象。如果任何顾问中的切入点都不匹配业务对象中的任何方法,则不代理该对象。当为新的业务对象添加bean定义时,如果需要,它们会自动代理。
一般来说,自动代理的优点是使调用者或依赖项不可能获得未通知的对象。在这个ApplicationContext上调用getBean(“businessObject1”)将返回一个AOP代理,而不是目标业务对象。(前面展示的“内部bean”习语也提供了这个好处。)
下面的例子创建了一个DefaultAdvisorAutoProxyCreator bean和本节讨论的其他元素:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>
<bean id="businessObject1" class="com.mycompany.BusinessObject1">
<!-- Properties omitted -->
</bean>
<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
如果您希望将相同的建议一致地应用于许多业务对象,则DefaultAdvisorAutoProxyCreator非常有用。一旦基础设施定义就绪,就可以添加新的业务对象,而无需包含特定的代理配置。您还可以轻松地删除其他方面(例如,跟踪或性能监视方面),只需对配置进行最小的更改。
DefaultAdvisorAutoProxyCreator提供了对过滤的支持(通过使用命名约定,只对特定的advisor进行评估,从而允许在同一工厂中使用多个不同配置的AdvisorAutoProxyCreators )和排序。顾问可以实现org.springframework.core.Ordered接口,以确保正确的顺序,如果这是一个问题。前面示例中使用的TransactionAttributeSourceAdvisor有一个可配置的订单值。默认设置是无序的。
==使用TargetSource实现
Spring提供了TargetSource的概念,在org.springframe .aop中表示。TargetSource接口。这个接口负责返回实现连接点的“目标对象”。每次AOP代理处理一个方法调用时,都会请求TargetSource实现一个目标实例。
使用Spring AOP的开发人员通常不需要直接使用TargetSource实现,但是这提供了支持池、热可切换和其他复杂目标的强大方法。例如,通过使用池来管理实例,合用TargetSource可以为每次调用返回不同的目标实例。
如果不指定TargetSource,则使用默认实现包装本地对象。每次调用都返回相同的目标(正如您所期望的)。
本节的其余部分将描述Spring提供的标准目标源以及如何使用它们。
注意:使用自定义目标源时,目标通常需要是原型,而不是单例bean定义。这允许Spring在需要时创建新的目标实例。
===可热插拔的目标源
org.springframework.aop.target。HotSwappableTargetSource的存在是为了让AOP代理的目标被切换,同时让调用者保留对它的引用。
更改目标源的目标将立即生效。HotSwappableTargetSource是线程安全的。
您可以通过使用HotSwappableTargetSource上的swap()方法来更改目标,如下面的示例所示:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
下面的例子展示了所需的XML定义:
<bean id="initialTarget" class="mycompany.OldTarget"/>
<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
<constructor-arg ref="initialTarget"/>
</bean>
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
前面的swap()调用将更改可切换bean的目标。持有对该bean的引用的客户机不知道更改,但立即开始触及新目标。
虽然本例没有添加任何建议(使用TargetSource不需要添加建议),但是任何TargetSource都可以与任意建议结合使用。
===Pooling Target Sources
使用池目标源提供了与无状态会话ejb类似的编程模型,其中维护了一个相同实例池,方法调用将释放池中的对象。
Spring池和SLSB池的一个重要区别是,Spring池可以应用于任何POJO。与Spring一般情况一样,这种服务可以以一种非侵入性的方式应用。
Spring支持Commons Pool 2.2,它提供了一个相当高效的池实现。您需要应用程序的类路径上的公共池Jar来使用这个特性。您还可以子类化org.springframework.aop.target.AbstractPoolingTargetSource支持任何其他池API。
注意:还支持Commons Pool 1.5+,但从Spring Framework 4.2开始就不支持它了。
下面的清单显示了一个配置示例:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
<property name="maxSize" value="25"/>
</bean>
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="poolTargetSource"/>
<property name="interceptorNames" value="myInterceptor"/>
</bean>
请注意,目标对象(前面示例中的businessObjectTarget)必须是原型。这允许PoolingTargetSource实现创建目标的新实例,以便根据需要扩展池。请参阅 javadoc of AbstractPoolingTargetSource
和希望用于了解其属性的具体子类。maxSize是最基本的,并且总是保证出现。
在本例中,myInterceptor是需要在相同的IoC上下文中定义的拦截器的名称。但是,不需要指定拦截器来使用池。如果只想要池而不想要其他通知,则根本不要设置interceptorNames属性。
您可以配置Spring,使其能够将任何池中的对象强制转换为org.springframework.aop.target.PoolingConfig接口,通过介绍公开关于池的配置和当前大小的信息。你需要定义一个顾问类似于以下:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
这个advisor工具是通过调用AbstractPoolingTargetSource类上的一个方便方法获得的,因此使用了MethodInvokingFactoryBean。这个advisor工具的名称(这里是poolConfigAdvisor)必须位于暴露池对象的ProxyFactoryBean中的拦截器名称列表中。
cast的定义如下:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
注意:通常不需要使用无状态服务对象池。我们不认为这应该是默认的选择,因为大多数无状态对象天生就是线程安全的,而且如果缓存了资源,实例池就有问题。
通过使用自动代理,可以使用更简单的池。您可以设置任何自动代理创建者使用的TargetSource实现。
===Prototype Target Sources
设置“原型”目标源类似于设置池目标源。在这种情况下,在每次方法调用时都会创建一个新的目标实例。尽管在现代JVM中创建新对象的成本并不高,但连接新对象(满足其IoC依赖项)的成本可能更高。因此,如果没有很好的理由,您不应该使用这种方法。
为此,您可以修改前面显示的poolTargetSource定义,如下所示(为了清晰起见,我们还更改了名称):
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
惟一的属性是目标bean的名称。在TargetSource实现中使用继承来确保一致的命名。与池目标源一样,目标bean必须是原型bean定义。
=== ThreadLocal目标源
如果需要为每个传入请求(即每个线程)创建一个对象,那么ThreadLocal目标源非常有用。ThreadLocal的概念提供了一个整个jdk范围的工具来透明地在线程旁边存储资源。设置ThreadLocalTargetSource与为其他类型的目标源所做的解释基本相同,如下面的示例所示:
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
注意:ThreadLocal实例在多线程和多类加载器环境中不正确地使用时会带来严重的问题(可能导致内存泄漏)。您应该始终考虑在其他类中包装threadlocal,并且永远不要直接使用threadlocal本身(除了在包装器类中)。另外,您应该始终记住正确地设置和取消设置(后者仅涉及对ThreadLocal.set(null)的调用)线程的本地资源。在任何情况下都应该取消设置,因为不取消设置可能会导致有问题的行为。Spring的ThreadLocal支持为您做到了这一点,并且应该始终考虑使用ThreadLocal实例而不使用其他适当的处理代码。
==定义新的Advice类型
Spring AOP被设计成可扩展的。虽然侦听实现策略目前在内部使用,但是除了围绕着advice、before、throw advice和after return advice进行侦听外,还可以支持任意的advice类型。
org.springframework.aop.framework.adapter包是一个SPI包,它允许在不更改核心框架的情况下添加新的自定义通知类型。定制通知类型的惟一约束是它必须实现org.aopalliance.aop.Advice接口。
有关更多信息,请参见org.springframework.aop.framework.adapter javadoc
。
= Null-safety
虽然Java不允许您用它的类型系统来表达null-safety,但是Spring框架现在在org.springframework中提供了以下注释。让你声明api和字段为空的lang包:
- @Nullable:用于指示特定参数、返回值或字段可以为空的注释。
- @NonNull:用于指示特定参数、返回值或字段不能为空的注释(在参数/返回值和@NonNullApi和@NonNullFields分别应用的字段上不需要)。
- @NonNullApi:包级别的注释,声明非null作为参数和返回值的默认语义。
- @NonNullFields:包级别的注释,声明非null作为字段的默认语义。
Spring框架本身利用了这些注释,但是也可以在任何基于Spring的Java项目中使用它们来声明空安全的api和可选的空安全字段。泛型类型参数、可变参数和数组元素可空性还不支持,但应该在即将发布的版本中支持,有关最新信息,请参阅 SPR-15942。可空性声明将在Spring框架版本之间进行微调,包括小版本。方法体内部使用的类型的可空性超出了该特性的范围。
注意:反应器和Spring Data等其他公共库提供了空安全api,它们使用类似的可空性安排,为Spring应用程序开发人员提供了一致的整体体验。
= =用例
除了为Spring Framework API可空性提供显式声明外,IDE(如IDEA或Eclipse)还可以使用这些注释来提供与空安全相关的有用警告,以避免在运行时出现NullPointerException。
它们还用于使Kotlin项目中的Spring API空安全,因为Kotlin本身支持空安全。更多细节可以在Kotlin支持文档中找到。
= = JSR-305元注释
Spring注释使用 JSR 305注释(一个休眠但广泛传播的JSR)进行元注释。JSR-305元注释允许IDEA或Kotlin等工具供应商以通用的方式提供空安全支持,而无需对Spring注释进行硬编码支持。
为了利用Spring空安全API,没有必要也不建议在项目类路径中添加JSR-305依赖项。只有基于spring的库在其代码库中使用空安全注释的项目才应该添加com.google.code.findbugs:jsr305:3.0.2,其中只提供编译级配置或Maven,以避免编译警告。
=数据缓冲区和编解码器
Java NIO提供了ByteBuffer,但是许多库在其上构建自己的字节缓冲区API,特别是对于重用缓冲区和/或使用直接缓冲区有利于提高性能的网络操作。例如,Netty具有ByteBuf层次结构,Undertow使用XNIO, Jetty使用池化的字节缓冲区,并释放回调,等等。spring-core模块提供了一组抽象来处理各种字节缓冲区api,如下所示:
-
[databuffers-factory] abstracts the creation of a data buffer.
-
[databuffers-buffer] represents a byte buffer, which may be pooled.
-
[databuffers-utils] offers utility methods for data buffers.
-
[Codecs] decode or encode streams data buffer streams into higher level objects.
= = DataBufferFactory
DataBufferFactory是用来创建数据缓冲区的两种方式之一:
- 分配一个新的数据缓冲区,可以预先指定容量(如果已知),这是更有效的,即使DataBuffer的实现可以根据需要增减。
- 封装现有的byte[]或java.nio。ByteBuffer,它用一个DataBuffer实现装饰给定的数据,并且不涉及分配。
请注意,WebFlux应用程序并不直接创建DataBufferFactory,而是通过客户端上的ServerHttpResponse或ClientHttpRequest访问它。工厂的类型取决于底层客户端或服务器,例如NettyDataBufferFactory用于反应器Netty, DefaultDataBufferFactory用于其他。
= = DataBuffer
DataBuffer接口提供了与java.nio类似的操作。ByteBuffer也带来了一些额外的好处,其中一些是受到Netty ByteBuf的启发。以下是部分福利:
- 读写具有独立的位置,即不需要调用flip()来交替读写。
- 通过java.lang.StringBuilder,可以根据需要扩展容量。
- 缓冲池和引用计数通过[databuffers- bufferpooling]。
- 将缓冲区视为java.nio。ByteBuffer、InputStream或OutputStream。
- 确定给定字节的索引或最后一个索引。
= = PooledDataBuffer
正如ByteBuffer的Javadoc中所解释的,字节缓冲区 ByteBuffer可以是直接的,也可以是非直接的。直接缓冲区可能位于Java堆之外,这消除了本地I/O操作的复制需求。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但是创建和释放它们也更昂贵,这就产生了池缓冲区的想法。
PooledDataBuffer是DataBuffer的一个扩展,它可以帮助进行引用计数,这对于字节缓冲池非常重要。它是如何工作的?当一个PooledDataBuffer被分配时,引用计数为1。调用retain()递增计数,而调用release()递减计数。只要计数大于0,就保证不会释放缓冲区。当计数减少到0时,可以释放池中的缓冲区,这实际上可能意味着为缓冲区保留的内存返回到内存池。
注意,与直接在PooledDataBuffer上操作不同,在大多数情况下,更好的方法是使用DataBufferUtils中的便利方法,这些方法仅在PooledDataBuffer的实例上对DataBuffer应用release或retain。
= = DataBufferUtils
DataBufferUtils提供了许多实用程序方法来操作数据缓冲区:
- 如果底层字节缓冲区API支持的话,可以通过复合缓冲区将数据缓冲区流连接到单个缓冲区(可能是零拷贝)。
- 将InputStream或NIO通道转换为Flux<DataBuffer>,反之,将Publisher<DataBuffer>转换为OutputStream或NIO通道。
- 方法来释放或保留一个DataBuffer(如果缓冲区是PooledDataBuffer的实例)。
- 跳过或从一个字节流中取出一个特定的字节数。
= =编解码器
- org.springframework.core.codes包提供以下策略接口:
- 编码器将发布服务器<T>编码到数据缓冲区流中。
- 解码器将出版商<DataBuffer>解码成更高级别对象流。
spring-core模块提供了byte[]、ByteBuffer、DataBuffer、Resource以及字符串编码器和解码器实现。spring-web模块添加Jackson JSON、Jackson Smile、JAXB2、协议缓冲区和其他编码器和解码器。参见WebFlux部分的Codecs。
==使用DataBuffer
在使用数据缓冲区时,必须特别注意确保释放缓冲区,因为它们可能被池化 pooled。我们将使用编解码器来说明它是如何工作的,但是这些概念适用于更广泛的情况。让我们看看编解码器必须在内部做些什么来管理数据缓冲区。
解码器是在创建更高级别对象之前最后读取输入数据缓冲区的,因此它必须按如下方式释放这些缓冲区:
- 如果解码器只是读取每个输入缓冲区并准备立即释放它,那么它可以通过databufferutil .release(dataBuffer)来做。
- 如果解码器使用Flux或Mono操作符(如flatMap、reduce)和其他在内部预取和缓存数据项的操作符,或者使用filter、skip和其他遗漏数据项的操作符,那么doonreject (PooledDataBuffer类,DataBufferUtils::release)必须添加到组合链中,以确保在丢弃缓冲区之前释放这些缓冲区,也可能作为错误或取消信号的结果。
- 如果解码器以任何其他方式保留一个或多个数据缓冲区,则必须确保在完全读取时释放它们,或者在读取和释放缓存的数据缓冲区之前发生错误或取消信号时释放它们。
注意,DataBufferUtils#join提供了一种安全而有效的方法,可以将数据缓冲区流聚合到单个数据缓冲区中。同样,skipUntilByteCount和takeUntilByteCount也是解码器使用的附加安全方法。
编码器分配其他人必须读取(并释放)的数据缓冲区。所以一个编码器没有太多事情要做。然而,如果在用数据填充缓冲区时发生序列化错误,编码器必须注意释放数据缓冲区。例如:
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
// serialize and populate buffer..
release = false;
}
finally {
if (release) {
DataBufferUtils.release(buffer);
}
}
return buffer;
编码器的使用者负责释放它接收到的数据缓冲区。在WebFlux应用程序中,编码器的输出用于向HTTP服务器响应或客户端HTTP请求写入,在这种情况下,释放数据缓冲区是向服务器响应或客户端请求写入代码的责任。
注意,在Netty上运行时,有用于排除缓冲区泄漏故障的调试选项。
=附录
= = XML模式
附录的这一部分列出了与核心容器相关的XML模式。
=== util模式
顾名思义,util标记处理常见的实用程序配置问题,如配置集合、引用常量等。要在util模式中使用标记,您需要在Spring XML配置文件的顶部有以下序言(代码片段中的文本引用了正确的模式,因此可以使用util名称空间中的标记):
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!-- bean definitions here -->
</beans>
==== 使用 <util:constant/>
考虑以下bean定义:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
前面的配置使用Spring FactoryBean实现(FieldRetrievingFactoryBean)将bean上的隔离属性的值设置为java.sql.Connection.TRANSACTION_SERIALIZABLE常数的值。这一切都很好,但是它很冗长,并且(不必要地)向最终用户暴露了Spring的内部管道。
下面这个基于XML模式的版本更简洁,清楚地表达了开发人员的意图(“注入这个常量值”),并且读起来更好:
<bean id="..." class="...">
<property name="isolation">
<util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</property>
</bean>
====从字段值设置Bean属性或构造函数参数
FieldRetrievingFactoryBean是一个FactoryBean,它检索静态或非静态字段值。它通常用于检索公共静态final常量,然后可以用来设置另一个bean的属性值或构造函数参数。
下面的示例展示了如何使用staticField属性公开静态字段:
<bean id="myField"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
<property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>
还有一个方便的用法表单,其中静态字段被指定为bean名称,如下面的示例所示:
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
这并意味着不再有任何选择bean id是什么(任何其他bean,指的是它也有使用这个名称),但这种形式是非常简洁的定义和方便使用以来作为内在bean id不需要指定的bean引用,如以下示例所示:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
您还可以访问另一个bean的非静态(实例)字段,如FieldRetrievingFactoryBean类的API文档中所述。
在Spring中很容易将枚举值作为属性或构造函数参数注入bean。实际上,您不需要做任何事情或者了解任何关于Spring内部的内容(甚至不需要了解诸如FieldRetrievingFactoryBean之类的类)。下面的枚举示例演示了注入enum值是多么容易:
package javax.persistence;
public enum PersistenceContextType {
TRANSACTION,
EXTENDED
}
现在考虑以下PersistenceContextType类型的setter和相应的bean定义:
package example;
public class Client {
private PersistenceContextType persistenceContextType;
public void setPersistenceContextType(PersistenceContextType type) {
this.persistenceContextType = type;
}
}
<bean class="example.Client">
<property name="persistenceContextType" value="TRANSACTION"/>
</bean>
==== Using <util:property-path/>
考虑下面的例子:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
前面的配置使用了一个Spring FactoryBean实现(PropertyPathFactoryBean)来创建一个名为testBean(类型为int)的bean。其值等于testBean bean的年龄属性。
现在考虑下面的例子,它添加了一个<util:property-path/>元素:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>
<property-path/>元素的path属性的值遵循bean . beanproperty的形式。在本例中,它获取名为testBean的bean的年龄属性。age属性的值是10。
====使用<util: Property -path/>设置Bean属性或构造函数参数
PropertyPathFactoryBean是一个FactoryBean,它计算给定目标对象上的属性路径。目标对象可以直接指定,也可以通过bean名称指定。然后可以在另一个bean定义中使用这个值作为属性值或构造函数参数。
下面的例子显示了对另一个bean按名称使用的路径:
// target bean to be referenced by name
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
// results in 11, which is the value of property 'spouse.age' of bean 'person'
<bean id="theAge"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetBeanName" value="person"/>
<property name="propertyPath" value="spouse.age"/>
</bean>
在下面的示例中,将根据内部bean计算路径
<!-- results in 12, which is the value of property 'age' of the inner bean -->
<bean id="theAge"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetObject">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="12"/>
</bean>
</property>
<property name="propertyPath" value="age"/>
</bean>
还有一个快捷表单,其中bean名称是属性路径。下面的例子显示了快捷表单:
<!-- results in 10, which is the value of property 'age' of bean 'person' -->
<bean id="person.age"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
这种形式意味着在bean的名称上没有选择。对它的任何引用也必须使用相同的id,即路径。如果作为内部bean使用,则根本不需要引用它,如下面的示例所示:
<bean id="..." class="...">
<property name="age">
<bean id="person.age"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
</property>
</bean>
您可以在实际定义中专门设置结果类型。对于大多数用例来说,这并不是必需的,但是有时候它是有用的。有关此特性的更多信息,请参见javadoc。
==== Using <util:properties/>
考虑下面例子:
<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<bean id="jdbcConfiguration" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="classpath:com/foo/jdbc-production.properties"/>
</bean>
前面的配置使用了一个Spring FactoryBean实现(PropertiesFactoryBean)来实例化java.util。属性实例,其值从提供的资源位置加载)。
下面的例子使用了一个util:properties元素来进行更简洁的表示:
<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<util:properties id="jdbcConfiguration" location="classpath:com/foo/jdbc-production.properties"/>
==== Using <util:list/>
考虑下面例子:
<!-- creates a java.util.List instance with values loaded from the supplied 'sourceList' -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
<property name="sourceList">
<list>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</list>
</property>
</bean>
前面的配置使用了一个Spring FactoryBean实现(ListFactoryBean)来创建一个java.util.List实例。列出实例并使用从提供的sourceList获取的值初始化它。
下面的例子使用了<util:list/>元素来进行更简洁的表示:
<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</util:list>
您还可以显式地控制由<util: List />元素上的List -class属性实例化和填充的列表的确切类型。例如,如果我们真的需要java.util。要实例化LinkedList,我们可以使用以下配置:
<util:list id="emails" list-class="java.util.LinkedList">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>d'[email protected]</value>
</util:list>
如果没有提供List -class属性,则容器选择List实现。
==== Using <util:map/>
考虑下面例子:
<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="pechorin" value="[email protected]"/>
<entry key="raskolnikov" value="[email protected]"/>
<entry key="stavrogin" value="[email protected]"/>
<entry key="porfiry" value="[email protected]"/>
</map>
</property>
</bean>
前面的配置使用了一个Spring FactoryBean实现(MapFactoryBean)来创建一个java.util.Map实例。映射实例初始化的键值对取自提供的“sourceMap”。
下面的例子使用了一个<util:map/>元素来进行更简洁的表示:
<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
<entry key="pechorin" value="[email protected]"/>
<entry key="raskolnikov" value="[email protected]"/>
<entry key="stavrogin" value="[email protected]"/>
<entry key="porfiry" value="[email protected]"/>
</util:map>
您还可以显式地控制由<util: Map />元素上的' Map -class'属性实例化和填充的映射的确切类型。例如,如果我们真的需要java.util.TreeMap实例化,我们可以使用以下配置:
<util:map id="emails" map-class="java.util.TreeMap">
<entry key="pechorin" value="[email protected]"/>
<entry key="raskolnikov" value="[email protected]"/>
<entry key="stavrogin" value="[email protected]"/>
<entry key="porfiry" value="[email protected]"/>
</util:map>
如果没有提供“map -class”属性,则容器选择Map实现。
==== Using <util:set/>
例子:
<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
<property name="sourceSet">
<set>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</set>
</property>
</bean>
前面的配置使用了一个Spring FactoryBean实现(SetFactoryBean)来创建一个java.util.Set。设置实例初始化的值取自提供的sourceSet。
下面的例子使用了一个<util:set/>元素来进行更简洁的表示:
<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</util:set>
您还可以显式地控制通过使用<util:set/>元素上的set-class属性来实例化和填充的Set的确切类型。例如,如果我们真的需要java.util.TreeSet实例化,我们可以使用以下配置:
<util:set id="emails" set-class="java.util.TreeSet">
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</util:set>
如果没有提供set-class属性,则容器选择Set实现。
=== aop模式
aop标记处理在Spring中配置aop的所有事情,包括Spring自己的基于代理的aop框架和Spring与AspectJ aop框架的集成。这些标签在Spring面向方面编程一章中全面介绍Aspect Oriented Programming with Spring。
出于完整性的考虑,要在aop模式中使用标记,需要在Spring XML配置文件的顶部有以下序言(代码片段中的文本引用了正确的模式,因此aop名称空间中的标记对您是可用的):
<?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: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 definitions here -->
</beans>
===context
模式context
标记处理与管道相关的ApplicationContext配置——也就是说,通常不是对最终用户很重要的bean,而是在Spring中执行大量“繁重”工作的bean,比如BeanfactoryPostProcessors。下面的代码片段引用了正确的模式,因此上下文名称空间中的元素对您是可用的:
<?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">
<!-- bean definitions here -->
</beans>
= = = = 使用< property-placeholder / >
这个元素激活${…}占位符的替换,这些占位符根据指定的属性文件解析(作为Spring资源位置Spring resource location)。此元素是一种方便的机制,可为您设置PropertySourcesPlaceholderConfigurer。如果您需要对特定的PropertySourcesPlaceholderConfigurer设置有更多的控制,您可以自己显式地将它定义为一个bean。
==== Using <annotation-config/>
这个元素激活Spring基础结构来检测bean类中的注释:
-
Spring’s
@Configuration
model -
@Autowired
/@Inject
and@Value
-
JSR-250’s
@Resource
,@PostConstruct
and@PreDestroy
(if available) -
JPA’s
@PersistenceContext
and@PersistenceUnit
(if available) -
Spring’s
@EventListener
或者,您可以选择为这些注释显式地激活各个BeanPostProcessors
。
注意:此元素不激活对Spring的@Transactional注释的处理;为此,可以使用<tx:annotation-driven/>
元素。类似地,Spring的 caching annotations也需要显式地 enabled。
==== Using <component-scan/>
This element is detailed in the section on annotation-based container configuration.
==== Using <load-time-weaver/>
This element is detailed in the section on load-time weaving with AspectJ in the Spring Framework.
==== Using <spring-configured/>
This element is detailed in the section on using AspectJ to dependency inject domain objects with Spring.
==== Using <mbean-export/>
This element is detailed in the section on configuring annotation-based MBean export.
=== The Beans Schema
最后,我们在beans模式中有元素。这些元素从框架的一开始就在Spring中。这里没有显示bean模式中各种元素的示例,因为它们在依赖项和配置中得到了全面的详细介绍 dependencies and configuration in detail(实际上,在那一章中)。
注意,您可以向<bean/> XML定义添加零个或多个键值对。如何处理这些额外的元数据完全取决于您自己的定制逻辑(因此,通常只有在您按照附录[xml-custom]中描述的那样编写自己的定制元素时才有用)。
下面的例子显示了<bean/>环境中的<meta/>元素(注意,如果没有任何逻辑来解释它,元数据实际上是无用的)。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="foo" class="x.y.Foo">
<meta key="cacheName" value="foo"/>
<property name="name" value="Rick"/>
</bean>
</beans>
在前面的例子中,您可以假设有一些逻辑使用bean定义并设置一些使用提供的元数据的缓存基础结构。
== XML Schema Authoring
自2.0版以来,Spring提供了一种机制,用于将基于模式的扩展添加到基本的Spring XML格式中,以定义和配置bean。本节介绍如何编写自己的自定义XML bean定义解析器,并将这些解析器集成到Spring IoC容器中。
为了方便编写使用模式感知XML编辑器的配置文件,Spring的可扩展XML配置机制基于XML模式。如果您不熟悉Spring标准发行版附带的当前XML配置扩展,那么您应该首先阅读名为 [xsd-config]的附录。
要创建新的XML配置扩展:
- Author一个XML模式来描述您的自定义元素。
- Code编写自定义NamespaceHandler实现的代码。
- Code编写一个或多个BeanDefinitionParser实现(这是完成真正工作的地方)。
- Register用Spring注册您的新工件。
对于一个统一的示例,我们创建一个XML扩展(一个自定义XML元素),它允许我们配置SimpleDateFormat类型的对象(来自java.text
package)。完成后,我们将能够定义SimpleDateFormat类型的bean定义,如下所示:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
(我们将在本附录后面提供更详细的示例。第一个简单示例的目的是指导您完成定制扩展的基本步骤。
===编写模式
为Spring的IoC容器创建一个XML配置扩展,首先要编写一个XML模式来描述扩展。在我们的例子中,我们使用以下模式来配置SimpleDateFormat对象:
<!-- myns.xsd (inside package org/springframework/samples/xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.example/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.example/schema/myns"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType"> //1
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
//1 指示的行包含所有可识别标记的扩展基(意味着它们有一个id属性,我们可以将其用作容器中的bean标识符)。我们可以使用这个属性,因为我们导入了spring提供的bean名称空间。
前面的模式允许我们使用<myns:dateformat/>元素直接在XML应用程序上下文文件中配置SimpleDateFormat对象,如下面的示例所示:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
注意,在创建基础设施类之后,前面的XML片段与下面的XML片段基本相同:
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>
前面两个代码片段中的第二个代码片段在容器中创建了一个bean(通过SimpleDateFormat类型的名称dateFormat标识),并设置了两个属性。
注意:基于模式的配置格式创建方法允许与具有模式感知XML编辑器的IDE进行紧密集成。通过使用正确编写的模式,可以使用自动补全让用户在枚举中定义的几个配置选项之间进行选择。
===Coding a NamespaceHandler
除了模式之外,我们还需要一个NamespaceHandler来解析Spring在解析配置文件时遇到的这个特定名称空间的所有元素。对于本例,NamespaceHandler应该负责解析myns:dateformat元素。
NamespaceHandler接口有三个特性:
- init():允许初始化NamespaceHandler,并在使用该处理程序之前由Spring调用。
- BeanDefinition parse(Element, ParserContext):当Spring遇到顶级元素(不是嵌套在bean定义或不同名称空间中)时调用。这个方法本身可以注册bean定义,返回一个bean定义,或者两者都可以。
- BeanDefinitionHolder装饰(Node, BeanDefinitionHolder, ParserContext):当Spring遇到不同名称空间的属性或嵌套元素时调用。一个或多个bean定义的修饰(例如)与Spring支持的范围一起使用scopes that Spring supports。我们首先突出显示一个简单的示例,不使用装饰,然后在一个更高级的示例中显示装饰。
虽然您可以编写自己的NamespaceHandler整个名称空间(因此提供代码解析每一个元素的名称空间),通常情况下,每个顶级XML元素在Spring XML配置文件的结果在一个bean定义(在我们的例子中,一个< myns: dateformat / >元素的结果在一个SimpleDateFormat bean定义)。Spring提供了许多支持此场景的方便类。在下面的例子中,我们使用了NamespaceHandlerSupport类:
package org.springframework.samples.xml;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
您可能会注意到,这个类中实际上没有很多解析逻辑。实际上,NamespaceHandlerSupport类有一个内置的委托概念。它支持注册任意数量的BeanDefinitionParser实例,当需要解析名称空间中的元素时,它将委托给这些实例。这种清晰的关注点分离让NamespaceHandler处理解析其名称空间中所有自定义元素的编排,同时委托BeanDefinitionParsers来完成XML解析的繁重工作。这意味着每个BeanDefinitionParser只包含解析单个自定义元素的逻辑,我们将在下一步中看到这一点。
= = =使用BeanDefinitionParser
如果NamespaceHandler遇到已映射到特定bean定义解析器的类型的XML元素(本例中为dateformat),则使用BeanDefinitionParser。换句话说,BeanDefinitionParser负责解析模式中定义的一个不同的顶级XML元素。在解析器中,我们可以访问XML元素(也可以访问它的子元素),这样我们就可以解析定制的XML内容,如下例所示:
package org.springframework.samples.xml;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import java.text.SimpleDateFormat;
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { //1
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; //2
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.addConstructorArgValue(pattern);
// this however is an optional property
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
//1 我们使用spring提供的AbstractSingleBeanDefinitionParser来处理创建单个bean定义的大量基本工作。
//2 我们为AbstractSingleBeanDefinitionParser超类提供单个BeanDefinition表示的类型。
在这个简单的例子中,这就是我们需要做的。我们单个bean定义的创建由AbstractSingleBeanDefinitionParser超类处理,提取和设置bean定义的唯一标识符也是如此。
===注册处理程序和模式
编码完成了。剩下要做的就是让Spring XML解析基础设施知道我们的自定义元素。为此,我们将自定义namespaceHandler和自定义XSD文件注册到两个特殊用途的属性文件中。这些属性文件都放在应用程序的META-INF目录中,例如,可以与JAR文件中的二进制类一起发布。Spring XML解析基础结构通过使用这些特殊的属性文件来自动获取新的扩展,这些文件的格式将在下两个小节中详细介绍。
==== Writing META-INF/spring.handlers
名为spring.handlers的属性文件包含XML模式uri到名称空间处理程序类的映射。对于我们的例子,我们需要写以下内容:
http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler
(:字符是Java属性格式中的有效分隔符,因此:URI中的字符需要用反斜杠进行转义。)
键-值对的第一部分(键)是与自定义名称空间扩展相关联的URI,需要与targetNamespace属性的值完全匹配,正如在自定义XSD模式中指定的那样。
==== Writing 'META-INF/spring.schemas'
名为spring.schemas的属性文件包含XML模式位置(与模式声明一起在XML文件中引用,这些文件使用模式作为xsi:schemaLocation属性的一部分)到类路径资源的映射。需要这个文件是为了防止Spring必须使用默认的EntityResolver(需要Internet访问来检索模式文件)。如果在这个属性文件中指定映射,Spring将搜索模式(在本例中是myns)。类路径上的org.springframework.samples.xml包中的xsd)。下面的代码片段显示了我们需要为自定义模式添加的行:
http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
(记住:字符必须转义)
建议您将XSD文件(或多个文件)与NamespaceHandler和BeanDefinitionParser类一起部署在类路径上。
===在Spring XML配置中使用自定义扩展
使用您自己实现的自定义扩展与使用Spring提供的“自定义”扩展没有什么不同。下面的示例使用了在Spring XML配置文件中前面步骤中开发的自定义<dateformat/>元素:
<?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:myns="http://www.mycompany.example/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.example/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">
<!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
<bean id="jobDetailTemplate" abstract="true">
<property name="dateFormat">
<!-- as an inner bean -->
<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
</property>
</bean>
</beans>
===更详细的例子
本节将提供一些更详细的自定义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:foo="http://www.foo.example/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.foo.example/schema/component http://www.foo.example/schema/component/component.xsd">
<foo:component id="bionic-family" name="Bionic-1">
<foo:component name="Mother-1">
<foo:component name="Karate-1"/>
<foo:component name="Sport-1"/>
</foo:component>
<foo:component name="Rock-1"/>
</foo:component>
</beans>
前面的配置将自定义扩展彼此嵌套在一起。实际上由<foo:component/>元素配置的类是component类(在下一个示例中显示)。请注意,组件类如何不公开组件属性的setter方法。这使得通过使用setter注入来为组件类配置bean定义变得困难(甚至不可能)。下面的清单显示了组件类:
package com.foo;
import java.util.ArrayList;
import java.util.List;
public class Component {
private String name;
private List<Component> components = new ArrayList<Component> ();
// mmm, there is no setter method for the 'components'
public void addComponent(Component component) {
this.components.add(component);
}
public List<Component> getComponents() {
return components;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这个问题的典型解决方案是创建一个自定义FactoryBean,它为组件属性公开一个setter属性。下面的清单显示了这样一个自定义FactoryBean:
package com.foo;
import org.springframework.beans.factory.FactoryBean;
import java.util.List;
public class ComponentFactoryBean implements FactoryBean<Component> {
private Component parent;
private List<Component> children;
public void setParent(Component parent) {
this.parent = parent;
}
public void setChildren(List<Component> children) {
this.children = children;
}
public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}
public Class<Component> getObjectType() {
return Component.class;
}
public boolean isSingleton() {
return true;
}
}
这工作得很好,但是它向最终用户暴露了许多Spring管道。我们要做的是编写一个自定义扩展来隐藏所有的Spring管道。如果我们坚持前面描述的步骤,我们首先创建XSD模式来定义自定义标记的结构,如下面的清单所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.example/schema/component"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/schema/component"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="component">
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="component"/>
</xsd:choice>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="name" use="required" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
同样遵循前面描述的过程the process described earlier,然后我们创建一个自定义NamespaceHandler:
package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class ComponentNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}
}
接下来是定制的BeanDefinitionParser。请记住,我们正在创建一个描述ComponentFactoryBean的BeanDefinition。下面的清单显示了我们自定义的BeanDefinitionParser实现:
package com.foo;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.List;
public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
return parseComponentElement(element);
}
private static AbstractBeanDefinition parseComponentElement(Element element) {
BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
factory.addPropertyValue("parent", parseComponent(element));
List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
if (childElements != null && childElements.size() > 0) {
parseChildComponents(childElements, factory);
}
return factory.getBeanDefinition();
}
private static BeanDefinition parseComponent(Element element) {
BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
component.addPropertyValue("name", element.getAttribute("name"));
return component.getBeanDefinition();
}
private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}
最后,通过修改META-INF/spring.handlers
和META-INF/spring.schemas文件
,需要将各种构件注册到Spring XML基础结构中。如下:
# in 'META-INF/spring.handlers'
http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd
===“Normal”元素的自定义属性
编写您自己的自定义解析器和相关的构件并不困难。然而,有时这样做是不对的。考虑这样一个场景,您需要向已经存在的bean定义添加元数据。在这种情况下,您当然不希望必须编写自己的整个自定义扩展。相反,您只想向现有的bean定义元素添加一个附加属性。
通过另一个示例,假设您为访问集群JCache的服务对象(它不知道)定义了一个bean定义,并且您希望确保命名的JCache实例在周围的集群中被急切地启动。下面的清单显示了这样一个定义:
<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
jcache:cache-name="checking.account">
<!-- other dependencies here... -->
</bean>
然后,我们可以在解析“jcache:cache-name”属性时创建另一个bean定义。然后,这个BeanDefinition为我们初始化命名的JCache。我们还可以修改'checkingAccountService'的现有BeanDefinition,使其依赖于这个新的jcache初始化的BeanDefinition。下面的清单显示了我们的JCacheInitializer:
package com.foo;
public class JCacheInitializer {
private String name;
public JCacheInitializer(String name) {
this.name = name;
}
public void initialize() {
// lots of JCache API calls to initialize the named cache...
}
}
现在我们可以进入自定义扩展了。首先,我们需要创建描述自定义属性的XSD模式,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.example/schema/jcache"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/schema/jcache"
elementFormDefault="qualified">
<xsd:attribute name="cache-name" type="xsd:string"/>
</xsd:schema>
接下来,我们需要创建相关联的NamespaceHandler,如下所示:
package com.foo;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class JCacheNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
new JCacheInitializingBeanDefinitionDecorator());
}
}
接下来,我们需要创建解析器。注意,在本例中,因为我们要解析一个XML属性,所以我们编写了一个BeanDefinitionDecorator而不是BeanDefinitionParser。下面的清单显示了我们的BeanDefinitionDecorator实现:
package com.foo;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
ParserContext ctx) {
String initializerBeanName = registerJCacheInitializer(source, ctx);
createDependencyOnJCacheInitializer(holder, initializerBeanName);
return holder;
}
private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
String initializerBeanName) {
AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
String[] dependsOn = definition.getDependsOn();
if (dependsOn == null) {
dependsOn = new String[]{initializerBeanName};
} else {
List dependencies = new ArrayList(Arrays.asList(dependsOn));
dependencies.add(initializerBeanName);
dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
}
definition.setDependsOn(dependsOn);
}
private String registerJCacheInitializer(Node source, ParserContext ctx) {
String cacheName = ((Attr) source).getValue();
String beanName = cacheName + "-initializer";
if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
initializer.addConstructorArg(cacheName);
ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
}
return beanName;
}
}
最后,我们需要向Spring XML基础设施注册各种构件,通过修改META-INF/spring.handlers和
META-INF/spring.schemas文件
, as follows:
# in 'META-INF/spring.handlers'
http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas'
http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd